하리Hari

5. 빌더 패턴 (Builder)

메서드 체이닝을 활용해 복잡한 객체를 단계적으로 조립하는 빌더 패턴을 알아봅니다.

한 줄 요약
고를 것이 많은 객체를 한 번에 만들지 않고, 이름 붙은 단계를 하나씩 밟아 조립하게 하는 패턴이에요. 다 조립했을 때 한 번에 검사하고 건네줘요.

문제: 인자가 줄줄이 늘어선 생성자

햄버거 가게 주문 프로그램을 만든다고 해 볼게요. 손님이 고를 것이 많아요.

  • 빵 종류, 패티 장수
  • 치즈 넣기, 양상추·토마토 빼기
  • 소스 맛

가장 먼저 떠오르는 방법은 생성자가 이걸 전부 인자로 받는 거예요. 매번 다 쓰지 않아도 되게 기본값도 붙여 둘게요.

[햄버거]를 설계하자:
    '빵'을 [문자열]인 ""으로 정하자
    '패티'를 [숫자]인 0으로 정하자
    '치즈'를 [논리]인 거짓으로 정하자
    '양상추'를 [논리]인 거짓으로 정하자
    '토마토'를 [논리]인 거짓으로 정하자
    '소스'를 [문자열]인 ""으로 정하자

    (참고: 고를 것이 많아서 생성자의 인자도 많아요)
    처음 만들어질 때 ([문자열]인 '빵' = "참깨빵", [숫자]인 '패티' = 1, [논리]인 '치즈' = 거짓, [논리]인 '양상추' = 참, [논리]인 '토마토' = 참, [문자열]인 '소스' = "기본") 다음과 같이 하자:
        '나'의 '빵'을 '빵'으로 정하자
        '나'의 '패티'를 '패티'로 정하자
        '나'의 '치즈'를 '치즈'로 정하자
        '나'의 '양상추'를 '양상추'로 정하자
        '나'의 '토마토'를 '토마토'로 정하자
        '나'의 '소스'를 '소스'로 정하자

    <보여주기>를 만들자 ():
        틀"빵={'나'의 '빵'}, 패티={'나'의 '패티'}장, 치즈={'나'의 '치즈'}, 양상추={'나'의 '양상추'}, 토마토={'나'의 '토마토'}, 소스={'나'의 '소스'}"를 출력하자

(참고: 소스만 바꾸고 싶어도 앞의 인자를 전부 써야 해요)
'매운버거'를 새로운 [햄버거]("참깨빵", 1, 거짓, 참, 참, "매운맛")으로 정하자
'매운버거'의 <보여주기>()를 실행하자

(참고: 치즈를 넣고 토마토를 빼려다 순서를 헷갈렸어요)
'치즈버거'를 새로운 [햄버거]("호밀빵", 2, 참, 거짓, 참)으로 정하자
'치즈버거'의 <보여주기>()를 실행하자

(참고: 패티 0장 버거도 아무 말 없이 만들어져요)
'빈버거'를 새로운 [햄버거]("참깨빵", 0)으로 정하자
'빈버거'의 <보여주기>()를 실행하자

실행 결과
빵=참깨빵, 패티=1장, 치즈=거짓, 양상추=참, 토마토=참, 소스=매운맛
빵=호밀빵, 패티=2장, 치즈=참, 양상추=거짓, 토마토=참, 소스=기본
빵=참깨빵, 패티=0장, 치즈=거짓, 양상추=참, 토마토=참, 소스=기본

두 번째 줄을 보세요. 토마토를 빼려고 했는데 양상추가 빠졌어요. 에러는 없었어요.

  • 무엇이 무엇인지 안 읽혀요. ("호밀빵", 2, 참, 거짓, 참)만 봐서는 참이 치즈인지 토마토인지 알 수 없어요. 생성자를 다시 열어 순서를 세어 봐야 해요.
  • 순서를 헷갈리면 조용히 틀려요. 같은 [논리] 인자가 셋 나란히 있으니, 하나 밀려 써도 타입이 맞아서 하리도 잡아 줄 수 없어요.
  • 맨 뒤 하나만 바꿔도 앞을 전부 써야 해요. 기본값이 있어도, 소스만 바꾸려면 그 앞의 다섯 개를 다 채워야 해요.
  • 말이 안 되는 조합도 만들어져요. 패티 0장 버거처럼 이상한 주문을 막을 자리가 없어요. 생성자 안에서 검사하면 되지만, 고를 것이 늘수록 생성자가 점점 커져요.

해결책: 이름 붙은 단계로 하나씩 조립하기

빌더 패턴은 “한 번에 다 넘기기”를 “단계별로 하나씩 정하기”로 바꿔요.

  1. 만들 객체(햄버거)는 기본값이 들어간 채로 시작해요.
  2. 빌더가 만들던 햄버거를 들고 있고, 고를 것마다 이름이 붙은 메서드를 하나씩 가져요. <치즈넣기>, <토마토빼기>처럼요.
  3. 각 단계는 끝에서 빌더 자신('나')을 돌려줘요. 그래서 의 <다음단계>()로 줄줄이 이어 부를 수 있어요. 이걸 메서드 체이닝(Method Chaining)이라고 불러요.
  4. 다 정했으면 <완성>을 불러요. 빌더는 이때 한 번에 검사하고 햄버거를 건네줘요.

