커맨드 패턴이란?

- 요구사항을 객체로 캡슐화해서 객체를 서로 다른 요구사항에 따라 매개변수화할 수 있다.

 

클래스 다이어그램을 살펴보자.

- Client : 커맨드 객체 생성

- Command : 어떤 Recevier를 실행할 지 연결

- Invoker : 주문을 받아서, 실행하기 위해 Command 인터페이스 연결

- Receiver : 실제 명령을 수행

 

간단한 예제를 보면서 살펴보자.

 

문제점

 집의 불을 켜주고, 차고 문을 열어주고, 스피커 전원도 켜주는 등 다양한 기능을 하는 최첨단 만능 IOT 리모컨이 존재한다. 

public class Light {
    public void on(){
        System.out.println("불을 켭니다.");
    }
    public void off(){
        System.out.println("불을 끕니다.");
    }
}

 

public class GarageDoor {
    public void open(){
        System.out.println("차고 문을 엽니다.");
    }
    public void close(){
        System.out.println("차고 문을 닫습니다.");
    }

}

 

public class Stereo {
    public void up(){
        System.out.println("볼륨을 올립니다.");
    }
    public void down(){
        System.out.println("볼륨을 내립니다.");
    }
}

사용하는 객체는 많은데 on/off, up/down 등 객체마다 사용하는 메소드가 달라 복잡하다...

 

해결책

Command 패턴을 적용해 Command 인터페이스를 추가하고 API를 일치시켜준다. 

public interface Command {
    public void execute();
    public void undo();

}

 

public class GarageDoorCommand implements Command{
    GarageDoor gd;
    public GarageDoorCommand(GarageDoor gd){
        this.gd = gd;
    }
    @Override
    public void execute(){
        gd.open();
    }

    @Override
    public void undo() {
        gd.close();
    }
}

 

public class LightCommand implements Command{
    Light light;
    public LightCommand(Light light){
        this.light = light;
    }

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

    @Override
    public void undo() {
        light.off();
    }
}

 

public class StereoCommand implements Command{
    Stereo stereo;

    public StereoCommand(Stereo stereo){
        this.stereo = stereo;
    }
    @Override
    public void execute() {
        stereo.up();
    }

    @Override
    public void undo() {
        stereo.down();
    }
}

 불을 켜는 것도, 스테레오의 볼륨을 조절하는 것도, 차고 문을 여는 것도 execute()라는 메소드로 일치시킬 수 있다. 

물론 클래스가 많아진다는 단점이 있지만 객체 사용에 복잡성을 제거하고 감출 수 있게 된다.

퍼사드 패턴이란?

- 서브 시스템에 있는 여러개의 인터페이스를 통합하는 한 개의 인터페이스를 제공하는 패턴

- 퍼사드는 서브 시스템을 쉽게 사용할 수 있도록 해주는 고급 수준의 인터페이스를 정의한다.

 

 우리가 밴드를 구성해서 공연을 하려고 한다. 그러기 위해서는 마이크의 전원을 켜고, 볼륨을 조절하고, 기타의 톤을 맞추고, 피아노 전원을 켜고... 공연을 시작하기 위해서 할 작업이 너무 많다. 그저 "공연 준비"라는 버튼 하나로 모든 기구의 세팅이 완료되었으면 좋겠다. 퍼사드 패턴을 적용해보자.

 

문제점

마이크 볼륨 조절, 전원, 기타의 톤 세팅 등 공연을 시작할 때 마다 수행해야 할 서브 시스템이 너무 많고 사용하기가 복잡하다.

 Mike

public class Mike {
    public void on(){
        System.out.println("마이크의 전원을 켭니다.");
    }
    public void off(){
        System.out.println("마이크의 전원을 끕니다.");
    }
}

Guitar

public class Guitar {
    public void on(){
        System.out.println("일렉기타의 전원을 켭니다.");
    }
    public void off(){
        System.out.println("일렉기타의 전원을 끕니다.");
    }

    public void setTone(){
        System.out.println("일렉기타의 톤을 맞춥니다.");
    }



}

Drum

public class Drum {
    public void on(){
        System.out.println("드럼의 전원을 켭니다.");
    }
    public void off(){
        System.out.println("드럼의 전원을 끕니다.");
    }
}

 

