애플리케이션 레이어는 도메인 모델을 사용하여 실제 사용자의 목표를 달성시켜주는 역할을 합니다. 즉, 여러 Aggregate와 도메인 서비스를 조율(Orchestration)하여 하나의 완전한 비즈니스 프로세스를 완료하는 계층입니다. 소프트웨어 아키텍처에서 애플리케이션 계층을 구성하는 두 가지 대표적인 패턴이 있습니다. 바로 도메인 주도 설계(DDD)의 애플리케이션 서비스(Application Service)와 클린 아키텍처(Clean Architecture)의 유스케이스(Use Case)입니다. 둘은 비슷한 역할을 수행하지만, 그들을 바라보는 철학과 구조에 약간의 뉘앙스 차이가 존재합니다.
가장 근본적인 차이는 클래스를 구성하는 '중심 관점'에 있습니다. 애플리케이션 서비스(DDD)에서는 도메인 개념을 중심으로 관련 기능들을 묶습니다. 가령 Order 애그리거트와 관련된 기능들은 OrderService에, Member와 관련된 기능들은 MemberService에 모이는 식입니다. 이는 도메인 모델의 구조를 애플리케이션 계층에 반영하는 '모델 중심' 접근법입니다. 유스케이스(Clean Architecture)에서는 사용자의 행동(User Action) 또는 시스템의 단일 응답을 중심으로 클래스를 구성합니다. '주문 생성'이라는 행동은 CreateOrderUseCase 클래스로, '주문 취소'는 CancelOrderUseCase 클래스로 분리됩니다. 이는 사용자의 시나리오를 코드로 표현하는 '행위 중심' 접근법입니다. 이러한 관점의 차이는 클래스의 구조로 이어집니다.
애플리케이션 서비스에서는 하나의 서비스 클래스가 여러 개의 공개 메서드(Public Methods)를 가집니다. 각 메서드가 하나의 유스케이스에 해당합니다.
# 하나의 클래스, 여러 개의 책임
class OrderService:
def create_order(...): ...
def cancel_order(...): ...
def change_shipping_info(...): ...
Python
복사
반면 하나의 유스케이스 클래스는 단 하나의 공개 메서드(보통 execute 또는 handle)만을 가집니다. 클래스 자체가 하나의 유스케이스와 1:1로 매칭됩니다.
# 각 클래스가 단 하나의 책임
class CreateOrderUseCase:
def execute(...): ...
class CancelOrderUseCase:
def execute(...): ...
Python
복사
Application Service 클래스를 작성하는 경우와 달리, 사용자의 행위마다 메서드가 아니라 클래스가 새로 생겨나게 되므로 클래스 수가 늘어나고 보일러플레이트가 생겨나게 됩니다. 아주 간단한 CRUD(Create, Read, Update, Delete) 기능만 필요한 도메인에서도 CreateUserUseCase, UpdateUserUseCase, DeleteUserUseCase, GetUserUseCase처럼 클래스를 일일이 만드는 것은 오버엔지니어링입니다. 차라리 Application Service 클래스를 이용해서 '기능적 응집도' 측면에서는 하나의 도메인과 관련된 책임들을 모으는 편이 나을 수 있죠. 그래서 상대적으로 유즈케이스가 다양해서 하나의 비대한 Application Service 클래스를 작성하는 경우보다 쪼갰을 때 얻는 아래와 같은 장점이 클 때 채택하는 것을 추천합니다.
일단 이렇게 코드를 작성했을 때 얻을 수 있는 가장 큰 장점은 직관적인 이름입니다. CreateOrderUseCase는 '주문 생성 유스케이스'라는 의미를 명확하게 전달합니다. 복잡하고 여러 도메인의 협력이 필요한 핵심 비즈니스 로직일지라도 코드를 읽는 누구나 이 클래스가 어떤 역할을 하는지 즉시 이해할 수 있습니다. 직관적인 이름은 명확한 책임 범위를 의미하기도 합니다. 유즈케이스 클래스 하나는 '주문 생성'같은 단 하나의 책임만 가집니다. 이는 코드의 응집도를 높이고, 관련 없는 기능이 하나의 클래스에 섞이는 것을 방지하여 단일 책임 원칙을 자연스럽게 지키도록 유도합니다.
의미적으로는 클래스나 파일 이름에서 도메인 모델의 힘을 빼게 되면서, 어떤 행위를 하기 위해 여러 도메인 모델의 협력이 필요할 수 있음을 이름만으로도 유추할 수 있다는 장점이 있습니다. 가령 '주문 생성'이라는 비즈니스 프로세스는 단순히 Order Aggregate 하나만으로 끝나지 않습니다. 고객의 주문 가능 상태를 확인(Customer Aggregate)하고, 상품의 재고를 파악(Product Aggregate)하는 등의 작업이 필요할 것이라는 생각이 들게 됩니다. 반면 OrderService와 같이 클래스를 작성하게 이름이 Order이라는 Aggregate 이름에 강하게 종속되어 여러 Aggregate을 조합한다는 느낌이 잘 살지 않습니다.
의외로 마지막 장점을 상당히 크게 느낀 적이 있습니다. 도메인 클래스가 점점 많아지면 이에 맞게 애플리케이션 레이어가 항상 따라오게 되는데, 항상 SomeAggregateNameService와 같은 형식으로 클래스와 파일들이 생겨났습니다. 이렇게 파일과 클래스가 늘어나면 자연스레 도메인 계층과 애플리케이션 계층이 1:1 대응되는 느낌이 들게 됩니다. 사람이 애플리케이션 레이어의 수많은 파일들을 뒤질 때, 도메인 모델의 이름과 동일한 파일이 있으면 그것을 가장 먼저 보게 되니까요. 별 것 아닌 것 같지만 군사번역 프로젝트와 튜닝 프로젝트 등의 프로덕트를 구축할 때 명확하지 않은 애플리케이션 레이어의 역할, 비대해지는 SomeAggregateNameService 클래스, 다중 Aggregate들의 협력이 필요한 로직 배치와 명명 등이 쓸데없이 개발 속도를 늦추곤 했습니다. 가장 사용자와 가까운 계층에서부터 사고 흐름이 흘러가야 할 때에도, 도메인 모델부터 사고가 흘러갔기 때문입니다. 하지만 사용자가 하려는 행위를 기준으로 애플리케이션 레이어를 작성하게 되니 자연스럽게 클래스의 이름에서 Aggregate 이름이 사라지게 되었고, 그 결과 코드를 뒤지는 데 들어가는 시간이 크게 줄어들 수 있었습니다.
애플리케이션 서비스 패턴은 비교적 단순한 CRUD나, 도메인 하나의 책임이 명확한 기능인 경우 실용적입니다. 하나의 클래스에 관련 기능을 모아둠으로써 개발 속도를 높이고 코드의 파편화를 막을 수 있습니다. 복잡도가 높아지면 사용자의 행위를 중심으로 유스케이스를 추출하는 것이 유리합니다. 클래스 이름만으로도 "시스템이 무엇을 하는지" 명확히 드러나며, 비대해진 서비스 클래스로 인해 발생하는 인지 부하를 획기적으로 줄여줍니다.
from typing import Protocol
# 1. UseCase 인터페이스 정의
class CreateOrderUseCase(Protocol):
def create_order(self, items: list) -> str:
...
class CancelOrderUseCase(Protocol):
def cancel_order(self, order_id: str) -> bool:
...
# 2. Service 클래스에서 여러 UseCase 구현
class OrderApplicationService:
def create_order(self, items: list) -> str:
print(f"주문 생성 로직 실행: {items}")
# 길고 복잡하고 OrderApplicationService 내 메서드들 호출
def cancel_order(self, order_id: str) -> bool:
print(f"주문 취소 로직 실행: {order_id}")
# 길고 복잡하고 OrderApplicationService 내 메서드들 호출
# 3. 클라이언트(Controller 등)에서의 사용
def complete_checkout(use_case: CreateOrderUseCase):
# 이 함수는 cancel_order가 있는지 알 필요도 없고 접근할 수도 없음 (타입 힌트상)
result = use_case.create_order(["apple", "banana"])
return result
# 실행
service = OrderApplicationService()
complete_checkout(service) # OrderApplicationService는 CreateOrderUseCase 규격을 만족함
Python
복사
위와 같은 방식을 채택하면 구현은 서비스 클래스에서 통합 관리하여 하나의 클래스 내에서 공통 로직(헬퍼 함수, private 메서드)을 자유롭게 공유하고 재사용할 수 있어 코드 중복을 피할 수 있습니다. 애플리케이션 서비스의 의미와 응집성을 살리면서, 외부로 노출되는 인터페이스를 명시함으로써 유스케이스의 의미 또한 살릴 수 있는 구현이 가능합니다.
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
1.
None
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
1.
•
앞의 글은 대형 Entity를 의미하는 Aggregate라는 존재가 무엇인지 설명한다. 애플리케이션 레이어에서 주로 다루게 되는 도메인 레이어의 Entity는 Aggregate root일 가능성이 높다.
2.
•
이 글의 내용은 앞의 프롬프트에 반영되었다.
supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.
1.
None
opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.
1.
None
to : 이 문서에 작성된 생각이 어떤 생각으로 발전되거나 이어지는지를 작성하는 영역입니다.
1.
None
ref : 생각에 참고한 자료입니다.
1.
None