
Application Service와 Domain Service 차이는 DDD와 레이어드 아키텍처를 공부할수록 더 헷갈리는 주제입니다. 둘 다 Service라는 이름을 쓰고, 둘 다 비즈니스 로직 근처에 있기 때문입니다. 하지만 실제로는 하나는 유스케이스 흐름을 조정하고, 다른 하나는 도메인 규칙을 표현합니다.
이 차이를 놓치면 Application Service가 비대해지거나, 반대로 엔티티 책임까지 전부 Service로 빠져서 빈혈 도메인 모델로 흐르기 쉽습니다. 이번 글에서는 기존 도메인 서비스 글과 연결해서, 흐름과 규칙을 어디서 나눠야 하는지 실전 판단 기준으로 설명하겠습니다.

기본 개념의 뿌리는 Martin Fowler의 Service Layer 패턴, aggregate와 경계 감각은 DDD Aggregate 글, 도메인 모델의 불변 조건 감각은 Microsoft Learn의 Domain Model validation 문서에서 참고할 수 있습니다.
한눈에 정리
먼저 짧게 정리하면, Application Service는 조회·실행·저장 같은 유스케이스 흐름을 조정하고, Domain Service는 특정 엔티티 하나에 넣기 애매한 도메인 규칙을 판단합니다. 그리고 엔티티는 자기 상태와 불변 조건을 직접 지키는 쪽이 자연스럽습니다.
왜 자꾸 헷갈릴까
이 주제가 어려운 첫 번째 이유는 이름입니다. Application Service도 서비스고 Domain Service도 서비스라서, 초보자 입장에서는 둘 다 그냥 “비즈니스 로직 모아두는 클래스”처럼 보이기 쉽습니다.
두 번째 이유는 실제 코드에서 둘이 매우 가까이 붙어 있기 때문입니다. Application Service가 주문을 조회하고, Domain Service로 정책을 판단하고, 다시 저장하는 식으로 한 흐름 안에서 이어지기 때문에 경계가 흐려집니다.
하지만 질문을 바꾸면 조금 쉬워집니다. 이 로직은 무엇을 어떤 순서로 진행할지 다루는가, 아니면 무엇이 도메인적으로 맞는지 판단하는가를 먼저 보면 됩니다.
Application Service 역할
Application Service는 보통 유스케이스 흐름을 조정합니다. 주문을 조회하고, 권한을 확인하고, 필요한 도메인 규칙을 실행하고, 저장하고, 이벤트를 발행하는 식으로 한 요청의 시작과 끝을 묶습니다.
- 트랜잭션 경계를 잡는다
- 리포지토리 조회/저장 순서를 조정한다
- 여러 도메인 객체와 서비스를 연결한다
- 외부 시스템 호출과 응답 DTO 생성을 묶는다
즉 Application Service의 핵심은 도메인 규칙의 주인이 되는 것이 아니라, 유스케이스를 안전하게 끝까지 진행하는 오케스트레이션에 있습니다.
Domain Service 역할
Domain Service는 도메인 로직이 분명한데 특정 엔티티 하나의 메서드로 넣기에는 어색한 규칙을 담는 도구입니다. 여러 객체를 함께 봐야 하거나, 규칙의 주어가 특정 엔티티 한 명으로 잘 읽히지 않을 때 후보가 됩니다.
예를 들어 주문, 고객 등급, 승인 한도 정책을 함께 보고 주문 승인 가능 여부를 판단해야 한다면, 이 판단은 분명 도메인 규칙입니다. 하지만 주문 객체 하나에만 밀어 넣으면 오히려 주어가 부자연스러워질 수 있습니다.

즉 Domain Service는 “이 요청을 어떻게 처리하지”보다 “이 판단은 어떤 규칙으로 내려야 하지”에 더 가까운 곳입니다.
엔티티 메서드와 차이
여기서 더 중요한 질문은 “여러 객체를 보면 무조건 Domain Service인가”가 아닙니다. 더 중요한 기준은 이 책임의 주어가 자연스럽게 누구인가입니다.
주문이 이미 취소되었는지, 승인 가능한 상태인지처럼 자기 상태를 바탕으로 충분히 판단할 수 있는 것은 엔티티 메서드가 더 자연스럽습니다. 반대로 주문, 고객 등급, 외부 정책을 함께 봐야 하는 승인 정책은 엔티티 하나에만 밀어 넣기 어렵습니다.

