클린 아키텍처 보충 2 — 엔티티(EBI), 도메인 주도 설계

Junha Baek
junhabaek
Published in
17 min readJun 20, 2022

--

0. 이 포스팅에서는?

지난 포스팅에서는 클린 아키텍처, 포트와 어댑터의 의의, 의존성 규칙에 대해 알아보았습니다. 이 포스팅부터는 클린 아키텍처의 각 구성 요소들을 직접 구현하고, 구현 과정에서 드는 의문들을 해결해나갈 예정입니다.

특히 이 포스팅에서는 클린 아키텍처에서 도메인 영역을 DDD로 구현할 때, 고려해야 하는 점들을 위주로 다르며 자세한 내용은 목차를 확인 해주세요.

목차

· 0. 이 포스팅에서는?
· 목차

· 1. Enterprise Business Rule
· 2. 클린 아키텍처 엔티티(EBI)

· 2–1. 엔티티 개요
· 2–2. 엔티티 in EBI 접근법(approach)

· 3. 클린 아키텍처와 도메인 주도 설계(DDD)
· 3–1. 클린 아키텍처에 도메인 주도 설계를 적용한다는 것은?
· 3–2. 비즈니스 규칙 구현에 있어서 EBI, DDD의 차이
· 3–3. EBI vs DDD — 핵심 업무 데이터, 핵심 비즈니스 규칙
· 3–4. EBI vs DDD — 여러 응집성 있는 엔티티들이 관여하는 비즈니스 규칙(애그리거트)
· 3–5. EBI vs DDD — 기타 엔티티들이 관여하는 비즈니스 규칙(도메인 서비스)

· 4. 마치며
· 5. 참고
· 5–1. 서적
· 5–2. 포스팅/사이트

1. Enterprise Business Rule

이 장에서 소개할 엔티티는 클린 아키텍처에서 Enterprise Business Rules 영역의 요소입니다.

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

먼저, 비즈니스 규칙(Business Rule)이란 ‘사업적으로 수입을 얻거나 비용을 줄일 수 있는 규칙(개체에 대한 사실 or 개체간의 관계) 또는 절차(사람/컴퓨터 등 수행 주체와 무관)’을 말합니다.

이 정의가 ‘수입’을 포함해야 한다는 생각에 상당히 좁은 범위라고 생각하실 수 있습니다. 하지만, 예를 들어 ‘책의 가격 조정은 책 발매 후 12개월 동안 불가능하다.’ 라는 문장도 온라인 서점 회사가 운영하는 E-Commerce 비즈니스를 구성하는 요소들을 정의/제약하고 있으므로 비즈니스 규칙(또다른 정의 참고)으로 여겨질 수 있습니다.

이 비즈니스 규칙 중 특히 Enterprise Business Rule은 ‘Enterprise-wide + Business Rule’(원서 기준), 즉 ‘기업 전반적으로 적용되는 비즈니스 규칙’으로 이해하시는 것이 좋습니다.

2. 클린 아키텍처 엔티티(EBI)

그렇다면 이 Enterprise Business Rules 안에 속한 엔티티는 무엇일까요?

2–1. 엔티티 개요

핵심 비즈니스 규칙은 대부분 데이터를 요구합니다. 이 핵심 비즈니스 규칙과 핵심 비즈니스 데이터는 아주 긴밀한 관계를 가집니다.

예를 들어 위에서 들었던 가격 조정 비즈니스 규칙을 다시 생각해보면, 가격 조정 비즈니스 규칙은 발매일, 가격 등 ‘책’과 관련된 데이터들을 요구합니다. 이 때, 이 비즈니스 규칙과 비즈니스 데이터를 결합한 것이 클린 아키텍처에서 말하는 엔티티입니다.

