7. 데코레이터 패턴 (Decorator)
객체에 껍질을 씌우듯 동적으로 기능을 추가하는 데코레이터 패턴을 알아봅니다.
한 줄 요약
객체를 같은 약속을 따르는 껍질로 감싸서 기능을 덧붙이는 패턴이에요. 껍질 위에 껍질을 또 씌울 수 있어서, 기능을 원하는 만큼 원하는 순서로 쌓을 수 있어요.
문제: 조합마다 하나씩 늘어나는 클래스
카페 계산 프로그램을 만든다고 해 볼게요. 기본은 에스프레소이고, 손님이 토핑을 골라요.
- 우유 +500원
- 시럽 +300원
- 샷 +500원
가장 먼저 떠오르는 방법은 메뉴마다 상속으로 클래스를 하나씩 만드는 거예요.
[에스프레소]를 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
3000을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
"에스프레소"를 돌려주자
(참고: 조합마다 클래스를 하나씩 만들어요)
[에스프레소]를 바탕으로 [라떼]를 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
(3000 + 500)을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
"에스프레소 + 우유"를 돌려주자
[에스프레소]를 바탕으로 [바닐라라떼]를 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
(3000 + 500 + 300)을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
"에스프레소 + 우유 + 시럽"을 돌려주자
(참고: 우유 없이 시럽만 넣으려면 또 하나 만들어야 해요)
[에스프레소]를 바탕으로 [바닐라에스프레소]를 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
(3000 + 300)을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
"에스프레소 + 시럽"을 돌려주자
'메뉴'를 [새로운 [에스프레소](), 새로운 [라떼](), 새로운 [바닐라라떼](), 새로운 [바닐라에스프레소]()]로 정하자
'메뉴'의 '커피'마다 반복하자:
틀"{'커피'의 <설명>()}: {'커피'의 <가격>()}원"을 출력하자
실행 결과
에스프레소: 3000원
에스프레소 + 우유: 3500원
에스프레소 + 우유 + 시럽: 3800원
에스프레소 + 시럽: 3300원
메뉴가 네 개일 때는 괜찮아요. 그런데 토핑으로 고를 수 있는 게 늘어나면 금방 아파져요.
- 조합만큼 클래스가 늘어나요. 토핑이 세 가지면 넣고 빼는 조합이 8가지, 네 가지면 16가지예요. 토핑 하나가 늘 때마다 클래스 수가 두 배가 돼요.
- 같은 계산이 여기저기 복사돼요. “시럽은 +300원”이
[바닐라라떼]와[바닐라에스프레소]에 따로 들어 있어요. 시럽이 400원으로 오르면 시럽이 들어간 클래스를 모두 찾아 고쳐야 해요. - 같은 토핑을 두 번 넣는 것도 클래스예요. “샷 두 번”을 하려면
[투샷에스프레소]를 또 만들어야 해요. - 손님의 주문은 실행 중에 들어와요. 그런데 클래스는 코드를 쓸 때 정해져 있어요. 아무도 예상하지 못한 조합(샷 + 우유 + 시럽 두 번)을 주문하면 받아 줄 클래스가 없어요.
해결책: 같은 모양의 껍질로 감싸기
데코레이터 패턴은 토핑을 “커피의 종류”가 아니라 “커피를 감싸는 껍질“로 봐요.
- 커피라면 지킬 약속, 곧 규정을 만들어요.
<가격>과<설명>을 할 줄 알아야 해요. - 껍질도 이 약속을 따르게 해요. 껍질은 안쪽에 커피를 하나 품고 있고, 기본으로는 안쪽 커피에게 일을 그대로 넘겨요.
- 토핑마다 껍질을 하나씩 만들고, “안쪽 결과에 무엇을 더할지”만 적어요. 우유 껍질은 안쪽 가격에 500을 더해요.
- 껍질도 커피니까, 껍질을 또 다른 껍질로 감쌀 수 있어요. 순서도 개수도 자유예요.
비유: 선물 포장
선물을 상자에 넣고, 포장지로 싸고, 리본을 달아요. 포장지로 싼 상자도 여전히 “선물”이라서 그 위에 리본을 또 달 수 있어요. 받는 사람은 겉에서부터 하나씩 풀면서 안쪽 선물에 닿아요. 포장 한 겹 한 겹이 바로 “데코레이터”예요. 포장지 종류가 늘어도 “포장된 상자” 종류를 미리 다 만들어 둘 필요는 없죠.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 약속 | [커피] (규정) |
커피든 껍질이든 할 줄 알아야 하는 일을 정해요 |
| 알맹이 | [에스프레소] |
감싸지는 기본 객체예요 |
| 기본 껍질 | [토핑] |
안쪽 커피를 품고, 기본으로는 일을 그대로 넘겨요 |
| 구체적인 껍질 | [우유], [시럽], [샷] |
안쪽 결과에 자기 몫을 더해요 |
하리로 만들기
같은 카페를 데코레이터 패턴으로 다시 만들어 볼게요. 코드 속 (참고: n단계)를 따라 읽어 보세요.
(참고: 1단계 — 커피라면 지킬 약속)
[커피]를 규정하자:
[숫자]를 돌려주는 <가격>이 있어야 한다 ()
[문자열]을 돌려주는 <설명>이 있어야 한다 ()
[커피]를 따르는 [에스프레소]를 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
3000을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
"에스프레소"를 돌려주자
(참고: 2단계 — 커피를 하나 품고 있는 껍질. 껍질도 커피라서, 기본으로는 안쪽에 그대로 맡겨요)
[커피]를 따르는 [토핑]을 설계하자:
'안쪽'을 [커피]인 비어있음으로 정하여 물려주자
처음 만들어질 때 ([커피]인 '안쪽') 다음과 같이 하자:
'나'의 '안쪽'을 '안쪽'으로 정하자
[숫자]를 돌려주는 <가격>을 만들자 ():
'나'의 '안쪽'의 <가격>()을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
'나'의 '안쪽'의 <설명>()을 돌려주자
(참고: 3단계 — 껍질마다 '안쪽에 무엇을 더할지'만 적어요)
[토핑]을 바탕으로 [우유]를 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
('나'의 '안쪽'의 <가격>() + 500)을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
틀"{'나'의 '안쪽'의 <설명>()} + 우유"를 돌려주자
[토핑]을 바탕으로 [시럽]을 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
('나'의 '안쪽'의 <가격>() + 300)을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
틀"{'나'의 '안쪽'의 <설명>()} + 시럽"을 돌려주자
[토핑]을 바탕으로 [샷]을 설계하자:
[숫자]를 돌려주는 <가격>을 만들자 ():
('나'의 '안쪽'의 <가격>() + 500)을 돌려주자
[문자열]을 돌려주는 <설명>을 만들자 ():
틀"{'나'의 '안쪽'의 <설명>()} + 샷"을 돌려주자
(참고: 껍질을 원하는 만큼, 원하는 순서로 씌워요)
'바닐라라떼'를 [커피]인 새로운 [시럽](새로운 [우유](새로운 [에스프레소]()))로 정하자
'진한커피'를 [커피]인 새로운 [샷](새로운 [샷](새로운 [에스프레소]()))로 정하자
'메뉴'를 [새로운 [에스프레소](), '바닐라라떼', '진한커피']로 정하자
'메뉴'의 '커피'마다 반복하자:
틀"{'커피'의 <설명>()}: {'커피'의 <가격>()}원"을 출력하자
(참고: 손님이 고른 순서대로, 실행 중에 껍질을 씌울 수도 있어요)
'주문'을 ["샷", "우유", "시럽", "시럽"]으로 정하자
'손님커피'를 [커피]인 새로운 [에스프레소]()로 정하자
'주문'의 '이름'마다 반복하자:
'이름'에 따라 나누자:
"우유" 인 경우:
'손님커피'를 새로운 [우유]('손님커피')로 정하자
"시럽" 인 경우:
'손님커피'를 새로운 [시럽]('손님커피')로 정하자
"샷" 인 경우:
'손님커피'를 새로운 [샷]('손님커피')로 정하자
틀"손님 주문 — {'손님커피'의 <설명>()}: {'손님커피'의 <가격>()}원"을 출력하자
실행 결과
에스프레소: 3000원
에스프레소 + 우유 + 시럽: 3800원
에스프레소 + 샷 + 샷: 4000원
손님 주문 — 에스프레소 + 샷 + 우유 + 시럽 + 시럽: 4600원
토핑 클래스는 세 개뿐인데, 어떤 조합이든 만들 수 있어요. 마지막 줄처럼 아무도 미리 만들어 두지 않은 조합도요.
단계별로 다시 보기
- 1단계 — 약속 만들기.
[커피]는 “가격과 설명을 알려 줄 수 있으면 커피”라는 약속이에요. 알맹이도 껍질도 이 약속을 따르기 때문에, 쓰는 쪽은 상대가 알맹이인지 껍질인지 신경 쓰지 않아요. - 2단계 — 기본 껍질.
[토핑]은 안쪽 커피를'안쪽'에 품고, 기본으로는 일을 그대로 넘겨요.'안쪽'은물려주자로 만들어서 밖에서는 못 건드리고, 자식 껍질만 쓸 수 있어요. - 3단계 — 껍질마다 자기 몫만.
[우유]는 “안쪽 가격 + 500, 안쪽 설명 + 우유”만 알아요. 만들어질 때 할 일은 따로 적지 않았는데, 자식에 생성자가 없으면 부모([토핑])의 생성자가 대신 쓰이기 때문이에요. - 감싸는 순서가 곧 결과의 순서.
새로운 [시럽](새로운 [우유](...))은 우유로 먼저 감싸고 시럽으로 한 번 더 감싸요. 그래서 설명도 “우유 + 시럽” 순서로 나와요.
<가격>을 부르면 무슨 일이 일어나나요?
'바닐라라떼'의 <가격>()을 부르면 맨 바깥[시럽]이 먼저 안쪽[우유]에게 묻고,[우유]는 다시[에스프레소]에게 물어요.[에스프레소]가 3000을 돌려주면[우유]가 500을 더해 3500,[시럽]이 300을 더해 3800이 돼요. 선물 포장을 풀 듯 바깥에서 안으로 들어갔다가, 결과는 안에서 바깥으로 쌓여 나와요.
새 토핑을 더해 보면
“휘핑크림” +700원이 생겼다고 해 볼게요.
- 앞의 방식: 휘핑크림이 들어갈 수 있는 모든 조합마다 클래스를 새로 만들어야 해요. 지금 있는 클래스 수만큼 더 생겨요.
- 데코레이터 패턴:
[토핑]을 바탕으로 [휘핑]이라는 껍질 하나만 만들면 돼요. 이미 있는 모든 조합 위에 바로 얹을 수 있어요.
직접 해 보기
위 코드에[휘핑]껍질을 더해 보세요. 가격은 안쪽 + 700원, 설명은+ 휘핑이에요. 그리고새로운 [휘핑](새로운 [우유](새로운 [에스프레소]()))의 설명과 가격을 출력해 보세요. 다른 클래스는 하나도 고칠 필요가 없어요.
언제 쓰면 좋을까요?
- 기능을 골라서 조합해야 하고, 조합 수가 많아서 클래스로 다 만들 수 없을 때
- 같은 기능을 여러 번 겹쳐 쓰거나, 순서를 바꿔 쓰고 싶을 때
- 이미 있는 클래스를 고치지 않고 기능을 덧붙이고 싶을 때 (남이 만든 클래스, 패키지의 클래스)
- 조합이 실행 중에 정해질 때 (사용자 선택, 설정)
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 토핑 수만큼만 클래스를 만들면 모든 조합이 가능해요 | 작은 객체가 겹겹이 생겨서 한눈에 보기 어려워요 |
| 같은 계산이 한곳(그 껍질)에만 있어요 | 감싸는 순서가 결과에 영향을 줘서, 순서를 신경 써야 할 때가 있어요 |
| 실행 중에 기능을 붙였다 뗐다 할 수 있어요 | 중간 껍질 하나만 쏙 빼기는 어려워요 |
| 원래 클래스를 고치지 않아요 |
너무 이른 패턴은 독이에요.
메뉴가 고정되어 있고 조합이 두세 개뿐이라면, 메뉴마다 클래스를 두거나 값 하나로 처리하는 게 더 읽기 쉬워요. 조합이 늘어나 클래스가 불어나기 시작할 때 껍질로 바꿔도 늦지 않아요.