반응형

커맨드 패턴은 "명령", "실행" 이라는 관점에서 DIP 원칙과 전략 패턴을 적극 활용한 패턴이다.

 

예시

선언부

public class Lamp {
	public void turnOn(){ System.out.println("Lamp On"); }
}
public class Button {
	private Lamp theLamp;
	public Button(Lamp theLamp) { this.theLamp = theLamp; }
	public void pressed() { theLamp.turnOn(); }
}

 

사용부

public class Main {
	public static void main(String[] args) {
 		Lamp lamp = new Lamp();
  		Button lampButton = new Button(lamp);
  		lampButton.pressed();
	}
}

 

Button 이 명령을 내리는 발행자로 명령을 받는 수행자 Lamp 와 강하게 결합이 되어 있다. 새로운 요구사항이 생겨 해당 Button 으로 다른 수행자의 명령을 수행하게 만든다면 어떻게 될까?

 

선언부

enum Mode { LAMP, ALARM };
public class Button {
	private Lamp theLamp;
    private Alarm theAlarm;
    private Mode theMode;
    
    public Button(Lamp theLamp, Alarm theAlarm) {
    	this.theLamp = theLamp;
        this.theAlarm = theAlarm;
    }
    
    public void setMode(Mode mode) { this.theMode = mode; }
    public void pressed() {
    	switch(theMode) {
        	case LAMP: theLamp.turnOn(); break;
            case ALARM: theAlarm.start(); break;
        }
    }
}

 

Button 에 명령을 수행하는 수행자 Lamp 와 Alarm 이 생성자 결합으로 강하게 묶이게 되고 계속 요구사항이 늘어날 때마다 Button 클래스를 수정해야 한다. 이는 OCP 원칙에 위배된다.

 

DIP 원칙을 이용하여 발행자와 수행자 사이에 객체를 하나 두어 한 단계 추상화해주면 된다. (인터페이스로 연결하고 각각의 구현체로 실질적인 로직들을 구현)

 

선언부

// 인터페이스
public interface Command { public abstract void execute(); }

public class Button {
	private Command theCommand;
	public Button(Command theCommand) { setCommand(theCommand); }
	public void setCommand(Command newCommand) { this.theCommand = newCommand; }
	public void pressed() { theCommand.execute(); }
}

public class Lamp {
	public void turnOn(){ System.out.println("Lamp On"); }
}
public class LampOnCommand implements Command {
	private Lamp theLamp;
	public LampOnCommand(Lamp theLamp) { this.theLamp = theLamp; }
	public void execute() { theLamp.turnOn(); }
}

public class Alarm {
	public void start(){ System.out.println("Alarming"); }
}
public class AlarmStartCommand implements Command {
	private Alarm theAlarm;
	public AlarmStartCommand(Alarm theAlarm) { this.theAlarm = theAlarm; }
	public void execute() { theAlarm.start(); }
}

 

사용부

public class Main {
	public static void main(String[] args) {
    	Lamp lamp = new Lamp();
        Command lampOnCommand = new LampOnCommand(lamp);
        Alarm alarm = new Alarm();
        Command alarmStartCommand = new AlarmStartCommand(alarm);
        
        Button button1 = new Button(lampOnCommand);
        button1.pressed();

		Button button2 = new Button(alarmStartCommand);
  		button2.pressed();
  		button2.setCommand(lampOnCommand);
  		button2.pressed();
	}
}

 

Button 에 한 단계 추상화 한 Command 를 부착할 수 있게 만들어 두고, 그 Command 의 메서드로 각각의 구현체가 동작하는 방식이다.

 

리팩토링 관점에서 보면...

사실 처음부터 설계하는 것보다는 기존의 코드를 수정할 일이 더 많다.

 

"명령" 과 "수행" 이라는 관점에서 커맨드 패턴을 떠올려 리팩토링할 수도 있지만, 조금 더 범용적인 관점에서 문제코드의 문제점을 해결할 수 있어야 커맨드 패턴을 처음 만든 프로그래머들의 취지를 이해할 수 있다고 생각한다.

 

커맨드 패턴을 알지 못 한 시점에서 해당 코드를 보고 무슨 문제점이 있는지 다시 살펴보자.

 

조건문(if 문이나 switch 문)에 각각 객체들이 포진되어 있고, 요구사항이 늘어날 때마다 결합되는 객체의 갯수와 조건문이 점점 커진다. 이 말은 고차원에서 저차원으로 가는데 저차원의 모듈 갯수가 현저히 많아 생긴 문제이다. 앞서 소개한 커맨드 패턴으로 예를 들면, 명령을 할 때 버튼을 눌러서 명령할 수도 있고, 손가락을 지시하여 명령할 수도 있다. 명령을 받는 주체는 그 명령을 받아 명령을 수행한다.

 

명령한다 => 버튼을 눌러서 명령한다 or 손가락을 지시하여 명령한다. => 램프를 킨다. or 알람을 설정한다. or 자동차 시동을 건다 등등

 

명령을 내리는 주체보다 명령을 받는 주체가 점점 많아지므로 DIP 원칙에서도 설명하였듯이 항상 저차원의 갯수가 많다. 따라서 고차원과 저차원의 모듈이 강하게 결합이 되는 것을 막으려면 한 단계를 추상화해 변수를 만들고 그 변수를 중간 매개체로 삼아 연결해야 한다. 그 매개체가 인터페이스이다. 이 인터페이스를 합성으로 고차원에 연결하면 앞서 소개한 커맨드 패턴이 되는 것이다.

 

