_

Always be tactful

Only for Tech/insight

비즈니스 로직이니까 서비스에 있어야지!

funczun 2025. 4. 9. 04:09

비즈니스 로직, 무조건 서비스에 두어야 할까?

도메인 주도 설계(DDD)를 공부하다 보면 흔히 맞닥뜨리는 질문이 있다.

"비즈니스 로직이면 원래 서비스 레이어에 두는 거 아닌가?"

 

이 질문은 언뜻 맞는 말처럼 들리지만, 사실 절반만 맞는 말이다. 이 글에서는 비즈니스 로직의 위치에 대해 하나씩 짚어보며, 헷갈릴 수 있는 지점들을 정리해본다.


서비스에 로직을 두는 게 맞는 경우

다음과 같은 경우에는 비즈니스 로직을 서비스 레이어에 두는 것이 자연스럽다.

  • 여러 도메인 객체 간의 상호작용이 필요한 경우
  • 외부 시스템과의 연동이 포함되는 경우 (예: 메일 전송, 결제 API 호출 등)
  • 도메인 객체 내부에 두기엔 과도하거나 부적절한 경우

예를 들어, "예약을 저장하고, 알림도 보내고, 결제도 트리거하는 흐름"이라면 이는 하나의 도메인 객체가 책임지기 어렵다. 이런 흐름은 서비스가 orchestrator(조정자) 역할을 하며 담당하는 것이 적절하다.


도메인 핵심 규칙은 객체가 책임져야 한다

중요한 점은, 도메인의 핵심 규칙(비즈니스 룰) 은 도메인 객체 또는 도메인 서비스에 있어야 한다는 것이다.

 

왜냐하면 도메인 객체는 자신의 상태를 일관되게 유지할 책임이 있으며, 핵심 규칙이 서비스 레이어에 흩어져 있으면 결국 절차지향적인 코드처럼 되어버린다. 이는 유지보수가 어려워지고, 객체지향 설계의 이점을 살리기 어렵게 만든다.

로직 종류 위치
도메인 규칙 (상태 변화 등) 도메인 엔티티 or 밸류 오브젝트
여러 객체 조합, 도메인 내부 흐름 도메인 서비스
외부 시스템 연동, 앱 수준 흐름 제어 애플리케이션 서비스

[Q1] 아래 Fee 계산 로직은 어디에 둬야 할까?

다음은 실제 프로젝트에서 있었던 고민이다.

private long calculateFee(String reason) {
    ReservationReason reservationReason = ReservationReason.from(reason);
    FeePolicy feePolicy = feePolicyResolver.getPolicy(reservationReason);
    return feePolicy.calculateFee();
}
  • 진료 목적에 따라 진료비를 계산하는 행위 → 이는 도메인의 핵심 규칙이다. 따라서 FeePolicy 도메인 모델이 책임져야 한다.
  • 어떤 정책을 선택할지 결정하는 로직 → 여러 정책 중 하나를 선택하는 행위는 도메인 객체의 책임으로 보기 어렵다. 이는 도메인 서비스인 FeePolicyResolver가 담당하는 것이 적절하다.

즉, 둘 다 도메인 계층에 속하지만 역할은 명확히 구분되어야 한다.


[Q2] FeePolicyResolver는 도메인 모델일까?

많은 사람들이 "서비스니까 도메인 모델은 아니지 않나?"라고 생각하기 쉽지만, DDD에서 도메인 모델은 다음 세 가지 구성요소로 나뉜다.

  • 엔티티 (예: Reservation)
  • 밸류 오브젝트 (예: ReservationReason)
  • 도메인 서비스 (예: FeePolicyResolver)

FeePolicyResolver는 특정 도메인 객체에 귀속되지 않으며, 상태 없이 로직만을 제공한다. 또한 핵심적인 비즈니스 룰을 조율한다. 따라서 이는 전형적인 도메인 서비스의 조건을 충족하며, 도메인 모델로 분류된다.


비즈니스 로직 ≠ 무조건 서비스

비즈니스 로직이라 해서 모두 서비스에 둬야 하는 것은 아니다. 오히려 도메인의 핵심 규칙이라면 도메인 객체가 책임지는 것이 DDD의 기본 원칙이다.

  • 핵심 규칙과 상태 변화 → 도메인 객체
  • 복수 객체 간의 협력 로직 → 도메인 서비스
  • 외부 연동, 흐름 제어 → 애플리케이션 서비스