데코레이터 패턴

Lee yun bok·2021년 9월 1일
0
post-thumbnail

데코레이터 패턴이란?

데코레이터 패턴에서는 객체에 추가적인 로직을 동적으로 추가한다.
서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.


카페에서 사용하는 주문 어플리케이션을 좀 더 확장성 있게 구현해보자.

요구사항

  1. 해당 카페는 급속도로 성장을 이어가고 있는 카페이기에 메뉴의 변동이 많다.
  2. 커피를 주문할 때는 두유, 스팀 우유, 모카, 휘핑 크림과 같은 추가 메뉴로 구성한다.
  3. 모든 메뉴들은 cost()를 구현해야 한다.
  4. 현재 기존 시스템의 구성도의 일부는 다음과 같다.

기존 시스템의 문제점

  1. 위의 구성도에는 4가지 메뉴 밖에 없어 단순해보이지만 만약 수많은 첨가물 메뉴, 수많은 메뉴들을 나타내려면 몇십, 몇백가지의 클래스가 필요할 것이다.
  2. 심지어 특정 첨가물 메뉴의 가격이 오르면..? 몇십, 몇백가지의 클래스를 변경해야하는 일이 발생할수도 있다.
  3. 첨가물 메뉴의 종류가 하나만 증가해도 클래스는 폭발적으로 증가할 것이다.

그렇다면 이 방식은 어떨까?

  1. Beverage가 모든 첨가물의 존재 유무를 확인할 수 있는 메서드를 가진다.
  2. 각 메뉴를 나타내는 클래스는 Beverage를 상속받은 후 cost()를 구현할 때 슈퍼 클래스 Beveragecost()를 호출하여 비용을 추가하는 식으로 구현한다.
  3. 다음과 같이 구현하면 단순히 메뉴의 갯수만큼만 클래스를 구현하면 된다. 그런데...
  4. 만약 첨가물 메뉴의 가격이 변경되면..?, 첨가물 종류가 증가하면 Beverage를 계속 수정해야한다.
  5. OCP 원칙을 지키지 않고 있다.

참고) OCP란?

확장에는 열려있고 변경에는 닫혀있어야 한다.

데코레이터 패턴을 이용하여 OCP 원칙을 지킬 수 있도록 설계해보자.

  1. 데코레이터 패턴은 추가 로직을 동적으로 추가할 수 있도록 하는 패턴이다.
  2. 따라서 위와 같은 요구사항에서 손쉽게 첨가물을 추가하고 제거할 수 있다.
  3. 정리하자면 추가되는 메뉴가 기존의 메뉴들을 감싸는 형식이라고 볼 수 있다.

정확히 이해가 되지 않을땐 개발자들은 코드를 직접 보는게 최고다.
한번 위의 요구사항을 데코레이터 패턴을 적용하여 유연하게 만든 코드를 살펴보자.

데코레이터 패턴을 적용한 구현 방식

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

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

// Beverage 형태의 변수에 삽입되어야 하기 때문에 상속 활용
public abstract class CondimentDecorator extends Beverage{
    public abstract String getDescription();
}
  1. 각각 메뉴와 첨가물들이 직접 구현해야 하는 추상 클래스이다.
  2. 상속을 이용하는 이유는 단순히 Beverage 변수에 해당 객체를 넣기 위해 상속받는 것이다.
/*
 * HouseBlend 커피 클래스
 */
public class HouseBlend extends Beverage {
    public HouseBlend() {
        description = "하우스 블랜드 커피";
    }

    public double cost() {
        return 0.89;
    }
}

public class Mocha extends CondimentDecorator {
    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }

    /**
     * Mocha가 감싸고 있는 beverage의 가격 로직 + 본인의 세부 로직을 합친다.
     */
    @Override
    public double cost() {
        return 0.20 + beverage.cost();
    }
}
  1. 각각 HouseBlend 메뉴 구현 클래스와 Mocha 첨가물 구현 클래스이다.
  2. Mocha는 데코레이터 클래스로서 로직에서 호출할 Beverage 형태의 변수를 가지고 있다.
  3. 후에 실행 시점에 Beverage 변수에 특정 객체가 삽입되게 된다.
public class DecoratorSimulation {
    public static void main(String[] args) {
        Beverage beverage = new HouseBlend();
        beverage = new Mocha(beverage);
        beverage = new Mocha(beverage); // 하우스 블랜드 커피 + 모카 + 모카
        System.out.println(beverage.getDescription() + " $" + beverage.cost());
    }
}
  1. 메인 로직이다.
  2. Mocha 첨가물 구현 객체가 HouseBlend 메뉴 구현 객체를 감싼다.
  3. 그리고 이어서 추가된 Mocha 첨가물 구현 객체가 Mocha를 다시 한번 감싼다.
  4. 이제 해당 음료는 Mocha 2개를 포함한 HouseBlend 음료가 되었다.
  5. 마지막에 berverage.cost()를 호출함으로써 호출은 mocha -> mocha -> houseblend 순이지만 완료는 houseblend -> mocha -> mocha 순으로 완료된다.
  6. 정리하자면 데코레이터 패턴은 기본 로직에 세부 로직을 동적으로 손쉽게 추가할 수 있는 패턴이라고 할 수 있다.

사용 예시 (자바 I/O)

  1. JAVA.IO 패키지의 많은 부분이 데코레이터 패턴을 바탕으로 만들어졌다.
  2. 예를 들어 FileInputStream -> BufferedInputStream -> LineNumberInputStream이 있는데 LineNumberInputStream은 행번호를 붙여주는 역할을, BufferedInputStream은 버퍼 입출력 기능을 FileInputStream은 데이터를 읽어들일 수 있는 기본 구성요소 역할을 한다.
  3. 즉 각각의 클래스가 세부로직을 하나씩 더해가며 하나의 로직을 완성시키고 있다.

출처
Head First Design Patterns - (에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 비트 베이츠) 을 보며 정리한 내용입니다.

profile
https://ybdeveloper.tistory.com/ 로 이동했습니다 : )

0개의 댓글