헤드퍼스트 디자인패턴 Chapter3

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 3. 객체 꾸미기 - 데코레이터 패턴

OCP 살펴보기

  • OCP(Open-Closed Principle)

디자인 원칙 5

  • 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
    • 우리의 목표는 기존 코드를 건드리지 않고 확장으로 새로운 기능을 추가하는 것이다.
    • 이러한 경우 유연하고 튼튼한 디자인을 만들 수 있다.

커피 전문점 프로젝트

  • 커피 주문 시스템에는 기본 커피 메뉴 가격에 고객들이 추가로 첨가하는 첨가물들의 가격도 각각 더해야 한다는 점을 고려해야 한다.

데코레이터 패턴 살펴보기

  • 특정 음료에서 첨가물로 그 음료를 장식(decorate)한다고 생각해보면…
    • 예시로 모카와 휘핑 크림을 추가한 다크 로스트 커피를 주문한다고 가정한다.
      1. DarkRoast 객체를 가져온다.
      2. Mocha 객체로 장식한다.
      3. Whip 객체로 장식한다.
      4. cost() 메서드를 호출한다. 단, 이때 첨가물의 가격을 개산하는 일은 해당 객체에게 위임한다.
    • 위 예시에서 객체를 어떻게 장식할지, 계산하는 일을 어떻게 위임할지가 바로 데코레이터 패턴의 방식이다.

주문 시스템에 데코레이터 패턴 적용하기

  1. DarkRoast 객체를 가져온다.
  2. Mocha 객체를 만들고 그 객체로 DarkRoast를 감싼다.
  3. Whip 객체를 만들어 Mocha를 감싼다.
  4. 가격을 계산한다. 가격을 계산할 때는 가장 바깥쪽에 있는 Whip 객체의 cost()를 호출한다.
  5. Whip은 그 객체가 장식하고 있는 객체에게 가격 계산을 위임한다. 가격이 구해지면, 계산된 가격에 휘핑크림 가격을 더해 그 결과값을 반환한다.
    1. 가장 바깥쪽에 있는 데코레이터인 Whipcost()를 호출
    2. WhipMochacost()를 호출
    3. MochaDarkRoastcost()를 호출
    4. DarkRoast는 다크 로스트 커피 가격을 반환
    5. MochaDarkRoast로부터 리턴받은 가격에 모카 가격을 더해서 반환
    6. WhipMocha로부터 받은 가격에 휘핑크림 가격을 더해서 최종 가격 반환

데코레이터 패턴의 정의

  • 데코레이터 패턴으로 객체에 추가 요소를 동적으로 더할 수 있다.
  • 데코레이터 패턴을 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.

데코레이터 패턴 클래스 다이어그램

  • 각 구성 요소는 직접 쓰일 수도 있고 데코레이터에 감싸여 쓰일 수도 있다.
    |    Component    |
    | --------------- |
    | methodA()       |
    | methodB()       |
    | // 기타 메서드   |
    
  • ConcreteComponent에 새로운 행동을 동적으로 추가한다.
    |    ConcreteComponent    |
    | ----------------------- |
    | methodA()               |
    | methodB()               |
    | // 기타 메서드           |
    
  • Decorator에는 장식할 구성 요소와 같은 인터페이스 또는 추상 클래스를 구현한다.
    |        Decorator        |
    | ----------------------- |
    | Component wrappedObj    |
    | ----------------------- |
    | methodA()               |
    | methodB()               |
    | // 기타 메서드           |
    
  • ConcreteDecorator에는 데코레이터가 감싸고 있는 Component 객체용 인스턴스 변수가 존재한다.
    | ConcreteDecoratorA  |
    | ------------------- |
    | methodA()           |
    | methodB()           |
    | newBehavior()       |
    | // 기타 메서드       |
    
  • Decorator는 Component의 상태를 확장할 수 있다.
    | ConcreteDecoratorB  |
    | ------------------- |
    | Object newState     | 
    | ------------------- |
    | methodA()           |
    | methodB()           |
    | // 기타 메서드       |
    

