[Server] 도메인 서비스(Domain Service)와 애플리케이션 서비스(Application Service)에 대한 고찰

2025. 12. 16. 22:17·Server

시작하며

헥사고날 아키텍처를 기반으로 프로젝트를 진행하며, 도메인 서비스와 애플리케이션 서비스의 역할 분리에 대해 고민했습니다.

이 글은 그 고민의 과정과 두 서비스를 분리하게 된 기준을 정리한 기록입니다.

 

개인적인 경험과 주관적인 견해가 포함되어 있어 정답이 아닙니다. 그저 저의 고민과 결론을 적어놓은 글입니다.

 

도메인 서비스 vs 애플리케이션 서비스

도메인 서비스(Domain Service)는 도메인 로직을 수행하지만, 특정 엔티티에 넣기에 애매한 로직을 다룰 때 사용됩니다.

반면, 애플리케이션 서비스(Application Service)는 도메인 영역을 바라보며 트랜잭션, 영속성 처리, 외부 통신 등을 담당하고 전체적인 흐름을 제어하는 역할을 합니다.

 

이 두 서비스의 가장 큰 차이점은 도메인 로직의 유무입니다.

  • 도메인 서비스: 도메인 로직(비즈니스 의사결정 규칙)을 포함함.
  • 애플리케이션 서비스: 도메인 로직을 직접 포함하지 않고, 도메인 객체들을 사용(위임)함.

제가 생각하는 도메인 서비스 분리 기준은 다음과 같습니다.

  1. 복잡한 비즈니스 로직: 여러 엔티티나 값 객체 사이의 상호작용을 조정해야 하는 경우.
  2. 캡슐화 유지: 특정 엔티티나 값 객체의 캡슐화를 깨뜨리지 않고 로직을 구현해야 하는 경우.

도메인 서비스 분리 기준

프로젝트를 진행하며 코드를 작성할 때, "어느 수준에서 도메인 서비스로 분리해야 하지?"라는 고민을 자주 했습니다.

특히, '애플리케이션 서비스에서 도메인 엔티티를 두 개 이상 사용한다면, 이것은 도메인 로직을 알고 있는 것이 아닐까? 도메인 서비스로 내려야 하나?'라는 고민이 있었습니다.

 

예시와 함께 살펴보겠습니다.

(실제 프로젝트 코드와 상이합니다. 예시로 만든 코드입니다. 정확한 로직이 들어있지 않습니다.)

class BettingParticipateService implements BettingParticipateUseCase {

    private final BettingRepository bettingRepository;
    private final PredictionRepository predictionRepository;
    private final UserRepository userRepository; // 추가 추정

    @Transactional
    public void participate(BetParticipateCommand command) {
        User user = userRepository.findById(command.getUserId())
                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
        Prediction prediction = predictionRepository.findById(command.getPredictionId())
                .orElseThrow(() -> new IllegalArgumentException("예측을 찾을 수 없습니다."));
     
        // 1. 포인트 차감
        user.deductPoints(command.getAmount());
        
        // 2. 베팅 생성
        Betting betting = Betting.create(user.id(), prediction.id(), command.getAmount());
        
        bettingRepository.save(betting);
        userRepository.save(user);
    }
}

 

participate 메서드는 유저가 예측 게임에 참여하는 API를 제공하는 애플리케이션 서비스의 일부입니다. 로직은 단순합니다.

유저의 포인트를 차감하고, 베팅 정보를 DB에 저장합니다.

 

여기서 User와 Betting이라는 두 개의 도메인 객체가 사용되었습니다.

"엔티티가 두 개 이상 사용됐으니 복잡한 비즈니스 로직인가? 도메인 서비스로 분리해야 할까?"

 

저는 user.deductPoints()와 Betting.create()를 호출하는 순서나 조합을 아는 것 자체가 도메인 지식이 아닌지 고민했습니다. 그래서 아래와 같이 도메인 서비스로 분리해 보았습니다.

 

// Application Service
public void participate(BetParticipateCommand command) {
    User user = userRepository.findById(command.getUserId())
            .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
    Prediction prediction = predictionRepository.findById(command.getPredictionId())
            .orElseThrow(() -> new IllegalArgumentException("예측을 찾을 수 없습니다."));
    
    // 도메인 서비스 호출
    Betting betting = bettingParticipateDomainService.bet(user, prediction, command);

    bettingRepository.save(betting);
}

// Domain Service
@Service
public class BettingParticipateDomainService {
    public Betting bet(User user, Prediction prediction, BetParticipateCommand command) {
        user.deductPoints(command.getAmount());
        return Betting.create(user.id(), prediction.id(), command.getAmount());
    }
}

 

저는 도메인 서비스로 분리할 필요가 없다는 결론을 내렸습니다.

