Beverage 라는 슈퍼 클래스를 만들고 해당 클래스를 상속 받는 DarkRost 클래스를 만들었다. Beverage 클래스에는 커피 주문 시 추가할 수 있는 첨가물에 대한 가격들이 있고 setXXX() 함수를 통해 첨가물 추가 시 가격이 정해진다. cost()에서 각 첨가물의 가격을 더해 반환한다.
DarkRost 클래스에서는 인스턴스 생성 시 description 변수를 정의하고 cost() 함수를 통해 기본 가격에 첨가물 가격을 더한 최종 가격을 반환한다. 고객이 다크 로스트 커피에 두유와 휘핑 크림을 추가해서 주문하는걸 코드로 표현하면 아래와 같을 것이다.
첨가물의 종류가 추가될 때마다 관련 메소드를 추가해야 하고, 슈퍼 클래스의 cost() 메소드도 수정해야 한다.
아이스티와 같은 특정 첨가물이 들어가면 안 되는 음료가 추가되는 경우 hasWhip() 같은 메소드를 상속 받게 되어 실수를 유발한다.
결국 우리가 원하는건 기존 코드를 수정하지 않고 기능만 확장시키는 것이다.
OCP(Open-Closed Principle) 클래스는 확장에는 열려 있고 변경에는 닫혀 있어야 한다.
모든 부분에서 OCP를 준수하는건 불가능하다. OCP를 준수하는 객체지향 디자인을 만들려면 많은 시간과 노력이 필요한데 그만큼 여유있는 상황은 흔치 않다. 또한 OCP를 지키다 보면 새로운 단계의 추상화가 필요한 경우가 있는데, 추상화를 하다 보면 코드가 복잡해진다. 그래서 디자인한 것 중에서 가장 바뀔 가능성이 높은 부분을 중점적으로 살펴보고 OCP를 적용하는 방법이 가장 좋다.
해결 방법
데코레이터 패턴을 적용해보자.
기존 Beverage 클래스의 cost() 메소드를 추상 메소드로 변경해서 상속을 강제한다.
첨가물이 추가되면 기존 코드를 건드리지 않고 CondimentDecorator 클래스를 상속 받는 클래스를 생성한다.
Beverage 클래스를 상속하는 음료 클래스들과 첨가물 클래스들을 분리하여 관련 없는 상속을 방지할 수 있다.
Java.io 패키지
우리에게 친숙한 Java.io 패키지를 데코레이터 패턴을 통해 알아보자. 데코레이터 패턴을 알고 나면 I/O 클래스가 왜 그렇게 되어 있는지 이해할 수 있다. 파일에서 데이터를 읽어오는 스트림에 기능을 더하는 데코레이터를 사용하는 객체는 다음과 같은 형식으로 구성된다.
자바 I/O를 보면 데코레이터의 단점도 발견할 수 있다. 데코레이터 패턴을 사용해서 디자인을 하다 보면 잡다한 클래스가 너무 많아진다. 하지만 데코레이터가 어떤 식으로 작동하는지 이해하면 다른 사람이 데코레이터 패턴을 활용해서 만든 API를 끌어 쓰더라도 클래스를 데코레이터로 감싸서 원하는 행동을 구현할 수 있다.