하리Hari

2. 팩토리 패턴 (Factory)

객체 생성을 공장에 위임하여 결합도를 낮추는 팩토리 패턴을 알아봅니다.

한 줄 요약
“어떤 객체를 만들지” 고르는 일을 공장 한 곳에 모아 두고, 객체가 필요한 곳은 공장에 부탁만 하게 하는 패턴이에요.

문제: 여기저기 흩어진 “무엇을 만들지”

회원에게 알림을 보내는 프로그램을 만든다고 해 볼게요. 회원마다 알림 방식이 달라요.

  • 문자로 받는 회원
  • 메일로 받는 회원

알림을 보내는 곳은 여러 군데예요. 가입했을 때도, 주문이 끝났을 때도 보내요. 가장 먼저 떠오르는 방법은 알림을 보내는 곳마다 방식을 보고 알맞은 알림 객체를 만드는 거예요.

[알림]을 규정하자:
    <보내기>가 있어야 한다 ([문자열]인 '내용')

[알림]을 따르는 [문자알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[문자] {'내용'}"을 출력하자

[알림]을 따르는 [메일알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[메일] {'내용'}"을 출력하자

(참고: 알림을 보내는 곳마다 '어떤 알림을 만들지' 고르는 코드가 있어요)
<가입축하>를 만들자 ([문자열]인 '방식'):
    '알림'을 [알림]인 비어있음으로 정하자
    '방식'에 따라 나누자:
        "문자" 인 경우:
            '알림'을 새로운 [문자알림]()으로 정하자
        "메일" 인 경우:
            '알림'을 새로운 [메일알림]()으로 정하자
    '알림'의 <보내기>("가입을 환영해요!")를 실행하자

<주문완료>를 만들자 ([문자열]인 '방식'):
    '알림'을 [알림]인 비어있음으로 정하자
    '방식'에 따라 나누자:
        "문자" 인 경우:
            '알림'을 새로운 [문자알림]()으로 정하자
        "메일" 인 경우:
            '알림'을 새로운 [메일알림]()으로 정하자
    '알림'의 <보내기>("주문이 완료됐어요.")를 실행하자

<가입축하>("문자")를 실행하자
<주문완료>("메일")을 실행하자

실행 결과
[문자] 가입을 환영해요!
[메일] 주문이 완료됐어요.

잘 돌아가요. 그런데 알림을 보내는 곳이 늘어나면 아파져요.

  • 같은 고르기가 복사돼요. <가입축하>와 <주문완료>의 앞부분이 똑같아요. 비밀번호 변경, 배송 시작… 알림을 보내는 곳이 열 군데면 이 코드도 열 벌이에요.
  • 새 방식을 넣으려면 모든 곳을 고쳐야 해요. “앱 알림”이 생기면 열 군데 모두에 "앱" 인 경우:를 넣어야 하고, 한 곳만 빠뜨려도 그곳에서는 앱 알림이 안 가요.
  • 보내는 곳이 너무 많이 알아요. <가입축하>는 “환영 인사를 보낸다”는 일만 알면 되는데, [문자알림], [메일알림]이라는 클래스 이름과 만드는 방법까지 다 알고 있어요.
  • 잘못된 방식이 엉뚱한 곳에서 터져요. "문짜"처럼 오타를 내면 아무 경우에도 맞지 않아 '알림'이 비어있음으로 남고, 한참 뒤 <보내기>를 부를 때에야 에러가 나요. 에러 문구만 봐서는 오타 때문인지 알기 어려워요.
[알림]을 규정하자:
    <보내기>가 있어야 한다 ([문자열]인 '내용')

[알림]을 따르는 [문자알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[문자] {'내용'}"을 출력하자

<가입축하>를 만들자 ([문자열]인 '방식'):
    '알림'을 [알림]인 비어있음으로 정하자
    '방식'에 따라 나누자:
        "문자" 인 경우:
            '알림'을 새로운 [문자알림]()으로 정하자
    '알림'의 <보내기>("가입을 환영해요!")를 실행하자

<가입축하>("문짜")를 실행하자

실행하면 “비어있음 타입의 값에는 멤버로 접근할 수 없어요”라는 에러가 나요. 오타가 원인인데, 에러는 그 다음 줄에서 엉뚱한 이야기를 해요.

해결책: 만드는 일을 공장에 맡기기

팩토리 패턴은 “무엇을 만들지 고르는 코드”를 한곳으로 옮겨요.

  1. 만들어질 객체들이 지킬 약속, 곧 규정을 만들어요. 여기서는 이미 있는 [알림]이에요.
  2. 공장을 만들고, 방식을 받아 알맞은 객체를 돌려주는 메서드를 하나 둬요. 고르는 코드는 이곳에만 있어요.
  3. 객체가 필요한 곳은 공장에 “문자 알림 하나 주세요”라고 부탁만 해요. 무엇이 만들어졌는지는 몰라도, 약속([알림])대로 <보내기>를 할 수 있다는 것만 알면 돼요.
  4. 모르는 방식이 들어오면 공장이 그 자리에서 알아듣기 쉬운 에러를 내요.

비유: 카페 카운터

카페에서 손님은 “아이스 아메리카노 주세요”라고만 말해요. 원두를 얼마나 갈고, 얼음을 언제 넣는지는 몰라도 돼요. 새 메뉴가 생겨도 손님이 주문하는 방법은 그대로이고, 바뀌는 건 카운터 뒤뿐이에요. 없는 메뉴를 말하면 카운터에서 바로 “그런 메뉴는 없어요”라고 알려 주고요. 카운터가 바로 “공장”이에요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
제품 약속 [알림] (규정) 만들어질 객체가 할 줄 알아야 하는 일을 정해요
구체적인 제품 [문자알림], [메일알림] 실제로 일을 해요
공장 [알림공장]의 <만들기> 방식을 보고 알맞은 제품을 만들어 돌려줘요
쓰는 곳 <가입축하>, <주문완료> 공장에 부탁하고, 받은 제품을 약속대로 써요

하리로 만들기

같은 프로그램을 팩토리 패턴으로 다시 만들어 볼게요. 공장의 <만들기>는 객체 없이 부를 수 있도록 '우리'의를 붙인 정적 메서드로 만들어요.

[알림]을 규정하자:
    <보내기>가 있어야 한다 ([문자열]인 '내용')

[알림]을 따르는 [문자알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[문자] {'내용'}"을 출력하자

[알림]을 따르는 [메일알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[메일] {'내용'}"을 출력하자

(참고: 어떤 알림을 만들지는 이 공장 한 곳에서만 정해요)
[알림공장]을 설계하자:
    [알림]을 돌려주는 '우리'의 <만들기>를 만들자 ([문자열]인 '방식'):
        '방식'에 따라 나누자:
            "문자" 인 경우:
                새로운 [문자알림]()을 돌려주자
            "메일" 인 경우:
                새로운 [메일알림]()을 돌려주자
        새로운 [오류](틀"모르는 알림 방식이에요: {'방식'}")를 발생시키자

(참고: 알림을 보내는 곳은 공장에 부탁하기만 해요)
<가입축하>를 만들자 ([문자열]인 '방식'):
    [알림공장]의 <만들기>('방식')의 <보내기>("가입을 환영해요!")를 실행하자

<주문완료>를 만들자 ([문자열]인 '방식'):
    [알림공장]의 <만들기>('방식')의 <보내기>("주문이 완료됐어요.")를 실행하자

<가입축하>("문자")를 실행하자
<주문완료>("메일")을 실행하자

일단 해보자:
    <주문완료>("문짜")를 실행하자
오류가 발생했다면 ('에러'):
    ('에러'의 '메시지')를 출력하자

실행 결과
[문자] 가입을 환영해요!
[메일] 주문이 완료됐어요.
모르는 알림 방식이에요: 문짜

앞의 두 줄은 리팩터링 전과 같아요. 마지막 줄을 보세요. 이번에는 오타가 들어온 바로 그 자리(공장)에서 무엇이 잘못됐는지 말해 줘요.

하나씩 다시 보기

  • 고르는 곳은 한 군데. 따라 나누자가 [알림공장]의 <만들기>에만 있어요. 알림을 보내는 곳이 열 군데로 늘어도 고르는 코드는 하나예요.
  • 보내는 곳은 한 줄. <가입축하>는 [문자알림]이나 [메일알림]이라는 이름을 전혀 몰라요. 공장에서 [알림]을 하나 받아서 <보내기>를 할 뿐이에요.
  • 돌려주는 타입은 약속으로. [알림]을 돌려주는이라고 적었기 때문에, 공장이 약속을 따르지 않는 것을 돌려주면 하리가 알려 줘요. 받는 쪽은 안심하고 <보내기>를 부를 수 있어요.
  • 모르는 방식은 공장이 막아요. 어느 경우에도 맞지 않으면 마지막 줄의 발생시키자가 실행돼요. 비어있음이 프로그램 안으로 새어 나가지 않아요.

새 방식을 더해 보면

“앱 알림”이 생겼다고 해 볼게요.

  • 앞의 방식: 알림을 보내는 모든 곳을 찾아서 "앱" 인 경우:를 넣어야 해요.
  • 팩토리 패턴: [알림]을 따르는 [앱알림]을 만들고, [알림공장]에 "앱" 인 경우: 하나만 더하면 돼요. <가입축하>, <주문완료>는 그대로예요.

직접 해 보기
위 코드에 [앱알림]을 더해 보세요. <보내기>는 [앱] 가입을 환영해요!처럼 출력해요. 그리고 [알림공장]에 "앱"을 더한 뒤 <가입축하>("앱")을 실행해 보세요. <가입축하>와 <주문완료>는 한 글자도 고칠 필요가 없어요.

한 걸음 더: 팩토리 메서드

공장을 따로 두는 대신, 자식 클래스가 무엇을 만들지 정하게 하는 방법도 있어요. 부모는 “알림을 하나 만들어서 보낸다”는 순서만 정해 두고, 어떤 알림을 만들지는 있어야 한다로 비워 두는 거예요. 이걸 팩토리 메서드라고 불러요. 부모를 밑설계하자(추상 클래스)로 만들면 돼요.

[알림]을 규정하자:
    <보내기>가 있어야 한다 ([문자열]인 '내용')

[알림]을 따르는 [문자알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[문자] {'내용'}"을 출력하자

[알림]을 따르는 [메일알림]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '내용'):
        틀"[메일] {'내용'}"을 출력하자

(참고: 순서는 부모가 정하고, 어떤 알림을 만들지는 자식이 정해요)
[안내창구]를 밑설계하자:
    <알림만들기>가 있어야 한다 ()

    <안내하기>를 만들자 ([문자열]인 '내용'):
        '알림'을 [알림]인 '나'의 <알림만들기>()로 정하자
        "[안내 시작]"을 출력하자
        '알림'의 <보내기>('내용')를 실행하자

[안내창구]를 바탕으로 [문자창구]를 설계하자:
    [알림]을 돌려주는 <알림만들기>를 만들자 ():
        새로운 [문자알림]()을 돌려주자

[안내창구]를 바탕으로 [메일창구]를 설계하자:
    [알림]을 돌려주는 <알림만들기>를 만들자 ():
        새로운 [메일알림]()을 돌려주자

'창구들'을 [새로운 [문자창구](), 새로운 [메일창구]()]로 정하자
'창구들'의 '창구'마다 반복하자:
    '창구'의 <안내하기>("내일은 휴무예요.")를 실행하자

실행 결과
[안내 시작]
[문자] 내일은 휴무예요.
[안내 시작]
[메일] 내일은 휴무예요.

<안내하기>는 부모에 한 번만 있고, 자식은 <알림만들기> 하나만 채워요. 새 창구가 생기면 [안내창구]를 바탕으로 클래스를 하나 더 만들면 돼요.

어느 쪽을 쓸까요?
무엇을 만들지가 실행 중에 들어오는 값(사용자 설정, 입력)으로 정해진다면 앞의 공장 클래스가 편해요. 무엇을 만들지가 어떤 종류의 객체인가에 따라 정해진다면 팩토리 메서드가 자연스러워요.

언제 쓰면 좋을까요?

  • “무엇을 만들지” 고르는 코드가 여러 곳에 복사되기 시작할 때
  • 객체를 쓰는 쪽이 구체적인 클래스 이름을 몰라도 되게 하고 싶을 때
  • 새 종류가 자주 늘어날 것 같을 때
  • 잘못된 종류를 만드는 순간 막고 싶을 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
고르는 코드가 한곳에 모여 고치기 쉬워요 클래스(공장)가 하나 더 생겨요
쓰는 쪽이 구체적인 클래스를 몰라도 돼요 만드는 곳이 한 군데뿐이라면 오히려 돌아가는 길이에요
새 종류를 더해도 쓰는 쪽은 그대로예요 공장 안의 따라 나누자는 종류가 늘 때마다 길어져요
잘못된 종류를 만드는 자리에서 막을 수 있어요

너무 이른 패턴은 독이에요.
객체를 만드는 곳이 한 군데뿐이고 종류도 늘 일이 없다면, 그 자리에서 새로운 [문자알림]()을 쓰는 게 가장 읽기 쉬워요. 같은 고르기를 두 번째로 복사하려는 순간이 공장을 만들 때예요.

다른 패턴과의 관계

  • 전략 패턴과 잘 어울려요. “VIP면 비율 할인, 쿠폰이 있으면 쿠폰 할인”처럼 어떤 전략을 끼울지를 공장이 정하게 하면, 고르는 코드가 한곳에 모여요.
  • 빌더 패턴과 비교해 보세요. 팩토리는 어떤 종류를 만들지 고르는 데 집중하고, 빌더는 하나를 여러 단계에 걸쳐 조립하는 데 집중해요.
  • 싱글톤 패턴과 함께 쓸 수 있어요. 이 페이지의 공장처럼 '우리'의 정적 메서드만 있는 공장은 객체를 만들 필요조차 없지만, 공장이 설정 같은 값을 기억해야 한다면 하나만 만들어 함께 쓰면 돼요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.