예를 들어 책 엔티티를 Java 언어를 이용해 다음과 같이 표현할 수 있습니다.

  • Book Class는 bookName, price, publishedDate 등 핵심 비즈니스 데이터들을 정의하고 있습니다.
  • 온라인 서점 도메인 내의 다른 엔티티인 Author와의 일대 일 관계를 가지고 있습니다. 간과하기 쉽지만, 엔티티 간의 관계도 핵심 비즈니스 규칙의 일부로 볼 수 있습니다.
  • changePrice라는 핵심 비즈니스 규칙을 포함하고 있습니다. 이 핵심 비즈니스 규칙은 publishedDate와 price의 핵심 비즈니스 데이터와 긴밀하게 관련되어 있습니다.

2–2. 엔티티 in EBI 접근법(approach)

엔티티가 무엇인지 더 잘 이해하기 위해서는 엔티티가 어디에서 비롯되었는지 알아볼 필요성이 있습니다. 클린 아키텍처의 엔티티와 유즈케이스는 Ivar Jacobson의 EBI approach(ECB)의 idea로부터 비롯되었습니다.

EBI 접근법은 주요 객체의 종류를 Entity, Boundary, Interactor(Control)로 구분합니다.

  • Entity는 핵심적인 데이터와 범용적으로 사용될 수 있는(Application에 독립된) 비즈니스 규칙이 있는 비즈니스 객체입니다.
  • Boundary는 외부 영역과의 연결고리로써, 받아들일 request와 반환할 response에 대한 spec(interface)이 정의되어 있습니다. 포트와 어댑터에서는 Driving Port/Input Port(유즈케이스 보충에서 다룰 주제)와 유사한 개념입니다.
  • Interactor(Control)는 Boundary의 구현체로 요청을 받아들인 후 엔티티와 상호작용(호출)하여 요청에 맞는 결과를 냅니다.(반환을 하거나, 영속성 작업을 수행하거나)
    Boundary나 Entity에 필요한 기능들(메서드)을 배치한 후에도 여전히 남아있는 작업(엔티티가 직접 수행하기에 부적절한 작업)은 Interactor 내에 구현 되어야 합니다. 예를 들어, 책 주문시 회원 등급에 따라 차등적으로 가격이 정해진다면, 책 엔티티와 회원 엔티티의 협력이 필요한데, 이를 특정 엔티티에는 넣기 어려우므로 Interactor가 담당해야 합니다.
    이는 클린 아키텍처의 Usecase와 유사한 개념입니다.

3. 클린 아키텍처와 도메인 주도 설계(DDD)

이 부분은 클린 아키텍처를 사용하지만, 도메인 주도 설계 적용에는 익숙치 않은 분들을 위한 설명이 이어집니다. 이미 도메인 주도 설계를 실천 중이시라면, 이 부분은 가볍게 읽어주세요.

3–1. 클린 아키텍처에 도메인 주도 설계를 적용한다는 것은?

먼저 클린 아키텍처에서 비즈니스 규칙을 구현하는 방법을 되짚어 보겠습니다.

클린 아키텍처에서는 관심사의 분리, 의존성 규칙을 통해 핵심 비즈니스 규칙을 Enterprise Business Rules 영역에 격리시켰습니다. 핵심(Enterprise) 비즈니스 규칙은 엔티티(in EBI 접근법)가 주로 담당하고, 엔티티가 담당하기에 부적절(여러 엔티티를 아우르는 연산 등)하거나 응용 비즈니스 규칙에 해당하는 로직들은 Interactor가 담당하게 됩니다.

클린 아키텍처에서는 의존성 규칙을 준수하기 때문에 Enterprise Business Rules 영역은 유즈케이스, DB 등 외부 요소의 영향을 받지 않습니다. 이 덕분에 이 영역의 구현을 EBI 엔티티만으로 구현하는 것이 아니라 도메인 주도 설계 등 다른 방식으로 구현할 수 있습니다.