결국 커맨드 패턴은 DIP 와 합성을 "명령" + "실행" 관점으로 바라본 디자인 패턴이다.

조건문이 계속 커지고 그 조건문 내에 객체의 갯수가 많아지면 인터페이스로 분리하여 합성하자.

반응형

'디자인 패턴' 카테고리의 다른 글

[디자인 패턴] 전략 패턴 (Strategy Pattern)  (0) 2022.04.23
반응형

전략 패턴이라고 부르지만, 사실 합성 강조한 패턴이다.

 

많은 블로그들에서 제시하는 전략패턴

 

웹 사이트나 블로그들에서 전략 패턴에 대해서 찾아보면 위와 같은 다이어그램을 제시하고, 아래와 같이 정의하는 것을 보게 된다.

 

"행위 자체를 클래스화해 동적으로 행위를 자유롭게 바꿀 수 있는 패턴"

 

그런데 솔직히 무슨 말인지 와닿지 않는다. 심지어 제시한 다이어그램이 DIP 다이어그램 구조와 동일하다.

일단 DIP 개념부터 간단하게 소개하고, 전략패턴과 비교하여 어떤 공통점과 차이점이 있는지 확인해보자.

 

DIP (Dependency Inversion Principle)

저수준 모듈에서 고수준 모듈이 정의한 추상 타입에 의존하여 역전 관계를 만들어야 한다.

 

저수준 모듈? 고수준 모듈? 이라는 컨셉이 처음 등장하게 되는데 아래와 같이 예시를 통해 손쉽게 이해할 수 있다.

고수준 모듈 저수준 모듈
자동차를 만든다. 자동차 뼈대를 만든다.
운전대를 만든다.
의자를 만든다.
타이어를 만든다.
...

고수준 모듈이 어떤 단일 기능을 제공하는 모듈이라면 저수준 모듈은 고수준 모듈 기능을 구현하기 위해 필요한 하위 기능들의 실제 구현이다. 그래서 항상 고수준 모듈보다 저수준 모듈의 갯수가 많을 수 밖에 없고, 고수준 모듈에서 저수준 모듈로 흐름이 이동한다. 다시 말해 고수준 모듈 -> 저수준 모듈 의존성을 갖는다는 말이다.

 

고수준 모듈에서 저수준 모듈로 향하는 흐름은 DIP 에서 이야기하는 저수준 모듈에서 고수준 모듈로 향하는 흐름과 완전히 반대다. 이름에서 역전이라는 말이 나오게 된 이유이다.

 

"자동차를 만든다" 라는 예제에서 뼈대 객체와 운전대 객체, 의자 객체가 자동차 클래스 내부에 필드로 삽입이 되어 있는데 뼈대는 BMW 클래스로, 운전대는 빨간 휠로, 의자는 가죽의자로 설계했다고 가정하자. 그런데 새로운 요구사항이 등장해서 AUDI 뼈대 객체와 통풍시트 객체도 지원이 필요하다고 한다면 자동차 클래스는 뼈대가 바뀔 때마다, 운전대가 바뀔 때마다, 의자가 바뀔 때마다 새로운 객체들을 가지고 있어야 된다. 이러면 자동차 클래스는 계속해서 수정해야 되니 객체지향의 패러다임 OCP 원칙을 위배하게 된다.

 

아까 앞에서 살펴보았듯이 고수준 모듈에서 저수준 모듈로 흐름이 향하는데 저수준 모듈의 갯수가 현저히 많아 발생한 문제이다. 갯수를 줄이기 위한 가장 보편적인 방법은 여러 객체들의 공통점을 찾아 저수준 단계를 한 단계 추상화 하는 것이다. BMW 클래스와 AUDI 클래스를 뼈대 클래스로 추상화하여 실체화 관계 또는 상속 관계를 가지게 하면 고수준 모듈은 변경을 하지 않아도 된다. 신기한 건 인터페이스를 주입받기 때문에 고수준에서 저수준으로 가는 흐름은 유지된다.

런타임 시에 인터페이스가 직접 호출되는 게 아닌 구현체인 모듈이 호출되니까 저수준에서 고수준으로 역전시킨 것처럼 보여도 실제 흐름은 고수준에서 저수준으로 간다.

 

그래서 DIP 다이어그램이 위 그림과 같게 된다.

 

하지만 주의할 점이 하나 있다. 상속과 실체화 관계는 완벽히 IS-A 관계에 있어야 한다. 여러 모듈들을 합쳐서 추상화한다고 하더라도 틀린 부분이 존재하게 된다. 틀린 부분은 별도의 클래스로 만들어 HAS-A 관계로 만들어야 한다.

이 때 전략 패턴이 등장한다. HAS-A 관계는 합성을 통해 만들 수 있는데 합성은 떼었다 붙였다 할 수 있다.

 

 

위 다이어그램과 같이 IS-A 관계에 있지 않는 HAS-A 관계들을 따로 추출하여 합성 구조로 만든 것이 전략패턴이다.

합성을 강조한 디자인 패턴이며, 유연한 구조를 띄기 때문에 자주 사용된다.

반응형

'디자인 패턴' 카테고리의 다른 글

[디자인 패턴] 커맨드 패턴 (Command Pattern)  (0) 2022.10.09

+ Recent posts