비유: 샌드위치 가게의 주문대

샌드위치 가게에서는 점원이 “빵은요?”, “치즈는요?”, “야채 뺄 거 있으세요?”, “소스는요?” 하고 하나씩 물어봐요. 손님은 원하는 것만 대답하고, 말하지 않은 건 기본으로 들어가요. 마지막에 점원이 “주문 확인할게요”라며 한 번에 살펴보고 만들어 주죠. 이 점원이 바로 “빌더”예요. 손님이 재료 여섯 개를 순서대로 한 번에 외쳐야 하는 가게보다 훨씬 덜 헷갈려요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
만들어질 객체 [햄버거] 기본값을 가진 채로 시작해요. 생성자가 인자를 받지 않아요
빌더 [햄버거빌더] 만들던 햄버거를 들고, 단계마다 하나씩 정해요
단계 <빵>, <패티>, <치즈넣기>, <토마토빼기>, <소스> 하나를 정하고 빌더 자신을 돌려줘요
마무리 <완성> 한 번에 검사하고, 다 만든 햄버거를 건네줘요

하리로 만들기

같은 주문을 빌더 패턴으로 다시 만들어 볼게요.

[햄버거]를 설계하자:
    '빵'을 [문자열]인 "참깨빵"으로 정하자
    '패티'를 [숫자]인 1로 정하자
    '치즈'를 [논리]인 거짓으로 정하자
    '양상추'를 [논리]인 참으로 정하자
    '토마토'를 [논리]인 참으로 정하자
    '소스'를 [문자열]인 "기본"으로 정하자

    <보여주기>를 만들자 ():
        틀"빵={'나'의 '빵'}, 패티={'나'의 '패티'}장, 치즈={'나'의 '치즈'}, 양상추={'나'의 '양상추'}, 토마토={'나'의 '토마토'}, 소스={'나'의 '소스'}"를 출력하자

(참고: 1단계 — 빌더가 만들던 햄버거를 들고 있어요)
[햄버거빌더]를 설계하자:
    '버거'를 [햄버거]인 새로운 [햄버거]()로 정하여 숨기자

    (참고: 2단계 — 단계마다 이름이 붙은 메서드. 끝에서 '나'를 돌려줘서 이어 부를 수 있어요)
    [햄버거빌더]를 돌려주는 <빵>을 만들자 ([문자열]인 '종류'):
        '나'의 '버거'의 '빵'을 '종류'로 정하자
        '나'를 돌려주자

    [햄버거빌더]를 돌려주는 <패티>를 만들자 ([숫자]인 '장수'):
        '나'의 '버거'의 '패티'를 '장수'로 정하자
        '나'를 돌려주자

    [햄버거빌더]를 돌려주는 <치즈넣기>를 만들자 ():
        '나'의 '버거'의 '치즈'를 참으로 정하자
        '나'를 돌려주자

    [햄버거빌더]를 돌려주는 <토마토빼기>를 만들자 ():
        '나'의 '버거'의 '토마토'를 거짓으로 정하자
        '나'를 돌려주자

    [햄버거빌더]를 돌려주는 <소스>를 만들자 ([문자열]인 '맛'):
        '나'의 '버거'의 '소스'를 '맛'으로 정하자
        '나'를 돌려주자

    (참고: 3단계 — 다 만들었을 때 한 번에 검사하고 건네줘요)
    [햄버거]를 돌려주는 <완성>을 만들자 ():
        만약 ('나'의 '버거'의 '패티'가 1보다 작다) 라면:
            새로운 [오류]("패티는 1장 이상이어야 해요.")를 발생시키자
        '나'의 '버거'를 돌려주자

'매운버거'를 새로운 [햄버거빌더]()의 <소스>("매운맛")의 <완성>()으로 정하자
'매운버거'의 <보여주기>()를 실행하자

'치즈버거'를 새로운 [햄버거빌더]()의 <빵>("호밀빵")의 <패티>(2)의 <치즈넣기>()의 <토마토빼기>()의 <완성>()으로 정하자
'치즈버거'의 <보여주기>()를 실행하자

일단 해보자:
    '빈버거'를 새로운 [햄버거빌더]()의 <패티>(0)의 <완성>()으로 정하자
오류가 발생했다면 ('에러'):
    ('에러'의 '메시지')를 출력하자

실행 결과
빵=참깨빵, 패티=1장, 치즈=거짓, 양상추=참, 토마토=참, 소스=매운맛
빵=호밀빵, 패티=2장, 치즈=참, 양상추=참, 토마토=거짓, 소스=기본
패티는 1장 이상이어야 해요.

첫 줄은 리팩터링 전과 같아요. 두 번째 줄에서는 이번에야말로 토마토가 빠졌어요. 그리고 패티 0장 주문은 <완성>에서 막혔어요.