코드로 보면
아래 예시는 주문 승인 정책을 단순화한 예시입니다. Application Service는 조회, 권한 확인, 정책 실행, 저장, 이벤트 발행을 묶고, Domain Service는 승인 가능 정책을 판단하며, 엔티티는 자기 상태 전이를 직접 지킵니다.
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final ApprovalPolicy approvalPolicy;
private final DomainEventPublisher eventPublisher;
@Transactional
public void approve(Long orderId, UserApprover approver) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("주문이 없습니다."));
if (!approver.canApprove(order)) {
throw new IllegalStateException("승인 권한이 없습니다.");
}
ApprovalDecision decision = approvalPolicy.decide(order, approver);
order.approve(decision);
orderRepository.save(order);
eventPublisher.publish(new OrderApprovedEvent(order.getId()));
}
}public class ApprovalPolicy {
public ApprovalDecision decide(Order order, UserApprover approver) {
if (order.isCanceled()) {
throw new IllegalStateException("취소된 주문은 승인할 수 없습니다.");
}
if (order.totalAmount().isGreaterThan(approver.approvalLimit())) {
throw new IllegalStateException("승인 한도를 초과했습니다.");
}
if (!order.paymentCompleted()) {
throw new IllegalStateException("결제가 완료되지 않았습니다.");
}
return ApprovalDecision.approvedBy(approver.id());
}
}public class Order {
private OrderStatus status;
private Long approvedBy;
public void approve(ApprovalDecision decision) {
if (status != OrderStatus.PAID) {
throw new IllegalStateException("결제 완료 상태에서만 승인할 수 있습니다.");
}
this.status = OrderStatus.APPROVED;
this.approvedBy = decision.approverId();
}
}
트랜잭션 경계는 누가 다룰까
보통 트랜잭션을 언제 열고 닫을지, 저장 순서를 어떻게 가져갈지, 외부 시스템 호출을 어느 타이밍에 둘지는 Application Service 쪽이 더 가깝습니다. 이것은 규칙의 본질이라기보다 유스케이스 실행 방식에 가깝기 때문입니다.
반면 Domain Service는 트랜잭션 안에서 호출될 수는 있어도, 트랜잭션 자체를 책임의 중심에 두지는 않습니다. 그래서 Domain Service가 저장, 외부 API 호출, 응답 조립까지 다 들고 있다면 사실상 Application Service처럼 변해가고 있는 신호일 수 있습니다.
섞이면 생기는 문제
여기에 트랜잭션 스크립트 감각을 같이 놓고 보면 더 분명해집니다. CRUD 위주의 단순 유스케이스에서는 Application Service가 흐름과 규칙을 꽤 많이 같이 들고 있어도 크게 문제되지 않을 수 있습니다. 하지만 규칙이 두꺼워지고 예외가 늘기 시작하면, 그때부터는 흐름과 규칙을 같은 메서드에 계속 밀어 넣는 방식이 빠르게 버거워집니다.
즉 모든 Application Service가 바로 나쁘다는 뜻이 아니라, 변화 양상에 비해 규칙이 두꺼워졌는데도 계속 오케스트레이션 클래스 안에만 쌓이고 있다면 경계를 다시 볼 시점이라는 뜻입니다.
Application Service 비대화
Application Service 안에 정책 if 문과 계산이 계속 늘어나면 서비스 메서드가 점점 커지고, 같은 규칙이 여러 유스케이스에 복제되기 쉽습니다. 이때 코드는 왜 그런지보다 어떻게 흘렀는지만 남게 됩니다.
빈혈 도메인 모델
반대로 엔티티가 지켜야 할 자기 무결성까지 전부 Service로 빠지면 객체는 데이터 상자로 남고, 상태 규칙은 바깥에 흩어집니다. 이 문제는 도메인 서비스는 언제 필요할까 글과도 직접 연결됩니다.
실무에서 바로 쓰는 판단 체크리스트
- 이 로직의 주어가 자연스럽게 누구인가를 먼저 본다
- 무엇을 할지의 흐름이면 Application Service 쪽 질문이다
- 무엇이 맞는지의 규칙이면 Entity 또는 Domain Service 쪽 질문이다
- 특정 엔티티 하나에 자연스럽게 귀속되면 엔티티 메서드가 우선이다
- 저장 순서, 트랜잭션, 외부 호출 관리가 핵심이면 Application Service 성격이 강하다
- 같은 정책이 여러 유스케이스에서 반복되면 Domain Service나 Entity로 책임을 모을 시점일 수 있다
결국 이 셋은 경쟁 관계가 아니라 문장의 주어가 다른 도구입니다. 이 감각이 잡히면 서비스가 늘어나는 프로젝트에서도 어디에 무엇을 둬야 할지 훨씬 덜 헷갈립니다.
마무리
Application Service와 Domain Service 차이의 핵심은 이름이 아니라 책임의 방향입니다. Application Service는 유스케이스 흐름을 조정하고, Domain Service는 특정 엔티티 하나에 넣기 애매한 도메인 규칙을 표현합니다. 그리고 엔티티는 자기 상태와 불변 조건을 직접 지키는 쪽이 자연스럽습니다.
즉 흐름은 Application Service, 규칙은 Entity 또는 Domain Service라는 기준만 분명히 잡아도 Service 남용과 빈혈 모델을 동시에 많이 줄일 수 있습니다.