해결책

BandFacade라는 하나의 단순한 인터페이스를 제공하는 객체를 중간에 넣어서 startBand 메소드를 만들어 공연 시작 시 그에 맞는 메소드들을 실행해주게끔 한다.

public class BandFacade {
    Amplifier amp;
    Drum drum;
    Guitar guitar;
    Mike mike;

    public BandFacade(Amplifier amp, Drum drum, Guitar guitar, Mike mike) {
        this.amp = amp;
        this.drum = drum;
        this.guitar = guitar;
        this.mike = mike;
    }

    public void startBand(){
        amp.on();
        drum.on();
        guitar.on();
        mike.on();
        guitar.setTone();
    }

    public void endBand(){
        amp.off();
        drum.off();
        guitar.off();
        mike.off();

    }

}

 

클라이언트 입장에서는 startBand 메소드만 호출함으로서 복잡했던 공연 준비를 할 수 있게 되는 것이다.

어댑터 패턴이란?

- 클래스의 인터페이스를 클라이언트가 원하는 형태의 또 다른 인터페이스로 변환하는 패턴

- 어댑터는 호환되지 않는 인터페이스 때문에 동작하지 않는 클래스들을 함께 동작할 수 있도록 만들어준다.

 

전기 플러그를 생각해보자. 미국의 플러그 모양은 110V (11자 모형)이다. 미국 전자 제품을 한국에서 쓰려면 어댑터를 사용하여 한국의 플러그 모양에 맞게 변환해서 사용하면 된다. 이 것이 어댑터 패턴의 기본 원리라고 생각하면 된다.

 

헤드 퍼스트 디자인 책의 예시를 통해 문제점과 해결 방안을 살펴보자.

 

문제점

오리에 대한 Duck 인터페이스와 그를 구현한 MallardDuck, 칠면조에 대한 Turkey 인터페이스와 그를 구현한 WildTurkey 클래스가 존재한다. 오리쇼를 하고 싶은데 오리의 숫자가 부족하여 칠면조를 오리의 탈을 씌워 쇼에 참가해야 하는 상황이다. 먼저 클래스와 인터페이스를 살펴보자.

Duck

public interface Duck {
    public void quack();
    public void fly();
}

MallardDuck

public class MallardDuck implements Duck{
    @Override
    public void quack() {
        System.out.println("Quack");
    }

    @Override
    public void fly() {
        System.out.println("I'm flying");
    }
}

Turkey

public interface Turkey {
    public void gobble();
    public void fly();

}

WildTurkey

public class WildTurkey implements Turkey{

    // Turkey 인터페이스와 Duck 인터페이스는 다른 인터페이스다.
    // 문제점 : Duck 객체가 부족하여 Turkey 객체도 Duck 객체처럼 사용하고 싶다.
    // TurkeyAdapter를 사용하여 Turkey 객체도 Duck 객체로 변환하여 사용해보자.
    @Override
    public void gobble() {
        System.out.println("Gobble gobble");
    }

    @Override
    public void fly() {
        System.out.println("I'm flying a short distance");
    }
}

 

해결책

어댑터 패턴을 적용하여 Turkey 객체를 Duck "처럼" 사용해보자.

TurkeyAdapter

public class TurkeyAdapter implements Duck{
    Turkey turkey;

    public TurkeyAdapter(Turkey turkey){
        this.turkey = turkey;
    }

    @Override
    public void quack() {
        turkey.gobble();
    }

    @Override
    public void fly() {
        for(int i =0; i<5; i++){
            turkey.fly();
        }

    }
}

- Turkey 객체를 생성자의 매개 변수로 받아 Turkey의 매소드를 적절하게 Duck의 메소드에 활용한다.

 

Composite Pattern 이란?

- 객체를 트리구조로 구성해서 부분-전체 계층구조를 구현한다.

- 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다.

 

윈도우의 디렉토리 구조를 생각하면 쉽다. 폴더에는 폴더 또는 파일이 들어갈 수 있으며 개별 객체(파일), 복합 객체(폴더)를 똑같은 방법으로 다룰 수 있는 구조가 Composite Pattern 이라고 생각하면 된다!

 

Component