커피 전문점 프로젝트에 적용하기

  • Component 추상 클래스
    | Beverage         |
    | ---------------- |
    | description      |
    | ---------------- |
    | getDescription() |
    | cost()           |
    
  • 커피 종류마다 구성 요소를 나타내는 구상 클래스
    | DarkRoast |
    | --------- |
    | cost()    |
    
    | Espresso |
    | -------- |
    | cost()   |
    
    | Decaf  |
    | ------ |
    | cost() |
    
  • Decorator 클래스
    | CondimentDecorator |
    | ------------------ |
    | getDescription()   |
    
  • 각각의 첨가물을 나타내는 데코레이터
  • cost()getDescription()을 구현해야 한다.
    | Milk              |
    | ----------------- |
    | Beverage beverage |
    | ----------------- |
    | cost()            |
    | getDescription()  |
    
    | Mocha             |
    | ----------------- |
    | Beverage beverage |
    | ----------------- |
    | cost()            |
    | getDescription()  |
    
    | Whip              |
    | ----------------- |
    | Beverage beverage |
    | ----------------- |
    | cost()            |
    | getDescription()  |
    

커피 주문 시스템 코드 작성

  • Beverage 클래스
    public abstract class Beverage {
      String description = "제목 없음";
        
      public String getDescription() {
          return description;
      }
        
      // getDescription()은 이미 구현되어 있지만 cost()는 서브클래스에서 구현해야 한다.
      public abstract double cost();
    }
    
  • Decorator 클래스
    // Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하므로 Beverage를 상속받는다.
    public abstract class CondimentDecorator extends Beverage {
      // 각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체
      Beverage beverage;
        
      // 모든 첨가물 데코레이터에서 getDescription()을 새로 구현하도록 만들 계획이다.
      public abstract String getDescription();
    }
    

음료 코드 구현

  • 다크 로스트 커피 구현하기
    public class DarkRoast extends Beverage {
        
      public DarkRoast() {
          description = "다크 로스트 커피";
      }
        
      public double cost() {
          return 0.99;
      }
    }
    
  • 에스프레소 음료 구현하기
    public class Espresso extends Beverage {
        
      public Espresso() {
          description = "에스프레소";
      }
        
      public double cost() {
          return 1.99;
      }
    }
    

첨가물 코드 구현

  • 모카 첨가물 구현하기
    public class Mocha extends CondimentDecorator {
        
      public Mocha(Beverage beverage) {
          this.beverage = beverage;
      }
        
      public String getDescription() {
          return beverage.getDescription() + ", 모카";
      }
        
      public double cost() {
          return beverage.cost() + 0.20;
      }
    }
    
  • 휘핑크림 첨가물 구현하기
    public class Whip extends CondimentDecorator {
        
      public Whip(Beverage beverage) {
          this.beverage = beverage;
      }
        
      public String getDescription() {
          return beverage.getDescription() + ", 휘핑크림";
      }
        
      public double cost() {
          return beverage.cost() + 0.10;
      }
    }
    

커피 주문 시스템 코드 테스트

  • 주문용 테스트 코드
    public class CoffeeOrder {
      public static void main(String args[]) {
          Beverage beverage = new Espresso();
          System.out.println(beverage.getDescription()
              + " $" + beverage.cost());
          
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Whip(beverage2);
        System.out.println(beverage2.getDescription()
            + " $" + beverage2.cost());
      }
    }
    

데코레이터가 적용된 예: 자바 I/O

  • FileInputStream
    • 데코레이터로 장식될 예정인 클래스
  • BufferedInputStream
    • 구상 데코레이터로, FileInputStream에 입력을 미리 읽어서 더 빠르게 처리할 수 있게 해주는 버퍼링 기능을 더해주는 역할
  • ZipInputStream
    • 또한 구상 데코레이터로, zip 파일에서 데이터를 읽어올 때 안에 들어있는 항목을 읽는 기능을 더해주는 역할

데코레이터 패턴의 문제점

  1. 데코레이터 패턴을 사용해 디자인하다 보면 잡다한 클래스가 많아진다.
  2. 데코레이터 패턴을 도입하면 구성 요소를 초기화하는 데 필요한 코드가 훨씬 복잡해진다.
    • 빌더 패턴이나 팩토리 패턴으로 개선 가능