도메인 주도 설계를 적용하게 되면 크게 다음과 같은 이점들을 얻을 수 있습니다.

  • DDD의 요소들은 유비쿼터스 언어로 작성되므로, 도메인 전문가나 개발자 모두가 명확하게 이해할 수 있는 도메인 모델을 만들어 냅니다.
  • 바운디드 컨텍스트(전략적 패턴) 등으로 명확하게 구분된 정의 덕분에, 현재 주목해야 할 부분에 집중해 개발할 수 있습니다.
  • 핵심 도메인 모델은 특성이나 관계가 쉽게 변하지 않는 특징을 가지고 있기 때문에, 객체지향 개발에 있어 주요 객체 후보군을 추리고, 객체들간 협력 관계를 구성하는데 도움을 줍니다.

3–2. 비즈니스 규칙 구현에 있어서 EBI, DDD의 차이

그렇다면, DDD를 클린 아키텍처에 적용했을 때, 어떤 점들이 달라질까요? 다음은 EBI 접근법과 DDD의 비즈니스 규칙을 표현하는데 있어서 어떤 요소가 책임을 가지는지를 비교합니다.

Tactical DDD(전술적 패턴)에서는 도메인 영역이 애그리거트, 값 객체, 엔티티 등 여러 요소들로 구성되어 있습니다.

책임을 질 요소들이 많아진 만큼, 2개의 핵심 객체인 엔티티, Interactor만으로 많은 비즈니스 규칙을 구현하던 EBI에 비해 더 다양한 객체들이 비즈니스 규칙의 구현을 담당합니다.

3–3. EBI vs DDD — 핵심 업무 데이터, 핵심 비즈니스 규칙

먼저 DDD에서도 엔티티는 여전히 핵심 업무 데이터, 핵심 업무 로직을 지니고 있습니다. 인터넷 서점 사례의 Book 객체를 다시 생각해봅시다.

EBI 접근법에서나 DDD에서나 책 이름, 작가명, 가격, 발매일 등의 핵심 업무 데이터와 가격 변경 등의 핵심 업무 로직은 여전히 엔티티 내에 있습니다.

EBI 접근법에서는 엔티티가 가져야 했던 핵심 비즈니스 규칙들은 DDD에서 엔티티 뿐만 아니라 값 객체에 나누어져 정의됩니다.(참고 : 값 객체와 구분을 위해 DDD의 엔티티는 ‘식별성’을 지녀야 합니다.) 예를 들어, Book 엔티티가 가지고 있던 숫자형(java에서는 Long) 타입의 price는 Money 값 객체 형태로 저장됩니다.

값 객체는 말그대로 값 처럼 사용될 수 있어야 하므로, 다양한 계산 메서드와 초기화 메서드(0원)를 구현합니다.

DDD에서는 값으로 여겨질 수 있는 객체인 값 개체와 식별성 있는 객체인 엔티티를 구분하여, EBI에 비해 도메인 모델을 더 쉽게 이해할 수 있도록 해줍니다. 하지만, 값 개체는 DDD에 종속된 패턴이 아니기 때문에, DDD의 맥락이 아니더라도 DDD 없이도 클린 아키텍처에 적용할 수도 있습니다.

3–4. EBI vs DDD — 여러 응집성 있는 엔티티들이 관여하는 비즈니스 규칙(애그리거트)

여러 엔티티(동일 타입 포함)가 관여하는 비즈니스 규칙은 하나의 유즈케이스 내에서 여러 엔티티(애그리거트)를 다룰 때 이를 중재(orchestrate)하는 방법을 포함합니다. 엔티티가 이 역할을 직접 맡기에는 해당 엔티티가 다른 엔티티로의 너무 과한 의존관계를 가질 가능성이 있기 때문에 EBI 접근법에서는 Interactor에서 구현했었습니다.

반면, DDD에서는 이를 애그리거트나 도메인 서비스 내에 구현합니다. (여러 엔티티 관여 비즈니스 규칙을 제외한 나머지는 Application Service가 담당..)

