Domain Service를 사용하게 된 이유

2025. 5. 7. 22:16·Architecture

1. 배경

우아한 테크코스에서 예약 관리 미션을 진행중이고, 개발자 관점에서 예약 생성 API를 구현하는 과정의 일이다.

public ReservationServiceResponse create(CreateReservationServiceRequest request) {
    ReservationTime reservationTime = getReservationTimeById(request.timeId());
    LocalDateTime requestedDateTime = LocalDateTime.of(request.date(), reservationTime.startAt());
    if (requestedDateTime.isBefore(LocalDateTime.now())) {
        throw new IllegalArgumentException("이미 지나간 시간으로 예약할 수 없습니다.");
    }
    if (reservationRepository.existDuplicatedDateTime(request.date(), request.timeId(), request.themeId())) {
        throw new IllegalArgumentException("이미 예약된 시간입니다.");
    }
    ReservationTheme reservationTheme = reservationThemeRepository.getById(request.themeId());
    Reservation reservation = request.toReservation(reservationTime, reservationTheme);
    Reservation savedReservation = reservationRepository.save(reservation);
    return ReservationServiceResponse.from(savedReservation);
}

위 메서드는 서비스 계층의 클래스 중 하나의 메서드이다.

이 메서드에는 현재 도메인 로직이 두 개 포함되어있는데, 첫 번째로 지나간 시간 검증 두 번째로 예약 중복 검증이다. 나는 이 두 개의 도메인 로직을 해당 메서드로부터 분리하고자 하였다.

 

분리하고자 하는 이유는 나는 서비스 계층의 역할은 애플리케이션 로직과 도메인 로직의 순서를 제어하는 것이라 생각했다. 트랜잭션을 롤백시키기 위한 것이나 외부 서비스와 연동 실패 등 애플리케이션의 흐름을 제어하기 위한 예외를 발생시킬 수 있지만, 도메인과 직접적으로 연관된 요구사항을 구현하기 위한 예외를 발생시키는 것은 올바르지 않다고 생각했다.

 

첫 번째 지나간 시간 검증은 기존에 존재하던 Reservation 객체에 책임을 옮길 수 있었다.

아래와 같이 구현할 수 있었다. 미래의 시간만 예약하겠다는 createFutureReservation 를 만들어 해결했다.

public static Reservation createFutureReservation(ReservationDetails details) {
    LocalDateTime requestedDateTime = LocalDateTime.of(details.date(), details.reservationTime().getStartAt());
    validateFutureTime(requestedDateTime);
    return new Reservation(details.name(), details.date(), details.reservationTime(), details.reservationTheme());
}

private static void validateFutureTime(LocalDateTime requestedDateTime) {
    if (requestedDateTime.isBefore(LocalDateTime.now())) {
        throw new InvalidReservationTimeException("예약시간이 과거시간이 될 수 없습니다. 미래시간으로 입력해주세요.");
    }
}

 

두 번째 예약 중복 검증은 앞의 경우와는 달랐다. 이전 지나간 시간을 검증하는 것은 자바 라이브러리만 사용하여 구현한 순수 로직이었기 때문에 도메인 객체 내부에 넣어도 괜찮았다.

 

하지만 예약 중복 검증은 기존에 저장된 예약 엔터티들과 비교해야하기에 reservationRepository 를 활용해야 했다. 나의 프로젝트에서 reservationRepository 는 인터페이스고, 도메인 객체의 저장소를 행위를 정의했기에 도메인 패키지 내부에 포함되어 있다. 도메인 객체라 하더라도 reservationRepository 과 reservation 의 저장소이므로 reservationRepository가 reservation 을 의존하고 있는데, 도메인 검증 로직을 포함하겠다고 reservation 가 reservationRepository 를 의존하면 양방향 의존성이 생기게 되어 문제가 된다.

 

이를 해결하기 위해 나는 도메인 서비스를 생성하게 되었다.

2. Domain Service

기존 사용하던 서비스 계층을 애플리케이션 서비스라고 부르겠다!

 

도메인 서비스는 도메인 객체지만 애플리케이션 서비스처럼 상태가 없는(stateless) 클래스로, Entity와 VO 위에서 동작한다. 여기서 애플리케이션 서비스와 가장 큰 차이점은 도메인 로직을 가지고 있는지이다. 도메인 서비스는 도메인 로직을 구현하고 있지만, 애플리케이션은 단지 호출과 흐름 제어만 맡고 있다.

 

즉, 이것을 쉽게 말하면 도메인 서비스는 비즈니스를 결정하고, 애플리케이션 서비스는 비즈니스를 지휘한다고 할 수 있다.

언제 사용해야 할까?

블라디미르 호리코프는 도메인 서비스를 다음과 같이 사용해야한다고 하였다.

  1. 여러 객체 간의 조정이 필요한 경우
    : 여러 엔티티나 값 객체 사이의 상호작용을 조정해야 하는 로직
  2. 객체의 격리를 유지해야 할 경우
    : 엔티티나 값 객체의 캡슐화와 격리를 깨뜨리지 않고 로직을 구현해야 하는 경우
  3. 외부 시스템(예: 결제 게이트웨이, 데이터베이스)과의 협력이 필요한 경우
    : 불순함(외부 시스템이나 인프라와의 의존성)을 포함한 도메인 로직일 때

