본문 바로가기
Book

[독서 기록] 헤드퍼스트 디자인 패턴 6장 | 커맨트 패턴 - 호출 캡슐화하기

by Renechoi 2022. 11. 15.
 
헤드 퍼스트 디자인 패턴
이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈헤드 퍼스트〉 시리즈! 하나의 패턴에 하나의 이야기를 담았습니다. 틀에 박히지 않아 지루할 틈이 없는 구성과 친구와 이야기하듯 편안한 대화체로 이야기를 풀어냅니다. 이야기 속에 다양한 방법으로 해결할 수 있는 질문과 90개 이상의 연습문제를 담았습니다. 마치 게임 퀘스트를 해결하듯 문제를 하나하나 해결하다 보면 학습한 내용이 머릿속에 강렬하게 남습니다. 이유 2. 원스톱으로 배우는 14가지 GoF 핵심 디자인 패턴과 9가지 객체지향 디자인 원칙! 현장에서 자주 사용되는 옵저버, 어댑터, MVC 패턴 등 14가지 GoF 객체지향 패턴을 중점으로 패턴의 정의, 사용 시기, 사용처, 사용 이유, 즉시 디자인에 적용하는 방법을 알려줍니다. 이와 더불어 객체지향 프로그래밍에 광범위하게 적용할 수 있는 OCP, 할리우드 원칙 등 9가지 객체지향 디자인 원칙과 패턴으로 생각하는 방법도 알려줍니다. 이유 3. 시대의 변화에 맞춘 개정과 한국 독자만을 위한 특별판! 자바 8과 자바 16 이상에서 무리 없이 동작할 수 있도록 예제 코드를 수정했으며, 부가적인 설명과 Q&A 질문을 추가했습니다. 또한 16여 년 만의 개정을 기념해 오직 한국 독자만을 위한 새로운 삽화를 사용하고 한글 친화적인 구성했습니다. 원서를 읽을 때보다 더욱 편안하게 디자인 패턴을 학습할 수 있습니다. ▶ 이 책을 읽어야 하는 당신! ● 소프트웨어 출시는 완벽 그 자체! “어?~ 코드 수정하려고 다시 보니까 난리…” → 유지보수만 생각하면 그저 눈물인 주니어 (자바) 개발자 ● 코딩 실력은 장판파의 장비! “어?~ 팩토리 메소드 패턴을 이렇게 적용했던가?” → 디자인 패턴을 다시 한번 살펴보고 싶은 시니어 (자바) 개발자 ● 혼자 공부해서 다진 프로그래밍 언어 실력! “어?~ 근데 패턴이 뭐야?” → 개발 현장의 소프트웨어 디자인 방법이 궁금한 개발자 지망생
저자
에릭 프리먼, 엘리자베스 롭슨, 케이시 시에라, 버트 베이츠
출판
한빛미디어
출판일
2022.03.16

 

헤드퍼스트 디자인 패턴 6장 | 커맨트 패턴 - 호출 캡슐화하기

(에릭 프리먼 외 4인, 서환수 옮김, 한빛미디어)

 


 

메소드 호출을 캡슐화하면 계산 과정의 각 부분을 결정화할 수 있기에 계산하는 코드를 호출한 객체는 그 일이 어떤 식으로 처리되는지 전혀 신경쓸 필요가 없습니다. 그냥 결정화된 메소드를 호출해서 필요한 일만 잘 할 수 있으면 되는 거죠.

- 225p 

 

 

협력 업체 클래스

=> 많다

=> 모든 클래스에 on()과 off() 메소드만 있을 줄 알았는데 dim()이나 setTemperature(), setVolume(), setDirection() 같은 메소드들이 잔뜩 있다. 

=> 디자엔이 '커맨드 객체'를 추가하면 될 것 같아요. 커맨드 객체는 특정 객체(거실 조명 등)에 관한 특정 작업 요청(불을 켜는 것 등)을 캡슐화해서 주거든요. 버튼마다 커맨드 객체를 저장해두면 사용자가 버튼을 눌렀을 때 커맨드 객체로 작업을 처리할 수 있어요. 리모컨은 아무것도 몰라도 돼요. 어떤 객체에 어떤 일을 시켜야 하는지 잘 알고 있는 커맨드 객체가 있으니가요. 이러면 리모컨과 객체를 완전히 분리할 수 있지 않을까요? 

- 228 ~  230p 

 

 

 

객체마을 식당 등장인물의 역할 

- 주문서는 주문 내용을 캡슐화합니다.

- 종업원은 주문서를 받고 orderUp() 메소드를 호출합니다.

- 주방장은 식사를 준비하는 데 필요한 정보를 가지고 있습니다.

 

- 233p 

 

 

커맨드 패턴 

1. 클라이언트는 커맨드 객체를 생성해야 합니다. 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성되죠.

2. 커맨드 객체에는 행동과 리시버의 정보가 같이 들어있습니다.

