Post

서비스 구현 템플릿

주문·결제·레스토랑 서비스의 책임 경계와 포트/어댑터 구조를 템플릿화해 복제 가능한 MSA 구현 패턴 제시

서비스 구현 템플릿

이 글의 결론: 세 서비스는 기능이 아니라 구조적 역할이 다르고, 그 차이가 재사용 가능한 템플릿을 만든다.

이 강의의 주문/결제/레스토랑 서비스는 “무엇을 한다”보다 어디까지 책임지고 어디서 멈추는가가 다르다. 이 차이를 코드 구조로 고정하면, 다른 도메인에서도 그대로 복제 가능해진다.


서비스 책임과 도메인 경계 요약 결론: 각 서비스는 “결정의 종류”가 다르다.

주문 서비스(Order)

  • 책임: 주문 수명주기의 중앙 상태 관리자
  • 도메인 경계: 주문 생성/상태 전이의 단일 소스
  • 특징: SAGA 오케스트레이션의 중심이 되기 쉬움

결제 서비스(Payment)

  • 책임: 금전적 판단(승인/거절)의 사실 판정자
  • 도메인 경계: 결제 시도와 결과의 불변 기록
  • 특징: 외부 시스템 의존이 많고, 결과를 이벤트로만 공유

레스토랑 서비스(Restaurant)

  • 책임: 가게 정책에 따른 수락/거절 결정
  • 도메인 경계: 재고/영업/조리 가능성 같은 로컬 규칙
  • 특징: 주문의 “의미”는 모르고, 자기 정책만 본다

공통 모듈 구조 결론: 분리는 통일성을 위한 것이지, 복잡도를 늘리기 위한 게 아니다.

공통 구조

1
2
3
4
5
6
7
8
domain
application
  └─ port (in / out)
adapter
  ├─ web
  ├─ messaging
  └─ persistence
container (spring config)

왜 이렇게 나누는가

  • domain: 비즈니스 규칙의 최종 위치
  • application: 유스케이스 흐름과 트랜잭션
  • adapter: 기술 교체 가능성 확보(JPA/Kafka/REST)
  • container: 조립만 담당(의존성 방향 고정)

이 구조의 목적은 “서비스마다 같은 질문을 안 하게 만드는 것”이다.


Port 설계 규칙 결론: 포트는 기술이 아니라 의사결정의 입·출구다.

입력 포트(Input Port)

  • UseCase 인터페이스
  • “이 서비스가 외부에서 무엇을 할 수 있는가

출력 포트(Output Port)

  • Repository / Event Publisher
  • “이 서비스가 외부에 무엇을 요구하는가

DTO / Mapper 위치가 중요한 이유

  • DTO는 어댑터 전용
  • 도메인 ↔ DTO 직접 매핑 ❌
  • Mapper를 어댑터에 두는 이유:

    • 도메인이 외부 표현 형식을 모르게 하기 위함

예외/에러 처리 결론: 에러는 종류가 다르고, 섞는 순간 구조가 무너진다.

컨트롤러 어드바이스

  • 역할:

    • 기술 예외 → HTTP 응답 변환
    • 도메인 예외를 의미 있는 상태 코드로 매핑

도메인 예외 vs 기술 예외

  • 도메인 예외

    • 승인 불가, 정책 위반
    • 복구 불가, 의미 있음
  • 기술 예외

    • DB 타임아웃, 메시지 역직렬화 실패
    • 재시도/대체 가능

규칙: 도메인 계층에서 기술 예외를 던지지 않는다.


테스트 전략 결론: “유닛의 끝”과 “통합의 시작”을 명확히 그어야 한다.

  • 도메인

    • Entity / Domain Service
    • 순수 단위 테스트
  • 애플리케이션

    • UseCase + Port Mock
    • 흐름 검증
  • 어댑터

    • JPA/Kafka/Testcontainers
    • 통합 테스트 최소화

최소 예시 1 결론: 주문 생성 API는 “도메인 규칙 → 이벤트 의도”까지 한 번에 묶는다.

흐름

1
2
3
4
5
Controller
  → CreateOrderUseCase
    → Order.create()
    → OrderRepository.save()
    → EventPublisher.publish(OrderCreated)

핵심 포인트

  • Controller는 검증/변환만
  • 상태 변경은 Order Aggregate
  • 이벤트 발행은 출력 포트

최소 예시 2 결론: 결제 이벤트 소비는 “사실 판정”까지만 책임진다.