애플리케이션 서비스의 주된 역할 중 하나는 "여러 도메인 객체를 오케스트레이션(Orchestration)하여 하나의 유스케이스를 완성하는 것"입니다. 따라서 2개 이상의 엔티티를 사용하는 것은 애플리케이션 서비스의 역할입니다.

 

또한, 이 두 메서드(deductPoints, create)의 호출 순서는 비즈니스 정책적으로 아주 민감한 요소라기보다, 트랜잭션 내에서 원자적으로 이루어져야 하는 '절차'에 가깝습니다. 즉, 여러 엔티티의 상태를 엮어 복잡한 '정책'을 판단하는 단계가 아니므로 도메인 서비스의 분리는 불필요하다고 판단했습니다.

 

그리고 실제로 수행되는 비지니스 로직은 각각의 도메인 안에서 수행되기에 비지니스 로직이 애플리케이션 서비스에 침투되었다고 판단하기는 어렵다고 생각합니다.

도메인 서비스는 언제 분리

상황을 바꿔서, "VIP 유저라면 베팅한 금액의 1.1배를 인정해 준다" 는 정책이 추가되었다고 가정해 보겠습니다.

public Betting participateInBetting(User user, Prediction prediction, Long amount, Option option) {
    // ... 유효성 검증 로직 등 ...

    // [User의 상태에 따라 입력값을 변화시키는 '정책']
    long finalAmount = amount;
    if (user.isVip()) {
        finalAmount = (long) (amount * 1.1);
    }

    user.deductPoints(amount);
    return Betting.create(user.id(), prediction.id(),finalAmount);
}

여기서 중요한 부분은 VIP 보너스 정책입니다.

  • User 엔티티의 책임인가? 아닙니다. User는 자신이 VIP라는 '상태'만 알면 되지, '베팅'이라는 다른 도메인 컨텍스트에서 그 상태가 어떻게 활용될지(보너스 계산)까지 알 필요는 없습니다.
  • Prediction 엔티티의 책임인가? 더더욱 아닙니다. 예측 도메인이 유저 등급을 아는 것은 어색합니다.
  • Betting 엔티티의 책임인가? Betting은 이 모든 로직의 '결과'로 생성되는 객체이므로, 생성되기 전의 계산 로직을 책임질 수 없습니다.

결국 "VIP 유저의 베팅액을 계산하는 정책"은 User와 Betting 그 어디에도 속하지 않는, 주인 없는 도메인 로직이 됩니다.

바로 이렇게 특정 엔티티에 넣기 애매한 도메인 규칙이나 정책을 책임지는 객체가 바로 도메인 서비스입니다.

마치며

결론은, 단순한 위임과 절차는 애플리케이션 서비스가, 여러 엔티티에 걸친 복잡한 정책 계산은 도메인 서비스가 담당해야 합니다.

 

무엇보다 중요한 것은 각 도메인 객체의 책임과 역할을 먼저 명확히 정의하는 것입니다. 어떤 로직을 누가 책임져야 할지 고민했을 때, 비로소 도메인 서비스로 분리해야 할 '주인 없는 로직'을 명확하게 식별할 수 있다고 생각합니다.

 

제 주관적인 의견이 많이 담겨 있어 정확하지 않을 수 있습니다.

'Server' 카테고리의 다른 글

[Server] 게시글 좋아요 수 조회 전략 : COUNT 쿼리 VS 반정규화  (0) 2026.01.01
[Server] 신뢰성 확보 WITH Transactional OutBox Pattern  (0) 2025.03.21
[Server] 멱등성 보장  (0) 2025.03.21
[Server] 조회 쿼리 개선  (0) 2025.03.05
'Server' 카테고리의 다른 글
  • [Server] 게시글 좋아요 수 조회 전략 : COUNT 쿼리 VS 반정규화
  • [Server] 신뢰성 확보 WITH Transactional OutBox Pattern
  • [Server] 멱등성 보장
  • [Server] 조회 쿼리 개선
절박한개발자
절박한개발자
깃허브 주소 : https://github.com/Kzerojun
  • 절박한개발자
    절박한개발
    절박한개발자
  • 전체
    오늘
    어제
    • 분류 전체보기 (99)
      • Server (5)
      • 프로젝트 (7)
      • Spring (7)
      • AI (1)
      • JPA (6)
      • JAVA (7)
      • Backend (3)
      • WEB (3)
      • 알고리즘-이론 (6)
      • 알고리즘-문제 (28)
      • CS (24)
        • 데이터베이스 (8)
        • Network (5)
        • OS (10)
        • LINUX (1)
      • 개발면접준비 (1)
      • 기타 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    CPU
    2
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
절박한개발자
[Server] 도메인 서비스(Domain Service)와 애플리케이션 서비스(Application Service)에 대한 고찰
상단으로

티스토리툴바