3. 커맨드 객체에서 제공하는 메소드는 execute() 하나뿐입니다. 이 메소드는 행동을 캡슐화하여 리시버에 있는 특정 행동을 처리합니다.

4. 클라이언트는 인보커(invoker) 객체의 setCommand() 메소드를 호출하는데, 이때 커맨드 객체를 넘겨줍니다. 그 커맨드 객체는 나중에 쓰이기 전가지 인보커 객체에 보관됩니다.

5. 인보커에서 커맨드 객체의 execute 메소드를 호출하면

6. 리시버에 있는 행동 메소드가 호출됩니다. 

 

- 235p 

 

 

 

첫 번째 커맨드 객체 만들기

 

커맨드 인터페이스 구현

public interface Command {
	public void execute();
}

 

조명을 켤 때 필요한 커맨드 클래스 구현

 

Light 클래스 => on(), off()

 

public class LightOncommand implements Command {
	Light light;
    
    public LightOnCommand(Light light) {
    	this.light = light;
    }
    
    public void execute() {
    	light.on();
    }
 }

=> 커맨드 클래스니까 command 인터페이스 구현

=> 생성자에 이 커맨드 객체로 제어할 특정 조명의 정보가 전달됩니다. 그 객체는 light라는 인스턴스에 변수에 저장이 되지요. execute() 메소드가 호출되면 light 객체가 바로 그 요청의 리시버가 됩니다. 

 

- 237p 

 

 

커맨드 객체 사용하기

 

public class SimpleRemoteControl {
	command slot;
    public SimpleRemoteControl() {}
    => 커맨드를 저장할 슬롯이 1개. 이 슬롯으로 1개의 기기를 제어. 
    
    public void setCommand(Command command) {
    	slot = command;
    }
    => 슬롯을 가지고 제어할 명령을 설정하는 메서드. 
    리모컨 버튼의 기능을 바꾸고 싶다면 이 메소드를 사용해서 얼마든지 바꿀 수 있습니다. 
    
    public void buttonWasPressed() {
    	slot.execute();
    } 
   => 버튼을 누르면 이 메소드가 호출됩니다. 
   지금 슬롯에 연결된 커맨드 객체의 execute() 메소드만 호출하면 됩니다.

 

리모컨을 사용할 때 필요한 간단한 테스트 클래스 

 

public class RemoteControlTest { => 커맨드 패턴에서 클라이언트에 해당 
	public static void main(String[] args) {
    	simpleRemoteControl remote = new SimpleRemoteControl();
        => remote 변수가 인보커 역할. 필요한 작업을 요청할 때 사용할 커맨드 객체를 인자로 전달 받음
        
        Light light = new Light();
        => 요청을 받아서 처리할 리시버(receiver)인 Light 객체를 생성
        
        LightOnCommand = lightOn = new LightOnCommand(light);
        => 커맨드 객체를 생성. 이때 리시버를 전달해줌. 
        
        
        remote.setCommand(lightOn);		=> 커맨드 객체를 인보커에게 전달. 
        remot.buttonWasPressed(); 		=> 버튼 누르기
    }
}

 

 

- 238p 

 

 

커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있습니다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있습니다.

- 240p 

 

 

 

리모컨 코드 만들기

 

public class RemoteControl {
	Command[] onCommands;
    command[] offCommands;
    
    => 이 리모컨 코드는 7개의 on, off 명령을 처리할 수 있고 각 명령은 배열에 저장 
    
    public RemoteControl()	{
    	onCommands = new Command[7];
        offComands = new Command[7];
        
        => 생성자는 각 On, Off 배열의 인스턴스를 만들고 초기화 
        
        Command noCommand = new Nocommand();
        for (int i = 0; i> 7; i++) {
        	onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
   }
   
   
   
   	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
    
    => 각 커맨드 객체는 나중에 사용하기 편하게 onCommand와 offcommand 배열에 저장 
 
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
	}
    
    
    => 사용작 ON, OFF 버튼을 누르면 리모컨 하드웨어에서 각 버튼에 대응하는
    onButtonWasPushed() 메소드 호출
  
	public String toString() {
		StringBuffer stringBuff = new StringBuffer();
		stringBuff.append("\n------ Remote Control -------\n");
		for (int i = 0; i < onCommands.length; i++) {
			stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()
				+ "    " + offCommands[i].getClass().getName() + "\n");
		}
		return stringBuff.toString();
	}

 

- 244p

 

 

 

조명 객체 

public class Light {
   String location = "";

   public Light(String location) {
      this.location = location;
   }

   public void on() {
      System.out.println(location + " light is on");
   }