특히 협력하는 엔티티들이 하나의 애그리거트 내에 있었다면, 애그리거트 루트가 엔티티 사이를 중재합니다.

예를 들어 온라인 서점 애플리케이션에서 책을 주문하는 경우, 한 번의 주문에 여러가지 주문 항목이 포함되어 있습니다.

Aladin-주문내역

이를 DDD에서 모델링 한다면 대략 다음과 같은 구조를 상상해볼 수 있습니다.(설명을 위해 간소화 시켰습니다.)

위의 구조에서 주문 총액을 알고 싶다면 어떻게 계산해야 할까요?

애그리거트는 여러 엔티티들을 하나의 큰 단위로써 바라볼 수 있게 해주며, 정보 은닉과 도메인 invariant를 보장하는 역할을 수행합니다. 그 중 애그리거트 루트가 애그리거트의 단일 진입점이면서, 애그리거트 내 구성요소들 사이를 중재하는 역할을 수행하기 때문에 주문 총액을 반환하기에 적합합니다.

위 코드에서는 Order 애그리거트 루트가 OrderLine을 순회하면서 금액을 합산해 반환합니다.

물론 이는 Order가 totalAmount라는 필드를 지니고 있어 OrderLine이 추가/제거될때마다 갱신을 시키는 방식으로 구현될 수도 있습니다. 하지만 결국 메서드가 어떤식으로 처리되는지는 getTotalAmount라는 메서드(메시지)의 뒤로 숨겨지고, 이 판단은 전적으로 Order 애그리거트 루트가 담당한다는 점입니다.

만약 이를 EBI 접근법의 Interactor에서 구현한다면 다음과 같은 코드를 생각해볼 수 있습니다.(DDD가 아닌 방식으로 구현이 익숙치 않아, 주요 로직이 Interactor 내에 담겨있다는 것만 참고하는 용으로 봐주세요..)

  1. DDD와 달리, Order가 OrderLine들에 대한 참조를 가지고 있지 않다고 가정했습니다. 때문에, 전용 영속성 저장소를 사용해 OrderLine들을 별도로 불러옵니다.
  2. DDD에서는 애그리거트에 담겨있었던 합산 로직이, Interactor에 담겨있습니다.

DDD의 애그리거트는 준수해야 하는 Invariant가 있는 만큼, 밀접한 관련이 있는 엔티티 그룹으로 높은 응집성을 보장합니다. 하지만, EBI의 Interactor는 기능 자체가 엔티티 밖에 구현되기 때문에, 상대적으로 낮은 응집성을 가지게 됩니다. 또한, 무상태 객체가 책임을 진다는 면에서 GRASP의 Information Expert 조언도 약간 어기게 되는 면도 있습니다.

3–5. EBI vs DDD — 기타 엔티티들이 관여하는 비즈니스 규칙(도메인 서비스)

조금 전의 애그리거트의 사례와 달리, 만약 다른 애그리거트끼리 협력하거나, 다른 애그리거트 내의 엔티티의 협력을 필요로 하는 경우 DDD에서는 도메인 서비스가 이들 사이를 중재합니다.

설명을 위해 IDDD 7장에 나온 multi-tenancy 애플리케이션의 예시를 살펴보겠습니다. (설명을 위해 Tenant → Workspace로 수정해 설명합니다.)

Slack은 협업 도구로, 하나의 이메일 주소를 이용해서 여러개의 workspace를 이용할 수 있지만, 각각의 workspace의 맥락은 철저하게 격리됩니다. 이 때, 각각의 workspace를 tenant라고 부르며, 이 애플리케이션을 multi-tenancy 애플리케이션이라고 부릅니다.

위의 예시에서 ‘Baek’이 회사 workspace에 인증하는 상황을 생각해 보겠습니다. (인증은 로그인 외에도 중요 정보 변경에서 요구될 수도 있습니다.)