흐름

1
2
3
4
5
KafkaListener
  → ProcessPaymentUseCase
    → Payment.approve() / reject()
    → PaymentRepository.save()
    → EventPublisher.publish(PaymentApproved/Rejected)

핵심 포인트

  • 이벤트 리스너는 어댑터
  • 비즈니스 판단은 도메인
  • 다음 흐름 제어는 SAGA/주문 서비스 몫

서비스별 비교표 결론: 이 표가 템플릿이다.

서비스도메인 객체상태 전이입력 포트출력 포트발행 이벤트소비 이벤트
주문OrderPENDING→APPROVED/REJECTEDCreate/ChangeOrderOrderRepo, EventPubOrderCreated, OrderApprovedPayment, Restaurant
결제PaymentINIT→APPROVED/REJECTEDProcessPaymentPaymentRepo, EventPubPaymentApproved/RejectedOrderCreated
레스토랑RestaurantOrderNEW→ACCEPTED/REJECTEDDecideOrderRepo, EventPubRestaurantAccepted/RejectedOrderCreated/PaymentApproved
  • 설계에 따라 달라질 수 있음

흔한 오해 / 실수 3가지

  1. “서비스마다 구조가 달라야 한다”는 오해
    • 문제: 주문/결제/레스토랑을 각자 다른 패키지·레이어 규칙으로 구현
    • 결과: 신규 서비스 추가 시 설계 재논의 반복, 유지보수 비용 증가
    • 교정 기준: 책임은 다르지만 구조는 동일해야 한다. 차이는 도메인에만 둔다.
  2. Adapter에 비즈니스 판단을 두는 실수
    • 문제: Controller/Listener에서 승인·거절 판단 수행
    • 결과: 규칙 우회 경로 발생, 테스트 범위 확대
    • 교정 기준: Adapter는 변환·전달만, 판단은 Domain/UseCase로 이동.
  3. DTO를 도메인과 동일하게 쓰는 관행
    • 문제: 요청/이벤트 DTO가 그대로 도메인 객체로 사용
    • 결과: 외부 포맷 변경이 도메인 변경으로 전파
    • 교정 기준: DTO는 Adapter 전용, Mapper는 경계에만 둔다.

대표 실패 시나리오 3가지

  1. 주문 서비스가 모든 결정을 떠안는 경우
    • 상황: 결제/레스토랑 정책까지 주문 서비스에서 판단
    • 원인: 중앙 관리자 역할 오해
    • 결과: 주문 서비스 비대화, 변경 시 전체 영향
    • 대응: 주문은 상태 조율자, 판단은 각 도메인으로 환원
  2. 결제 이벤트 소비 로직이 중복 실행되는 경우
    • 상황: Kafka 재전달로 결제 승인 로직 2회 실행
    • 원인: 멱등 처리 부재
    • 결과: 중복 결제/이벤트 폭주
    • 대응: paymentAttemptId 기반 도메인 차단 + 이벤트 멱등
  3. 레스토랑 서비스가 주문 내부 상태를 가정하는 경우
    • 상황: “이미 결제됐겠지”라는 전제 하 로직 작성
    • 원인: 서비스 간 상태 공유 착각
    • 결과: 순서 역전 시 오류
    • 대응: 레스토랑은 자기 이벤트 입력만 신뢰

실무 체크리스트 (서비스 구조 복제 가능성 점검)

  1. 세 서비스가 동일한 패키지/레이어 규칙을 따르는가
  2. 각 서비스의 “결정 책임”을 한 문장으로 설명할 수 있는가
  3. 입력 포트가 기술 기준이 아니라 의사결정 기준인가
  4. 출력 포트가 실제 교체 가능성을 반영하는가
  5. Adapter에 비즈니스 if/else가 없는가
  6. DTO/Mapper가 Domain 밖에 고정돼 있는가
  7. 이벤트 리스너가 상태 변경을 직접 하지 않는가
  8. 중앙 상태 관리자가 명확히 한 곳인가
  9. 서비스 간 결합이 이벤트 계약으로만 이루어지는가
  10. 신규 서비스에 이 구조를 그대로 복제할 수 있는가
  11. 통합 테스트 범위가 Adapter에 한정돼 있는가
  12. “이 서비스는 무엇을 결정하지 않는가”를 말할 수 있는가

This post is licensed under CC BY 4.0 by the author.