5. 빌더 패턴 (Builder)
메서드 체이닝을 활용해 복잡한 객체를 단계적으로 조립하는 빌더 패턴을 알아봅니다.
한 줄 요약
고를 것이 많은 객체를 한 번에 만들지 않고, 이름 붙은 단계를 하나씩 밟아 조립하게 하는 패턴이에요. 다 조립했을 때 한 번에 검사하고 건네줘요.
문제: 인자가 줄줄이 늘어선 생성자
햄버거 가게 주문 프로그램을 만든다고 해 볼게요. 손님이 고를 것이 많아요.
- 빵 종류, 패티 장수
- 치즈 넣기, 양상추·토마토 빼기
- 소스 맛
가장 먼저 떠오르는 방법은 생성자가 이걸 전부 인자로 받는 거예요. 매번 다 쓰지 않아도 되게 기본값도 붙여 둘게요.
[햄버거]를 설계하자:
'빵'을 [문자열]인 ""으로 정하자
'패티'를 [숫자]인 0으로 정하자
'치즈'를 [논리]인 거짓으로 정하자
'양상추'를 [논리]인 거짓으로 정하자
'토마토'를 [논리]인 거짓으로 정하자
'소스'를 [문자열]인 ""으로 정하자
(참고: 고를 것이 많아서 생성자의 인자도 많아요)
처음 만들어질 때 ([문자열]인 '빵' = "참깨빵", [숫자]인 '패티' = 1, [논리]인 '치즈' = 거짓, [논리]인 '양상추' = 참, [논리]인 '토마토' = 참, [문자열]인 '소스' = "기본") 다음과 같이 하자:
'나'의 '빵'을 '빵'으로 정하자
'나'의 '패티'를 '패티'로 정하자
'나'의 '치즈'를 '치즈'로 정하자
'나'의 '양상추'를 '양상추'로 정하자
'나'의 '토마토'를 '토마토'로 정하자
'나'의 '소스'를 '소스'로 정하자
<보여주기>를 만들자 ():
틀"빵={'나'의 '빵'}, 패티={'나'의 '패티'}장, 치즈={'나'의 '치즈'}, 양상추={'나'의 '양상추'}, 토마토={'나'의 '토마토'}, 소스={'나'의 '소스'}"를 출력하자
(참고: 소스만 바꾸고 싶어도 앞의 인자를 전부 써야 해요)
'매운버거'를 새로운 [햄버거]("참깨빵", 1, 거짓, 참, 참, "매운맛")으로 정하자
'매운버거'의 <보여주기>()를 실행하자
(참고: 치즈를 넣고 토마토를 빼려다 순서를 헷갈렸어요)
'치즈버거'를 새로운 [햄버거]("호밀빵", 2, 참, 거짓, 참)으로 정하자
'치즈버거'의 <보여주기>()를 실행하자
(참고: 패티 0장 버거도 아무 말 없이 만들어져요)
'빈버거'를 새로운 [햄버거]("참깨빵", 0)으로 정하자
'빈버거'의 <보여주기>()를 실행하자
실행 결과
빵=참깨빵, 패티=1장, 치즈=거짓, 양상추=참, 토마토=참, 소스=매운맛
빵=호밀빵, 패티=2장, 치즈=참, 양상추=거짓, 토마토=참, 소스=기본
빵=참깨빵, 패티=0장, 치즈=거짓, 양상추=참, 토마토=참, 소스=기본
두 번째 줄을 보세요. 토마토를 빼려고 했는데 양상추가 빠졌어요. 에러는 없었어요.
- 무엇이 무엇인지 안 읽혀요.
("호밀빵", 2, 참, 거짓, 참)만 봐서는참이 치즈인지 토마토인지 알 수 없어요. 생성자를 다시 열어 순서를 세어 봐야 해요. - 순서를 헷갈리면 조용히 틀려요. 같은
[논리]인자가 셋 나란히 있으니, 하나 밀려 써도 타입이 맞아서 하리도 잡아 줄 수 없어요. - 맨 뒤 하나만 바꿔도 앞을 전부 써야 해요. 기본값이 있어도, 소스만 바꾸려면 그 앞의 다섯 개를 다 채워야 해요.
- 말이 안 되는 조합도 만들어져요. 패티 0장 버거처럼 이상한 주문을 막을 자리가 없어요. 생성자 안에서 검사하면 되지만, 고를 것이 늘수록 생성자가 점점 커져요.
해결책: 이름 붙은 단계로 하나씩 조립하기
빌더 패턴은 “한 번에 다 넘기기”를 “단계별로 하나씩 정하기”로 바꿔요.
- 만들 객체(햄버거)는 기본값이 들어간 채로 시작해요.
- 빌더가 만들던 햄버거를 들고 있고, 고를 것마다 이름이 붙은 메서드를 하나씩 가져요.
<치즈넣기>,<토마토빼기>처럼요. - 각 단계는 끝에서 빌더 자신(
'나')을 돌려줘요. 그래서의 <다음단계>()로 줄줄이 이어 부를 수 있어요. 이걸 메서드 체이닝(Method Chaining)이라고 불러요. - 다 정했으면
<완성>을 불러요. 빌더는 이때 한 번에 검사하고 햄버거를 건네줘요.
비유: 샌드위치 가게의 주문대
샌드위치 가게에서는 점원이 “빵은요?”, “치즈는요?”, “야채 뺄 거 있으세요?”, “소스는요?” 하고 하나씩 물어봐요. 손님은 원하는 것만 대답하고, 말하지 않은 건 기본으로 들어가요. 마지막에 점원이 “주문 확인할게요”라며 한 번에 살펴보고 만들어 주죠. 이 점원이 바로 “빌더”예요. 손님이 재료 여섯 개를 순서대로 한 번에 외쳐야 하는 가게보다 훨씬 덜 헷갈려요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 만들어질 객체 | [햄버거] |
기본값을 가진 채로 시작해요. 생성자가 인자를 받지 않아요 |
| 빌더 | [햄버거빌더] |
만들던 햄버거를 들고, 단계마다 하나씩 정해요 |
| 단계 | <빵>, <패티>, <치즈넣기>, <토마토빼기>, <소스> |
하나를 정하고 빌더 자신을 돌려줘요 |
| 마무리 | <완성> |
한 번에 검사하고, 다 만든 햄버거를 건네줘요 |
하리로 만들기
같은 주문을 빌더 패턴으로 다시 만들어 볼게요.
[햄버거]를 설계하자:
'빵'을 [문자열]인 "참깨빵"으로 정하자
'패티'를 [숫자]인 1로 정하자
'치즈'를 [논리]인 거짓으로 정하자
'양상추'를 [논리]인 참으로 정하자
'토마토'를 [논리]인 참으로 정하자
'소스'를 [문자열]인 "기본"으로 정하자
<보여주기>를 만들자 ():
틀"빵={'나'의 '빵'}, 패티={'나'의 '패티'}장, 치즈={'나'의 '치즈'}, 양상추={'나'의 '양상추'}, 토마토={'나'의 '토마토'}, 소스={'나'의 '소스'}"를 출력하자
(참고: 1단계 — 빌더가 만들던 햄버거를 들고 있어요)
[햄버거빌더]를 설계하자:
'버거'를 [햄버거]인 새로운 [햄버거]()로 정하여 숨기자
(참고: 2단계 — 단계마다 이름이 붙은 메서드. 끝에서 '나'를 돌려줘서 이어 부를 수 있어요)
[햄버거빌더]를 돌려주는 <빵>을 만들자 ([문자열]인 '종류'):
'나'의 '버거'의 '빵'을 '종류'로 정하자
'나'를 돌려주자
[햄버거빌더]를 돌려주는 <패티>를 만들자 ([숫자]인 '장수'):
'나'의 '버거'의 '패티'를 '장수'로 정하자
'나'를 돌려주자
[햄버거빌더]를 돌려주는 <치즈넣기>를 만들자 ():
'나'의 '버거'의 '치즈'를 참으로 정하자
'나'를 돌려주자
[햄버거빌더]를 돌려주는 <토마토빼기>를 만들자 ():
'나'의 '버거'의 '토마토'를 거짓으로 정하자
'나'를 돌려주자
[햄버거빌더]를 돌려주는 <소스>를 만들자 ([문자열]인 '맛'):
'나'의 '버거'의 '소스'를 '맛'으로 정하자
'나'를 돌려주자
(참고: 3단계 — 다 만들었을 때 한 번에 검사하고 건네줘요)
[햄버거]를 돌려주는 <완성>을 만들자 ():
만약 ('나'의 '버거'의 '패티'가 1보다 작다) 라면:
새로운 [오류]("패티는 1장 이상이어야 해요.")를 발생시키자
'나'의 '버거'를 돌려주자
'매운버거'를 새로운 [햄버거빌더]()의 <소스>("매운맛")의 <완성>()으로 정하자
'매운버거'의 <보여주기>()를 실행하자
'치즈버거'를 새로운 [햄버거빌더]()의 <빵>("호밀빵")의 <패티>(2)의 <치즈넣기>()의 <토마토빼기>()의 <완성>()으로 정하자
'치즈버거'의 <보여주기>()를 실행하자
일단 해보자:
'빈버거'를 새로운 [햄버거빌더]()의 <패티>(0)의 <완성>()으로 정하자
오류가 발생했다면 ('에러'):
('에러'의 '메시지')를 출력하자
실행 결과
빵=참깨빵, 패티=1장, 치즈=거짓, 양상추=참, 토마토=참, 소스=매운맛
빵=호밀빵, 패티=2장, 치즈=참, 양상추=참, 토마토=거짓, 소스=기본
패티는 1장 이상이어야 해요.
첫 줄은 리팩터링 전과 같아요. 두 번째 줄에서는 이번에야말로 토마토가 빠졌어요. 그리고 패티 0장 주문은 <완성>에서 막혔어요.
단계별로 다시 보기
- 1단계 — 빌더가 들고 있기.
[햄버거]는 이제 생성자가 인자를 받지 않아요. 기본값으로 시작하고,[햄버거빌더]가 그 햄버거를'버거'에 들고 있어요. - 2단계 — 이름이 곧 설명.
<치즈넣기>()와<토마토빼기>()는 읽기만 해도 무엇을 하는지 알 수 있어요. 순서를 셀 필요가 없고, 원하는 단계만 원하는 순서로 부르면 돼요. 부르지 않은 것은 기본값 그대로예요. '나'를 돌려주자가 이어 부르기의 비밀. 각 단계가 빌더 자신을 돌려주니까,새로운 [햄버거빌더]()의 <빵>(...)의 결과도 빌더예요. 그래서 바로 뒤에의 <패티>(2)를 또 붙일 수 있어요.- 3단계 — 마지막에 한 번 검사. 단계마다 검사하지 않고,
<완성>에서 다 고른 뒤 한 번에 살펴봐요. “치즈를 넣으려면 패티가 있어야 해요”처럼 여러 단계를 함께 봐야 하는 규칙도 여기에 두면 돼요.
한 줄이 너무 길 때는요?
이어 부르기는 짧을 때 보기 좋아요. 단계가 많으면 빌더를 이름표에 담아 두고 한 줄에 한 단계씩 불러도 돼요. 각 단계가 빌더의 햄버거를 직접 바꾸기 때문에 결과는 같아요.
'빌더'를 새로운 [햄버거빌더]()로 정하자
'빌더'의 <빵>("호밀빵")을 실행하자
'빌더'의 <치즈넣기>()를 실행하자
'치즈버거'를 '빌더'의 <완성>()으로 정하자
새 선택지를 더해 보면
“베이컨 추가”가 생겼다고 해 볼게요.
- 앞의 방식: 생성자에 인자를 하나 더 붙여야 해요. 맨 뒤에 붙이면 베이컨만 넣고 싶어도 앞의 여섯 개를 다 써야 하고, 중간에 끼우면 이미 쓰고 있는 모든
새로운 [햄버거](...)의 순서가 어긋나요. - 빌더 패턴:
[햄버거]에'베이컨'값을 하나,[햄버거빌더]에<베이컨넣기>단계를 하나 더하면 끝이에요. 이미 쓰고 있는 주문 코드는 한 글자도 바뀌지 않아요.
직접 해 보기
위 코드에 베이컨을 더해 보세요.[햄버거]에'베이컨'(기본 거짓)을,[햄버거빌더]에<베이컨넣기>를 만들고,<보여주기>에도 베이컨을 보여 주세요. 그리고<완성>에 “베이컨은 패티가 2장 이상일 때만 넣을 수 있어요”라는 검사를 더해 보세요.
언제 쓰면 좋을까요?
- 생성자의 인자가 많고, 그중 상당수가 선택일 때
- 같은 타입의 인자가 나란히 있어서 순서를 헷갈리기 쉬울 때
- 여러 선택지를 함께 봐야 하는 규칙이 있어서, 다 고른 뒤 한 번에 검사하고 싶을 때
- 객체를 여러 단계에 걸쳐 조금씩 만들어야 할 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 단계마다 이름이 있어서 읽기 쉬워요 | 빌더 클래스가 하나 더 생기고, 선택지마다 메서드가 생겨요 |
| 원하는 것만, 원하는 순서로 정할 수 있어요 | 만들 객체와 빌더 두 곳에 선택지가 나뉘어 있어요 |
| 새 선택지를 더해도 이미 쓰는 코드가 안 바뀌어요 | 이 예제의 빌더는 햄버거를 하나만 들고 있어서, 같은 빌더로 <완성>을 두 번 부르면 같은 햄버거가 나와요 |
<완성>에서 한 번에 검사할 수 있어요 |
너무 이른 패턴은 독이에요.
고를 것이 두세 개뿐이라면 생성자 인자로 충분해요. 인자 순서를 헷갈리기 시작하거나, 같은 타입의 인자가 줄줄이 늘어서기 시작할 때 빌더를 만들어도 늦지 않아요.
다른 패턴과의 관계
- 팩토리 패턴과 비교해 보세요. 팩토리는 어떤 종류를 만들지 고르는 데 집중하고, 빌더는 하나를 여러 단계에 걸쳐 조립하는 데 집중해요. “불고기버거 주세요”처럼 자주 쓰는 조합은 공장 메서드가 빌더를 대신 불러 주게 만들 수도 있어요.
- 퍼사드 패턴과 함께 쓸 수 있어요. 여러 객체를 조립해야 하는 복잡한 준비 과정을 퍼사드 뒤에 숨기고, 퍼사드 안에서 빌더로 조립하면 쓰는 쪽은 한 줄로 끝나요.
- 데코레이터 패턴과 헷갈리지 마세요. 둘 다 “하나씩 더한다”처럼 보이지만, 빌더는 만드는 과정에서 값을 정하고 끝나면 완성된 객체 하나가 남아요. 데코레이터는 만든 뒤에 껍질을 겹겹이 씌워 기능을 더해요.