인증은 다음과 같은 세부사항을 포함합니다.

  1. Workspace에 인증하기 위해서는 Workspace가 활성화 된 상태여야 한다.(활성화/비활성화 변환 가능하다고 가정)
  2. 비밀번호는 암호화 되어있다. (모든 tenant에 대해서 동일한 방식)
  3. 입력받은 비밀번호(암호화 후)가 현재 사용자의 비밀번호와 일치해야 한다.

이 기능은 ‘Identity & Access Bounded Context(식별자와 액세스)’ 내에 구현된다고 가정합니다. 이 영역은 프로젝트 내에서 인증/인가를 전적으로 담당하는 영역입니다.

위의 세부사항을 가지는 인증 기능을 어떤 방식으로 구현해야 할까요? 반 버논은 이에 대해 4가지 방법들을 먼저 제시합니다. 이 4가지 방법은 모두 적절하지 않다는 결론을 내게 되지만, ‘이렇게 하면 되지 않나?’라는 질문을 해소하고자 같이 살펴보고 가겠습니다.

  1. Workspace(Tenant) 내에서 암호화를 다루고, 암호화된 비밀번호를 User에게 보낸다.

이 방법에서 Workspace는 ‘User가 암호화된 비밀번호를 요구한다는 것’과 사용된 암호화 방식(AES 등)과 User에 대한 인터페이스(메서드 형태)를 알고 있어야 합니다.

만약, Workspace가 애그리거트이며, User가 이 애그리거트에 속하는 엔티티 중 하나였다면 이것은 자연스러울 수 있습니다. 하지만, 애그리거트는 가급적 작게 유지되어야 하며, 유지해야 할 invariant가 없는 이상 같은 애그리거트에 담길 필요가 없다는 조언에 따라 이 둘은 별도의 애그리거트가 될 가능성이 높기 때문에 이 방식은 적합하지 않아 보입니다.

2. User가 암호화 수행을 담당합니다.

User에 저장된 비밀번호는 암호화되어 있어야 한다는 제약사항이 있으므로, User가 암호화에 대해서 어느정도 알고 있어야 한다는 근거에 따른 방법입니다.

하지만, 여전히 Workspace(tenant) 활성화 여부가 먼저 체크되어야 하기 때문에, Tenant는 User에 대한 Facade로써의 역할도 수행합니다. 암호화를 포함한 실질적인 인증 로직은 User가 전부 처리하는 셈인데 Workspace가 User를 알아야 한다는 것은 과도한 책임이라 생각할 수 있습니다. 이전 방법에서 언급했던 것과 마찬가지로 유지할 invariant가 있는 같은 애그리거트 내에 존재한다면 유효한 방법일 수 있습니다.

3. Workspace가 User에게 raw 비밀번호의 암호화를 요청하고, 이를 User가 가진 비밀번호와 비교한다.

이 방법에서 Workspace는 User의 파사드일 뿐이라는 오명을 벗기 위해, 비밀번호 비교를 직접 수행합니다. 하지만, 이것은 디미터의 법칙이나 묻지말고 시켜라를 어기기 때문에, 좋은 객체의 협력이라고 보기 어렵습니다.

4. 클라이언트가 비밀번호를 암호화해서 Workspace(Tenant)로 보내도록 한다.

클라이언트의 후보군 중 하나는 애플리케이션 서비스입니다.(Test Script도 또다른 클라이언트 중 하나) 즉, 애플리케이션 서비스가 미리 비밀번호를 암호화해서 전달해야 합니다.

만일 이것이 Single-Tenancy에 DDD를 사용하지 않는다면, 애플리케이션 서비스가 암호화를 담당해도 괜찮다고 판단할 수 있습니다. 하지만 DDD에서는 사용자 관리 또한 별도의 도메인 영역(Identity & Access Bounded Context)을 형성하는 것을 권장합니다. 자연스럽게 암호화 로직은 이 영역 내에 담기게 되겠죠.

앞선 4가지 방법들은 각자 약간의 문제점을 가지고 있었습니다.

