[헤드퍼스트 디자인패턴] Chapter 03 - 데코레이터 패턴

뚱이·2023년 5월 1일
0
post-thumbnail

데코레이터 패턴 (Decorator Pattern)

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

1. 데코레이터 패턴

(1) 데코레이터 패턴이란?

스타벅스의 퍼스널 옵션 을 생각하면 이해하기 쉬울 거 같다.

실제로 나는 스타벅스에서 자바칩 프라푸치노 를 좋아한다.



위 영수증들은 실제로 내가 주문했던 내역인데, 퍼스널 옵션 을 굉장히 애용하는 편이다 !

자, 그러면 스타벅스의 커피 주문 시스템 측면에서 봤을 때
내가 시킨 3개의 자바칩 프라푸치노 는 같은 객체일까? 다른 객체일까?

당연히 같은 객체는 아니겠지만 그렇다고 또 아예 다른 객체인 거 같지는 않다 🤔

각각의 주문을 하나의 객체로 구현한다면,
사람마다 원하는 퍼스널 옵션은 다양하고 다르므로
엄 ~~ 청나게 많은 객체가 생겨날 것이다.

우린 .. 이 모든 객체를 구현할 수 있을까?
그리고 이렇게 많은 객체를 구현하는 게 과연 효율적인 방법일까?

이러한 상황을 잘 설명할 수 있는 패턴이 데코레이터 패턴 이다.

스타벅스에서 퍼스널 옵션 을 추가하는 것처럼,
데코레이터 패턴에서는 데코레이터 를 추가한다.

다음은 책에서 설명하는 데코레이터 패턴이다.




(2) 데코레이터 패턴의 구조

데코레이터 패턴의 클래스 다이어그램은 다음과 같다.


(3) 데코레이터 패턴의 특징

  • 데코레이터의 슈퍼클래스 == 자신이 장식하고 있는 객체의 슈퍼클래스
    ex) 자바칩 프라푸치노의 슈퍼클래스 == 저지방 우유의 슈퍼클래스

  • 한 객체를 여러 개의 데코레이터로 감쌀 수 있음.
    ex) 당연함. 한 번 주문할 때 퍼스널 옵션 3-4개씩 추가함.

  • 원래 객체(싸여 있는 객체) 자리에 데코레이터 객체 넣어도 됨 (<- 1번 특징)

  • 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업 수행 가능

  • 객체는 언제든지 감싸기 가능이라서 실행 중에 필요한 데코레이터 적용 가능



2. 적용해보기: 초대형 커피 전문점, 스타버즈

(0) 스타버즈의 주문 시스템을 만들자

스타버즈(ㅋㅋㅋ)는 단기간에 폭발적으로 성장한 초대형 커피 전문점이다.
그만큼 다양한 메뉴도 많이 생겼는데, 주문 시스템은 초기에 마련된 거라서 좀 구리다 🥲

이 시스템을 개선해보자 !

사업이 시작될 시기에 만들어진 주문 시스템 클래스는 다음과 같다.


ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

이게 처음에 말했던 문제점이다.

기본 원두에 이런 저런 옵션을 추가한 메뉴들을 하나하나 클래스로 구현하니까 이렇게 클래스가 '폭발'한 상황이 됐다.

이건 누가 봐도 좋지 못한 시스템이라는 걸 알겠지만, 그래도 문제를 하나 말해보자면

사람들이 잘 주문하지 않는 경우도 고려해서 다 ~~ 구현을 해놔야 한다는 거다.
그러니까 클래스가 '폭발'하지


(1) 인스턴스 변수와 슈퍼클래스 상속하기

아니 클래스가 너무 많아
이렇게 클래스가 많이 필요할 이유가 없는데 ?

인스턴스 변수슈퍼클래스 상속 을 사용해서 옵션을 관리하면 안 되는 건가?

안 될 게 뭐가 있나.
한 번 해 보면 되지.

그러면 다음과 같이 표현된다.


언뜻 보기엔 ,, 괜찮아보이는 거 같기도 하다 🤔