- 개별 객체 뿐만 아니라 이런한 개별 객체들을 계층 구조로 포함하는 복합 객체를 나타내는 인터페이스 또는 추상 클래스

- 모든 클래스에 공통적인 행위에 대해 기본 기능을 구현할 수 있다.

Leaf (개별 객체)

- 개별 객체에 해당되는 컴포넌트

Composite (복합 객체)

- 다른 컴포넌트를 포함할 수 있는 컴포넌트

- 개별 객체 또는 다른 복합 객체를 포함할 수 있음

 

예시

맥도날드에서 메뉴판을 보면 불고기 버거 단품, 감자 튀김, 불고기 버거 세트 메뉴, 패밀리 세트 메뉴 등 다양한 메뉴가 존재한다. 이 때, 햄버거와 감자튀김은 단일 메뉴로서 역할을 할 수도 있고 버거 세트 메뉴로서 하나의 메뉴에 포함될 수도 있다. 이 때, 햄버거는 개별 객체(Leaf)로서 버거 세트 메뉴는 복합 객체(Composite)으로 비유할 수 있고 손님은 이 둘을 똑같은 방식으로 다룰 수 있다.

 

Component

public interface MenuComponent {
    public default void add(MenuComponent comp){
        throw new UnsupportedOperationException();
    }
    public default void remove(MenuComponent comp){
        throw new UnsupportedOperationException();
    }
    public default MenuComponent getChild(int i){
        throw new UnsupportedOperationException();
    }
    public default String getName(){
        throw new UnsupportedOperationException();
    }
    public default String getDescription(){
        throw new UnsupportedOperationException();
    }

    public default double getPrice(){
        throw new UnsupportedOperationException();
    }
    public default boolean isVegetarian(){
        throw new UnsupportedOperationException();
    }
    public default void print(){
        throw new UnsupportedOperationException();
    }
}

 

Leaf

public class MenuItem implements MenuComponent{
    String name;
    String description;
    boolean vegetarian;
    double price;

    public MenuItem(String name, String desc, boolean vege, double price){
        this.name = name;
        this.description = desc;
        this.vegetarian = vege;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public void print() {
        System.out.println(" "+getName());
        if(isVegetarian()){
            System.out.println("(v)");
        }
        System.out.println(", "+getPrice());
        System.out.println("    -- "+getDescription());
    }
}

 

Composite

public class Menu implements MenuComponent{
    ArrayList<MenuComponent> menuComponents = new ArrayList();
    String name;
    String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void add(MenuComponent comp) {
        menuComponents.add(comp);
    }

    @Override
    public void remove(MenuComponent comp) {
        menuComponents.remove(comp);
    }

    public void print() {
        System.out.print("\n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");
        Iterator it = menuComponents.iterator();
        while (it.hasNext()) {
            MenuComponent component
                    = (MenuComponent) it.next();
            component.print();
        }
    }

}

 

빌더 패턴

빌더 패턴은 복잡한 객체 생성 과정을 단순화하고 객체의 다양한 속성을 설정하는 방법을 제공하는 디자인 패턴 이다.

 

이해가 잘 되지 않는다. 바로 예제를 살펴보자

 


빌더 패턴 예제

콜라 같은 식품의 영양 정보표를 만들어보자. 이 표에는 요구되는 필수 정보와 넣어도 되고 넣지 않아도 되는 선택적 정보를 포함한다.

필수 정보

- 1회 분량(serving size), 총 분량(servings) ...

선택 정보

- 지방(total fat), 포화지방(saturated fat), 트랜스 지방(trans fat), 나트륨(sodium)...

 

 

이 때, 선택 정보에 대해 생성자를 여러개 만들어 오버로딩할 수 도 있지만 너무 코드가 길어지고  파라미터의 순서나 개수를 파악하기 힘들어 문제가 생긴다. 이 때, 빌더 패턴을 사용한다. 코드를 살펴보자.

 

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        public Builder calories(int val) {
            calories = val;
            return this;
        }
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }

    }
    public int getCalories(){
        return calories;
    }
    NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

- Builder라는 빌더 클래스를 생성한다. 파라미터로는 필수 정보인 servingSize와 servings를 받는다.

