포스트

(생성패턴) Factory Pattern - 슬쩍 팩토리에서 처리하기 (메시징 서비스를 예제로)

(생성패턴) Factory Pattern - 슬쩍 팩토리에서 처리하기 (메시징 서비스를 예제로)

이전 글에서 디자인 패턴의 필요성과 중요성에 대해 이야기한 바 있습니다.

이번 글에서는 실제 팩토리 패턴(Factory Pattern)을 메시징 서비스에 어떻게 적용할 수 있는지를 소개하려 합니다.


서비스가 커지면 반드시 생기는 변화

당신은 메시징 플랫폼의 개발자입니다. 처음 요구사항은 이랬습니다:

1
2
3
- 문자 발송 (SMS, LMS, MMS)
- 이메일 발송 (EMAIL)
- 카카오 알림톡 (대한민국)

그래서 이렇게 인터페이스를 만들었죠:

1
2
3
4
5
6
7
public interface MessageService {
    SendResult sendSms();
    SendResult sendLms();
    SendResult sendMms();
    SendResult sendEmail();
    SendResult sendKakao();
}

하지만 어느 날, 갑자기 기획자가 말합니다:

“로재님! 해외 진출 때문에 WeChat, Zalo 같은 글로벌 채널도 빨리 붙여야 해요!”

결국 요구사항은 다음처럼 바뀌게 됩니다:

1
2
3
- WeChat (중국)
- Zalo (베트남)
- ... 기타 글로벌 채널 추가

팩토리 패턴 도입 – 객체 생성을 위임하자

기존의 MessageService 방식은 채널이 늘어날 때마다 코드 수정이 필요합니다.
이를 해결하기 위해 우리는 팩토리 패턴을 도입합니다.

팩토리는 다음을 해줍니다:

  • 구현체 생성 책임을 위임
  • 클라이언트는 타입만 넘기면 적절한 구현체를 받음
  • 코드 변경 없이도 새로운 채널 추가 가능

설계 다이어그램

classDiagram
    class SenderChannelHandler {
        <<interface>>
        +send()
    }

    class EmailSenderChannelHandler
    class SmsSenderChannelHandler
    class KakaoSenderChannelHandler

    class MessageSenderFactory {
        +getHandler(channelType): SenderChannelHandler
    }

    SenderChannelHandler <|.. SmsSenderChannelHandler
    SenderChannelHandler <|.. EmailSenderChannelHandler
    SenderChannelHandler <|.. KakaoSenderChannelHandler

    MessageSenderFactory --> SenderChannelHandler : returns

NotifyChannelType – 채널을 enum으로 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
public enum NotifyChannelType {
    SMS("SMS", "SMS 문자"),
    LMS("LMS", "LMS 문자"),
    MMS("MMS", "MMS 문자"),
    EMAIL("EMAIL", "이메일"),
    KAKAO("KAKAO", "카카오"),
    WECHAT("WECHAT", "위챗"),
    ZALO("ZALO", "잘로");

    private final String code;
    private final String description;

    NotifyChannelType(String code, String description) {
        this.code = code;
        this.description = description;
    }
}

SenderChannelFactory – 전략 반환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component
public class SenderChannelFactory {
    private final Map<NotifyChannelType, SenderChannelHandler> handlerMap = new EnumMap<>(NotifyChannelType.class);

    public SenderChannelFactory(
        SmsSenderChannelHandler smsHandler,
        LmsSenderChannelHandler lmsHandler,
        MmsSenderChannelHandler mmsHandler,
        EmailSenderChannelHandler emailHandler,
        KakaoSenderChannelHandler kakaoHandler,
        WechatSenderChannelHandler webchatHandler,
        ZaloSenderChannelHandler zaloHandler
    ) {
        handlerMap.put(NotifyChannelType.SMS, smsHandler);
        handlerMap.put(NotifyChannelType.LMS, lmsHandler);
        handlerMap.put(NotifyChannelType.MMS, mmsHandler);
        handlerMap.put(NotifyChannelType.EMAIL, emailHandler);
        handlerMap.put(NotifyChannelType.KAKAO, kakaoHandler);
        handlerMap.put(NotifyChannelType.WECHAT, webchatHandler);
        handlerMap.put(NotifyChannelType.ZALO, zaloHandler);
    }

    public SenderChannelHandler getChannel(NotifyChannelType type) {
        SenderChannelHandler handler = handlerMap.get(type);
        if (handler == null) {
            throw new IllegalArgumentException("지원하지 않는 채널 타입입니다: " + type);
        }
        return handler;
    }
}

SenderChannelHandler – 공통 인터페이스

1
2
3
4
public interface SenderChannelHandler {
    NotifyChannelType getType();
    ResponseEntity<SendResponse> send(SendRequest request);
}

SmsSenderChannelHandler – 구현 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@RequiredArgsConstructor
public class SmsSenderChannelHandler implements SenderChannelHandler {

    private final NotifySenderClient notifySenderClient;

    @Override
    public NotifyChannelType getType() {
        return NotifyChannelType.SMS;
    }

    @Override
    public ResponseEntity<SendResponse> send(SendRequest request) {
        return notifySenderClient.sendSms(
            new SmsSendRequest(request.getPhoneNumber(), request.getTitle(), request.getContents())
        );
    }
}

📌 Mms, Lms, Email, Kakao, Zalo 등은 유사한 방식으로 구현하시면 됩니다.


호출 예시

  • 클라이언트는 알림타입만을 통해서 내부 구현체 (Factory > Handler)를 받아서 처리합니다.

  • 각각의 채널 구현체(Handler)는 별도로 존재하므로, 확장하더라도 기존 부분을 수정할 필요가 없습니다.

1
2
SenderChannelHandler channel = senderChannelFactory.getChannel(request.getNotifyChannelType());
ResponseEntity<SendResponse> response = channel.send(form);

한번 더 확장하자 – 전략 패턴과의 융합

팩토리 패턴을 통해 우리는 채널 추가에 유연하게 대응할 수 있게 되었습니다.

  • 확장성과 유지보수성을 높이고
  • 비즈니스 로직과 기술 구현을 분리하며
  • 팀 협업에 유연성을 제공합니다.

하지만 실무에서는 메시지 발송이 끝이 아닙니다.
결과 저장, 실패 재시도, 로그 기록 등 후처리가 필요합니다. (ex. outbox pattern)

이를 위해 다음 글에서는 전략 패턴(Strategy Pattern) 을 통해 CompositeSenderService 구조를 설계하고
발송 후처리(SyncStrategyService)까지 확장해보겠습니다.


CompositeSenderService (Factory Pattern + Strategy Pattern)

classDiagram
    class CompositeSenderService {
        <<interface>>
        +send()
    }

    class SenderChannelFactory {
        +getHandler(channelType): SenderChannelHandler
    }

    class SenderChannelHandler {
        <<interface>>
        +send()
    }

    class SmsSenderChannelHandler
    class KakaoSenderChannelHandler

    class SyncStrategyService {
        <<interface>>
        +sync()
    }

    class SyncStrategyHandler {
        <<interface>>
        +sync()
    }

    CompositeSenderService --> SenderChannelFactory
    CompositeSenderService --> SyncStrategyService

    SenderChannelFactory --> SenderChannelHandler
    SenderChannelHandler <|.. SmsSenderChannelHandler
    SenderChannelHandler <|.. EmailSenderChannelHandler
    SenderChannelHandler <|.. KakaoSenderChannelHandler

    SyncStrategyService <|.. SyncStrategyHandler
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.