반 버논은 여기에 도메인 서비스를 도입해 문제를 해결하고자 합니다.

이 예제에서는 AuthenticationService, EncryptionService 2개의 도메인 서비스가 정의됩니다.

도메인 서비스는 여러 애그리거트를 중재하는 목적 외에도, 도메인 로직을 담고 있는 무상태(stateless) 오퍼레이션을 구현하기 위해 도입될 수 있습니다. 위의 예에서는 EncryptionService가 그 예시입니다.

AuthenticationServiceEncryptionServiceWorkspace, User에 대한 중재를 합니다. 따라서, Workspace(Tenant)와 User가 서로에 대해 알지 않고도 인증 로직을 구현할 수 있습니다.

4. 마치며

이 포스팅에서는 주로 ‘핵심 비즈니스 로직을 어디에 구현하는가?’에 관점을 두어 EBI와 DDD의 비교를 해보았습니다.

핵심을 정리해보자면

  • DDD는 EBI에 비해 더 많은 요소들(Tactical DDD)로 만들어질 수 있다.
  • DDD에서 도메인 로직은 도메인 영역안에서만 구현된다. 반면, EBI는 엔티티와 Interactor가 나누어 책임을 가진다.

하지만, 여전히 DDD의 요소들은 EBI에 비해 지켜야 할 규약/조언들이 많이 있기 때문에, 실제 적용을 위해서는 DDD 관련 서적(IDDD, 도메인 주도 개발 시작하기 등)을 읽어보시는 것을 권장합니다.

참고로 기존 EBI 방식에서의 비즈니스 로직 구현이 뒤떨어진다고는 오해하지는 말아주세요. 여기에서는 어디까지나 EBI 방식과 DDD 방식의 차이를 설명하기 위해, **‘정의 그대로의 EBI’**와 DDD를 비교했습니다. 하지만, EBI Architecture가 여러 영역들을 의존성 규칙을 통해 확실하게 분리한 만큼, 비즈니스 규칙 영역은 DDD를 적용하지 않더라도 여러 객체지향적 방법론이나 조언들을 적용해서 깔끔하게 구현할 수도 있을 것입니다.

여기서 언급되지 않았지만 EBI의 Boundary, Interactor와 DDD의 애플리케이션 서비스/도메인 서비스는 더 많은 차이점을 가지고 있습니다. 이는 유즈케이스 포스팅에서 애플리케이션 서비스와 도메인 서비스를 비교하면서 다시 다루어질 예정입니다.

다음 포스팅에서는 엔티티와 ORM annotation에 대한 이야기를 집중적으로 다룰 예정입니다. 감사합니다.

5. 참고

5–1. 서적

  • 클린 아키텍처 19~20장, 22장 — Robert C. Martin
  • 만들면서 배우는 클린 아키텍처 8장, 11장 — Tom Hombergs
  • Implementing Domain Driven Design(IDDD) 1장, 4~7장, 10장 — Vaughn Vernon
    DDD의 이점, DDD와 다른 아키텍처의 조합, 엔티티의 특징 참고, 도메인 서비스 예시(설명을 위해 tenant → workspace로 수정) 인용, 작은 애그리거트의 조언 참고
  • 객체지향의 사실과 오해 6장 — 조영호
    도메인 모델의 이점 참고
  • 도메인 주도 개발 시작하기 3장 — 최범균
    예시로 든 주문 애그리거트 디자인 참고

5–2. 포스팅/사이트

  • Business Rule 정의 ( 참고1, 참고2 )
  • 비즈니스 로직과 도메인 로직 ( 참고 )
  • Enterprise Business Rule vs Application Business Rule( 참고 )
  • Entity-Control-Boundary( 참고 )
  • EBI approach와 클린 아키텍처 ( 참고 )
  • EBI approach와 클린 아키텍처 2 ( 참고 )
  • EBI approach 정리 ( 참고 )
  • 디미터 법칙( 참고 )

--

--