- 선택 정보는 setter의 형태로 메소드를 작성해준다.(set은 붙이지 않아도 된다.) 객체를 생성할 때 포함할지 하지 않을지 선택권을 준다.

- build() 메소드로 객체를 생성하면서 원하는 정보만 가진 객체를 생성할 수 있게 된다.

 

public class NutritionFactsMain {
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240,8)
                .calories(100)
                .sodium(35)
                .carbohydrate(27)
                .build();
        System.out.println(cocaCola.getCalories());
    }
}

- 넣고 싶은 선택 정보만 포함해서 객체를 생성함을 볼 수 있다.

 


정리

- 빌터 패턴은 객체 생성을 직접 하지 않고 필수 요소를 전달한 후 생성자를 호출해서 빌더 객체를 생성한다.

- 빌더 객체에 선택 요소에 대하 setter와 비슷한 메소들을 호출한다.

- build() 메소드를 호출해서 원하는 immutable 객체를 생성한다.

데코레이터 패턴

 

늘 그랬듯이 클래스 다이어그램부터 살펴보자.

천천히 알아보자.

 

데코레이터 패턴이란?

- decorate: 장식하다.

- 객체에 추가 요소를 동적으로 더할 수 있다.

- 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.

 


 

이해를 돕기 위해 예를 들어보자. 헤드 퍼스트 디자인 패턴 책의 예제를 가져왔다.

 

예제 : 스타버즈 커피

 

문제

빠른 성장세에 다양한 음료들을 모두 포괄하는 주문 시스템을 갖추려고 하는 스타버즈 커피숍이 있다. 초기 시스템은 아래와 같다.

문제점은 여기서 발생한다. 시스템은 구축해놓았는데 모카나 우유나 휘핑 크림 같은 추가 토핑을 추가해야 할 때, 또는 가격이 변동될 때 상속받는 클래스가 아래와 같이 너무 많아진다는 것이다. (ex 휘핑 크림을 얹은 모카 에스프레소 클래스... 이런 것 까지 따로 구현해야 한다.)

이를 데코레이터 패턴을 적용하여 개선시켜보자!!

이런 식으로 적용하면 "휘핑 크림은 얹고 모카를 넣은 DarkRoast 클래스" 를 만들 때 이 자체의 클래스를 만드는 것이 아니라 구성하는 객체들이 감싸주기만 하면 된다. 아래처럼 말이다.

 


코드로 살펴보자.

Beverage.java

public abstract class Beverage {
    String description = "제목 없음";

    public String getDescription(){
        return description;
    }
    public abstract double cost();

}

 

DarkRoast.java

public class DarkRoast extends Beverage{

    public DarkRoast(){
        description = "다크 로스트";
    }

    @Override
    public double cost() {
        return 0.99;
    }
}

 

CondimentDecorator.java

public abstract class CondimentDecorator extends Beverage{

    public abstract String getDescription();
    @Override
    public abstract double cost();
}

 

Moch.java

public class Mocha extends CondimentDecorator{
    Beverage beverage;

    public Mocha(Beverage beverage){
        this.beverage = beverage;
    }
    @Override
    public String getDescription() {
        return "모카 " + beverage.getDescription();
    }

    @Override
    public double cost() {
        return 0.2 + beverage.cost();
    }
}

 

Whip.java

public class Whip extends CondimentDecorator{
    Beverage beverage;

    public Whip(Beverage beverage){
        this.beverage = beverage;
    }
    @Override
    public String getDescription() {
        return "휘핑 " + beverage.getDescription();
    }

    @Override
    public double cost() {
        return 0.1 + beverage.cost();
    }
}

 이제 처음 봤던 클래스 다이어그램을 보며 마무리하자.

 

- Component : 각 구성요소는 직접 쓰일 수도 있고 데코레이터로 감싸져서 쓰일 수도 있다 (클래스 or 인터페이스)

- ConcreteComponent : 새로운 행동을 동적으로 추가 (DarkRoast)

- Decorator : 자신이 장식할 구성요소와 같은 인터페이스 또는 추상 클래스의 역할 (CondimentDecorator)

- ConcreteDecorator : 객체가 장식하고 있는 것을 위한 인스턴스 변수가 존재 (Whip, Mocha)

 

+ Recent posts