하지만 과연 그럴까

이러한 구조는 다음과 같은 문제점이 존재한다.

  • 옵션 가격이 바뀔 때마다 기존 코드를 수정해야 함

  • 새로운 옵션이 추가되면, 새로운 메소를 추가하고 슈퍼클래스의 cost() 메소드도 수정해야 함

  • 특정 옵션이 아 ~~ 예 필요하지 않은 메뉴에도 옵션 정보가 들어가 있음
    ex) 아이스티와 휘핑크림

  • 하나의 옵션을 여러 번 추가하면 어떻게 되는 거지?
    ex) 자바칩 3번 추가

상속이 항상 유연하고 관리하기 쉬운 디자인을 만들지는 않는다는 결론이 나온다.

그런데 재사용 이라는 목적은 달성해야 한다.
상속 외에 재사용을 구현할 수 있는 방법에는 구성위임 이 있다.
종합해보면, 구성과 위임으로 실행 중에 행동을 '상속'하는 방법 이 있다는 것이다.

이 때 !
새로운 디자인 원칙이 나온다.


OCP (Open-Closed Principle)

클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.

기존 코드를 건드리지 않고 확장으로 새로운 행동을 추가하면,
새로운 기능을 추가할 때 급변하는 주변환경에 잘 적응하는 유연하고 튼튼한 디자인을 만들 수 있다.

아까 언급한 구성과 위임으로 실행 중에 행동을 상속 하는 방법 에 대해 더 살펴보자.

이 방법은 구성 으로 객체의 행동을 확장해 실행 중에 동적으로 행동을 설정하는 방법이다.

기존의 방식(서브클래스를 만드는 방식)으로 상속받으면,
1. 컴파일할 때 행동 완전히 결정
2. 모든 서브클래스에서 똑같은 행동 상속
이와 같은 문제가 있다.

반면 구성 을 활용하면 실행 중에 행동을 설정하기 때문에,
1. 객체에 여러 임무를 새로 추가할 수도 있고,
2. 슈퍼클래스를 디자인했던 사람이 생각 못 한 내용을 새로운 코드를 통해 추가할 수도 있다.
3. 또한 기존 코드를 건드리지 않기 때문에, 코드 수정에 따른 버그나 부작용을 방지할 수 있다.

그렇다면,
모든 부분에서 OCP를 준수하는 것이 좋을까?
🙅‍♀️ 전혀 아니다 ! OCP를 만족하려면 새로운 단계의 추상화가 필요한 경우가 종종 있다. 즉, 코드가 복잡해진다는 뜻이다. 무조건 OCP를 적용하면 오히려 코드가 복잡해지고 비효율적일 수 있기 때문에 적절한 경우에만 OCP를 적용해야 한다.

그렇다면 바뀌는 부분 중에서 OCP를 적용할 만큼 중요한 부분은 어떻게 찾을 수 있나?
🤷‍♀️ 안타깝지만 .. 이건 경험과 지식의 영역이다. 여러 디자인을 살펴보고 공부하다보면 중요한 부분을 골라내는 안목이 자연스레 높아지게 된다.


(2) 데코레이터 패턴 적용하기

앞에서 데코레이터 패턴을 스타벅스로 설명했기 때문에 바로 적용할 수 있다 !
적용해보자.

이 구조를 활용하면 이제 옵션들을 실행 중에 동적으로 추가할 수 있다.

한 가지 주의할 점은,
원래 있던 구성 요소가 들어갈 자리에 데코레이터 객체가 들어갈 수 있어야 하기 때문에
데코레이터 객체가 자신이 감싸고 있는 개체랑 같은 인터페이스를 가져야 한다.
그렇기 때문에 원두 객체 랑 CondimentDecorator 객체가 똑같이 Beverage 객체를 상속하고 있다.

근데 Beverage 클래스가 왜 인터페이스가 아니라 추상 클래스인가요?
💁‍♀️ 초기 스타버즈에서 시스템을 개발했을 때 추상 클래스로 구현했기 때문에 추상 클래스인 것이지, 인터페이스로 정의해도 상관 없다 !