단계별로 다시 보기

  • 1단계 — 빌더가 들고 있기. [햄버거]는 이제 생성자가 인자를 받지 않아요. 기본값으로 시작하고, [햄버거빌더]가 그 햄버거를 '버거'에 들고 있어요.
  • 2단계 — 이름이 곧 설명. <치즈넣기>()와 <토마토빼기>()는 읽기만 해도 무엇을 하는지 알 수 있어요. 순서를 셀 필요가 없고, 원하는 단계만 원하는 순서로 부르면 돼요. 부르지 않은 것은 기본값 그대로예요.
  • '나'를 돌려주자가 이어 부르기의 비밀. 각 단계가 빌더 자신을 돌려주니까, 새로운 [햄버거빌더]()의 <빵>(...)의 결과도 빌더예요. 그래서 바로 뒤에 의 <패티>(2)를 또 붙일 수 있어요.
  • 3단계 — 마지막에 한 번 검사. 단계마다 검사하지 않고, <완성>에서 다 고른 뒤 한 번에 살펴봐요. “치즈를 넣으려면 패티가 있어야 해요”처럼 여러 단계를 함께 봐야 하는 규칙도 여기에 두면 돼요.

한 줄이 너무 길 때는요?
이어 부르기는 짧을 때 보기 좋아요. 단계가 많으면 빌더를 이름표에 담아 두고 한 줄에 한 단계씩 불러도 돼요. 각 단계가 빌더의 햄버거를 직접 바꾸기 때문에 결과는 같아요.
'빌더'를 새로운 [햄버거빌더]()로 정하자
'빌더'의 <빵>("호밀빵")을 실행하자
'빌더'의 <치즈넣기>()를 실행하자
'치즈버거'를 '빌더'의 <완성>()으로 정하자

새 선택지를 더해 보면

“베이컨 추가”가 생겼다고 해 볼게요.

  • 앞의 방식: 생성자에 인자를 하나 더 붙여야 해요. 맨 뒤에 붙이면 베이컨만 넣고 싶어도 앞의 여섯 개를 다 써야 하고, 중간에 끼우면 이미 쓰고 있는 모든 새로운 [햄버거](...)의 순서가 어긋나요.
  • 빌더 패턴: [햄버거]에 '베이컨' 값을 하나, [햄버거빌더]에 <베이컨넣기> 단계를 하나 더하면 끝이에요. 이미 쓰고 있는 주문 코드는 한 글자도 바뀌지 않아요.

직접 해 보기
위 코드에 베이컨을 더해 보세요. [햄버거]에 '베이컨'(기본 거짓)을, [햄버거빌더]에 <베이컨넣기>를 만들고, <보여주기>에도 베이컨을 보여 주세요. 그리고 <완성>에 “베이컨은 패티가 2장 이상일 때만 넣을 수 있어요”라는 검사를 더해 보세요.

언제 쓰면 좋을까요?

  • 생성자의 인자가 많고, 그중 상당수가 선택일 때
  • 같은 타입의 인자가 나란히 있어서 순서를 헷갈리기 쉬울 때
  • 여러 선택지를 함께 봐야 하는 규칙이 있어서, 다 고른 뒤 한 번에 검사하고 싶을 때
  • 객체를 여러 단계에 걸쳐 조금씩 만들어야 할 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
단계마다 이름이 있어서 읽기 쉬워요 빌더 클래스가 하나 더 생기고, 선택지마다 메서드가 생겨요
원하는 것만, 원하는 순서로 정할 수 있어요 만들 객체와 빌더 두 곳에 선택지가 나뉘어 있어요
새 선택지를 더해도 이미 쓰는 코드가 안 바뀌어요 이 예제의 빌더는 햄버거를 하나만 들고 있어서, 같은 빌더로 <완성>을 두 번 부르면 같은 햄버거가 나와요
<완성>에서 한 번에 검사할 수 있어요

너무 이른 패턴은 독이에요.
고를 것이 두세 개뿐이라면 생성자 인자로 충분해요. 인자 순서를 헷갈리기 시작하거나, 같은 타입의 인자가 줄줄이 늘어서기 시작할 때 빌더를 만들어도 늦지 않아요.

다른 패턴과의 관계

  • 팩토리 패턴과 비교해 보세요. 팩토리는 어떤 종류를 만들지 고르는 데 집중하고, 빌더는 하나를 여러 단계에 걸쳐 조립하는 데 집중해요. “불고기버거 주세요”처럼 자주 쓰는 조합은 공장 메서드가 빌더를 대신 불러 주게 만들 수도 있어요.
  • 퍼사드 패턴과 함께 쓸 수 있어요. 여러 객체를 조립해야 하는 복잡한 준비 과정을 퍼사드 뒤에 숨기고, 퍼사드 안에서 빌더로 조립하면 쓰는 쪽은 한 줄로 끝나요.
  • 데코레이터 패턴과 헷갈리지 마세요. 둘 다 “하나씩 더한다”처럼 보이지만, 빌더는 만드는 과정에서 값을 정하고 끝나면 완성된 객체 하나가 남아요. 데코레이터는 만든 뒤에 껍질을 겹겹이 씌워 기능을 더해요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.