위의 규칙들이 완벽히 이해가 되지 않지만, 도메인 순수성을 위해 서로 다른 도메인간 격리를 유지하는 경우와 외부 요소와의 협력이 필요한 복잡한 도메인 로직을 처리할 때 도메인 서비스를 사용해야 하는 것 같다.

 

나의 경우는 3번에 해당하는 것과 같고, 실제로 블라디미르 호리코프 글에는 아래와 같은 말이 있다.

For example, we don’t make it work with the repository as it is not required to make the business decision.

 

그래서 나는 ReservationValidator 라는 도메인 서비스를 생성하여 도메인 패키지 내부에 존재시켰다.

@Component
@RequiredArgsConstructor
public class ReservationValidator {

    private final ReservationRepository reservationRepository;

    public void validateNoDuplication(LocalDate date, Long timeId, Long themeId) {
        if (reservationRepository.existDuplicatedDateTime(date, timeId, themeId)) {
            throw new InvalidReservationTimeException("이미 예약된 시간입니다. 다른 시간을 예약해주세요.");
        }
    }
}

 

아래는 최종적으로 리팩토링 하기 전/후의 코드이다.

도메인 로직이 제거되고, 도메인 로직 호출과 애플리케이션 로직 호출의 흐름을 제어하는 역할만 담당하게 되었다.

//before
public ReservationServiceResponse create(CreateReservationServiceRequest request) {
    ReservationTime reservationTime = getReservationTimeById(request.timeId());
    LocalDateTime requestedDateTime = LocalDateTime.of(request.date(), reservationTime.startAt());
    if (requestedDateTime.isBefore(LocalDateTime.now())) {
        throw new IllegalArgumentException("이미 지나간 시간으로 예약할 수 없습니다.");
    }
    if (reservationRepository.existDuplicatedDateTime(request.date(), request.timeId(), request.themeId())) {
        throw new IllegalArgumentException("이미 예약된 시간입니다.");
    }
    ReservationTheme reservationTheme = reservationThemeRepository.getById(request.themeId());
    Reservation reservation = request.toReservation(reservationTime, reservationTheme);
    Reservation savedReservation = reservationRepository.save(reservation);
    return ReservationServiceResponse.from(savedReservation);
}

//after
public ReservationServiceResponse create(CreateReservationServiceRequest request) {
    ReservationDetails reservationDetails = createReservationDetails(request);
    reservationValidator.validateNoDuplication(request.date(), request.timeId(), request.themeId());
    Reservation reservation = Reservation.createFutureReservation(reservationDetails);
    Reservation savedReservation = reservationRepository.save(reservation);
    return ReservationServiceResponse.from(savedReservation);
}

3. 마무리

도메인 서비스는 DDD에 나온 개념이다. 나는 아직 DDD를 공부해도 공감할 수 없을 것이라는 말을 많이 들어서 책을 읽는 것을 꺼려하고 있다. 하지만, 객체지향적인 코드를 작성하기 위해 노력하여 도메인 중심 사고를 진행하다 보면 DDD에 정답이 써있는 것 같다.

 

도메인 서비스를 잘 사용했다고 자신있게 말할 수 없지만, 도메인 서비스의 역할로 도메인 로직을 애플리케이션 서비스로부터 분리하였다는 것과 도메인 객체 내부에 포함할 수 없는 도메인 로직을 책임졌다는 것에 충분히 유의미하다고 판단하였다.

 

[Reference]

https://enterprisecraftsmanship.com/posts/domain-vs-application-services/

'Architecture' 카테고리의 다른 글

REST(Representational State Transfer)ful 하다는것?  (3) 2025.04.28
'Architecture' 카테고리의 다른 글
  • REST(Representational State Transfer)ful 하다는것?
고선제
고선제
  • 고선제
    개발 로그
    고선제
  • 전체
    오늘
    어제
    • 분류 전체보기
      • JAVA
      • Spring
      • DB
      • 네트워크
      • JPA
      • Infra
      • Architecture
      • 여러가지
      • 우아한 테크코스
      • Test
      • 프로젝트
        • 띵동
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 관리
    • 글쓰기
  • 링크

  • 공지사항

    • 이 블로그의 시작을 알립니다.
  • 인기 글

  • 태그

    성능
    TEST
    application service
    restassured
    MediaConvert
    async dispatch
    enum
    Full GC
    restcontrollleradvice
    http
    NGINX
    SSE
    회고
    우아한 테크코스
    객체 설계
    출석미션
    controlleradvicebean
    우테코
    우아콘2025
    동영상 실시간 알람
    동영상 스트리밍 설계
    실시간 알람
    우테고
    스프링 컨텍스트
    비연결성
    피드줍줍
    @dirtycontext
    운영 서버
    쿼리로 성능 개선
    예약미션
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
고선제
Domain Service를 사용하게 된 이유
상단으로

티스토리툴바