코드 만들어보기

[Beverage Class]

public abstract class Beverage {
	String description = "제목 없음";
    
    public String getDescription() {
    	return description;
    }
    
    public abstract double cost();
}

[데코레이터 Class]

public abstract class CondimentDecorator extends Beverage {
	// 각 데코레이터 감쌀 객체를 여기서 저장한다
    // 이 때 주의할 건, Beverage 슈퍼클래스 타입을 사용한다는 거 !
    Beverage beverage;
    public abstract String getDescription();
}

[음료 Class]

public class Espresso extends Beverage {
	
    public Espresso() {
    	description = "에스프레소";
    }
    
    public double cost() {
    	return 1.99;
    }
    
}

[옵션 Class]

public class Whip extends CondimentDecorator {
	
    public Whip(Beverage beverage) {
    	this.beverage = beverage;
    }
    
    public String getDescription() {
    	return beverage.getDescription + ", 휘핑크림";
    }
    
    public double cost() {
    	return beverage.cost + .20;
    }
    
}

[주문 테스트 코드]

public class StarbuzzCoffee {
	
    public static void main(String args[]) {
    	/*일반 에스프레소*/
    	Beverage beverage1 = new Espresso();
        // beverage1 정보 출력
        
        /*에스프레소에 휘핑크림 추가*/
        Beverage beverage2 = new Espresso();
        beverage2 = new Whip(beverage2);
        // beverage2 정보 출력
        
        /*디카페인 커피에 두유랑 모카 시럽 2번 추가*/
        Beverage beverage3 = new Decaf();
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Mocha(beverage3);
        // beverage3 정보 출력
    }
    
}


3. 데코레이터가 적용된 사례: Java I/O

(1) java.io 클래스와 데코레이터 패턴


(2) 나만의 Java I/O 데코레이터 만들기

목표: 입력 스트림에 있는 대문자를 전부 소문자로 바꾸기

public class LowerCaseInputStream extends FilterInputStream {
	
    public LowerCaseInputStream(InputStream in) {
    	super(in);
    }
    
    public int read() throws IOException {
    	int c = in.read();
        return (c == -1 ? c : Character.toLowerCase((char)
    }
    
    public int read(byte[] b, int offset, int len) throws IOException {
    	int result = in.read(b, offset, len);
        for (int i = offset; i < offset ; i++) {
        	b[i] = (byte)Character.toLoserCase((char)b[i]);
        }
        return result;
    }
    
}
public class InputTest {
	
    public static void main(String[] args) throws IOException {
    	int c;
        
        try {
        	InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
            
            while ((c = in.read()) >= 0) }
            	System.out.print((char)c);
            }
            
            in.close();
        } catch (IOException e) {
        	e.printStackTrace();
        }
    }
    
}

4. 정리

[데코레이터 패턴 추가 정보 및 리마인드]

  • 데코레이터 클래스의 형식은 그 클래스가 감싸는 클래스 형식을 반영
    (상속 or 인터페이스 구현으로 자신이 감쌀 클래스와 같은 형식 가짐)

  • 구성 요소를 감싸는 데코레이터의 개수에는 제한이 없음

  • 구성 요소의 클라이언트는 데코레이터의 존재를 알 수 없음
    (단, 클라이언트가 구성 요소의 구체적인 형식에 의존하는 경우는 예외)

  • 데코레이터 패턴을 사용하면 자잘한 객체가 너무 많이 추가될 수 있고,
    데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해질 수 있음

객체지향 기초

  • 추상화
  • 캡슐화
  • 다형성
  • 상속

객체지향 원칙

  • 바뀌는 부분은 캡슐화 한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그램이한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
  • OCP: 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.

객체지향 패턴

전략 패턴

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

옵저버 패턴

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다 (one-to-many) 의존성을 정의한다.

데코레이터 패턴

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

0개의 댓글