   public void off() {
      System.out.println(location + " light is off");
   }

 

라이트온 command 

 

public LightOnCommand(Light light) {
   this.light = light;
}

public void execute() {
   light.on();
}

 

리모컨 테스트 

 

public static void main(String[] args) {
      RemoteControl remoteControl = new RemoteControl();
 
      Light livingRoomLight = new Light("Living Room");

  
      LightOnCommand livingRoomLightOn = 
            new LightOnCommand(livingRoomLight);

  

      remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

      System.out.println(remoteControl);
 
      remoteControl.onButtonWasPushed(0);
      remoteControl.offButtonWasPushed(0);

   }
}

 

=> 장치를 각자의 위치에 맞게 생성

=> 조명 커맨드 객체 

 

=> 커맨드가 준비되었으니 리모컨 슬롯에 커맨드를 로드 

 

- 246 ~ 247p

 

 

 

 

 

=>객체를 배열에 저장하는 것에 주목

=> string에서 꺼내면서 출력 

 

 

 

람다 표현식을 써서 고친 코드 

remoteControl.setCommand(0, () -> livingRoomLight.on(), livingRoomLight.off());

구상 커맨드 객체의 인스턴스를 생성하는 대신 그 자리에 함수 객체를 사용할 수 있어서 모든 구상 커맨드 클래스를 지울 수 있음 

 

- 250p 

 

 

 

작업 취소 기능을 구현할 때 상태를 사용하는 방법 

public class CeilingFan {
   String location = "";
   int level;
   public static final int HIGH = 2;
   public static final int MEDIUM = 1;
   public static final int LOW = 0;
   
   => 선풍기의 속도를 나타내는 상태를 저장합니다. 
 
   public CeilingFan(String location) {
      this.location = location;
   }
  
   public void high() {
      // turns the ceiling fan on to high
      level = HIGH;
      System.out.println(location + " ceiling fan is on high");
 
   } 

   public void medium() {
      // turns the ceiling fan on to medium
      level = MEDIUM;
      System.out.println(location + " ceiling fan is on medium");
   }

   public void low() {
      // turns the ceiling fan on to low
      level = LOW;
      System.out.println(location + " ceiling fan is on low");
   }
   
   
   => 위 3 메소드로 선풍기 속도를 설정합니다. 
   
 
   public void off() {
      // turns the ceiling fan off
      level = 0;
      System.out.println(location + " ceiling fan is off");
   }
 
   public int getSpeed() {
      return level;
      
      
      => 이 메소드로 선풍기의 현재 속도를 구할 수 있습니다. 
   }

 

선풍기 명령어에 작업 취소 기능 추가하기 

 

public class CeilingFanHighCommand implements Command {
   CeilingFan ceilingFan;
   int prevSpeed;		=> 상태 지역 변수로 선풍기 속도 저장
  
   public CeilingFanHighCommand(CeilingFan ceilingFan) {
      this.ceilingFan = ceilingFan;
   }
 
   public void execute() {
      prevSpeed = ceilingFan.getSpeed();		=> 이전 속도 저장
      ceilingFan.high();
   }
 
   public void undo() {
      if (prevSpeed == CeilingFan.HIGH) {
         ceilingFan.high();
      } else if (prevSpeed == CeilingFan.MEDIUM) {
         ceilingFan.medium();
      } else if (prevSpeed == CeilingFan.LOW) {
         ceilingFan.low();
      } else if (prevSpeed == CeilingFan.OFF) {
         ceilingFan.off();
      }
      
      => 작업 취소시 선풍기 속도를 이전으로 되돌림
      
   }

 

선풍기 테스트 코드 

 


   CeilingFan ceilingFan = new CeilingFan("Living Room");
  
   CeilingFanMediumCommand ceilingFanMedium = 
         new CeilingFanMediumCommand(ceilingFan);
   CeilingFanHighCommand ceilingFanHigh = 
         new CeilingFanHighCommand(ceilingFan);
   CeilingFanOffCommand ceilingFanOff = 
         new CeilingFanOffCommand(ceilingFan);
 
   remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
   remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);
  
   remoteControl.onButtonWasPushed(0);
   remoteControl.offButtonWasPushed(0);
   System.out.println(remoteControl);
   remoteControl.undoButtonWasPushed();
 
   remoteControl.onButtonWasPushed(1);
   System.out.println(remoteControl);
   remoteControl.undoButtonWasPushed();
}

 

 

=> 3개의 커맨드 객체 인스턴스를 만듭니다.

=> 0번 슬롯에는 선풍기 속도를 Medium으로 돌리는 커맨드 객체를 

1번 슬롯에는 High로 설정하는 객체를 넣습니다. 

선풍기를 끄는 커맨드도 로딩합니다. 

 

=> 선풍기 속도를 MEDIUM으로 설정

=> 끄기 

등등 

 

- 257p

 

 

커맨드 패턴 더 활용하기

 

어떤 애플리케이션은 모든 행동을 기록해 두었다가 애플리케이션이 다운되었을 때 그 행동을 다시 호출해서 복구할 수 있어야 합니다. 커맨드 패턴을 사용하면  store()와 load() 메소드를 추가해서 이런 기능을 구현할 수 있습니다. 

- 264p 

 

 

반응형