본문 바로가기

백엔드 잡학사전

"Domain Driven Development" 에 대하여

우리 팀 뿐 아니라 판교 전역에서 흔히 쓰이는 개발 방식인 DDD에 대해서 알아보자.

 

 

Q. DDD의 인기 추세

 

DDD는 반짝이는 트렌드라기보다는 "시대를 초월한 클래식"에 가깝다. 2003년에 Eric Evans의 책에 처음 언급되면서 세상에 알려졌고, 지난 20년 넘게 현대적인 아키텍처의 일환이다. 인기가 시들하다가, 최근 몇년간 MSA 마이크로서비스 아키텍처가 클라우드 네이티브 환경의 표준으로 자리잡으면서 다시 주목받기 시작한 케이스다. 하나의 거대한 monolith 시스템이 아닌, 여러 개의 작은 서비스로 나뉜 시스템을 적용하는 데에 있어서, DDD의 Bounded Context(경계가 있는 컨텍스트) 개념이 그 완벽한 해답을 제공하는 것처럼 보이기 때문이다. 참고로, "도메인 주도 설계"라는 키워드 그 자체보다, 원서의 부제인 "복잡한 소프트웨어 설계를 도와주는 지혜"가 더 와닿을 수 있다.

 

애초에 처음부터 MSA로 서버를 설계했으면 모르겠으나, 레거시 서버가 이미 대차게 자리잡고 있는 경우 부분적으로 마이그레이션을 생각한다면, 다른 것들보다 DDD가 좋다고 함.

 

 

Q. DDD와 비교되는 다른 방식들

 

1 CRUD 중심의 접근법: 가장 전통적이고 흔한 방식.

- 애플리케이션의 구조가 DB 테이블 구조와 거의 1:1로 매칭된다 (User 테이블 -> User 클래스 -> User API)

- 비즈니스 로직이 주로 Service 계층에 흩어져 있다. Domain 객체는 단순히 데이터만 담고 있는 껍데기가 되기 쉽다.

- 간단한 관리자 페이지나 비즈니스 로직이 거의 없는 시스템에 매우 효과적이지만, 비즈니스 규칙이 복잡해질수록 코드의 중복이 많아지고 유지보수가 극도로 어려워진다.

 

2 Transaction Script 패턴: CRUD 와 유사, "사용자 요청 하나를 처리하는 procedure = 하나의 트랜잭션 단위"로 묶어 설계하는 방식.

- UserService 안에 registerUser(), deleteUser() 같은 메서드가 있고, 이 안에서 모든 검증, 저장, 알림 등의 로직을 절차적으로 처리하는 방식.

- CRUD 접근법과 마찬가지로, 로직이 복잡해지면 여러 스크립트 간에 코드가 중복되고 일관성을 유지하기 어렵다.

 

 

결론적으로, 시스템이 복잡할 때 MSA의 구현 방식으로 현대 아키텍처 중 도메인 주도 개발 방식이 가장 대표적이라고 할 수 있다.

더불어, 최근 많이 언급되는 고급 아키텍처 패턴들은 대부분 DDD 사상을 기반으로 하거나, DDD와 함께 사용될 때 시너지를 낸다.

클린 아키텍처, CQRS, 이벤트 기반 아키텍처 등의 예시가 있지만, 일단 깊게 들어가지 말고 그렇게만 알고 있자.

 

 

Q. DDD가 뭔데?

 

다른 것들을 다 떠나서, DDD는 이것을 왜 사용해야 하는지 이해하는 것이 중요하다. DDD는, "비즈니스 복잡성"을 다루는 데에 핵심 목표가 있다. 소프트웨어를 개발할 때에 어려움은 코딩 기술에 대한 것도 있지만, 근본적으로 우리가 해결하려는 문제 그 자체의 복잡함에 있다. 예를 들어 내가 직면한 RGW 라이프사이클 모니터링만 해도 '룰', '버킷', '실행 정책', '히스토리', '상태' 등 여러 개념들이 얽혀 있다.

DDD란 이렇게 복잡한 도메인을 모든 사람이 이해할 수 있는 모델로 만들고, 그 모델을 코드에 그대로 반영하여 소프트웨어를 개발자 본인 뿐 아니라 프로 개발자가 아닌 해당 도메인 전문가도 이해하기 쉽게 만드는 설계 접근법이다. 이렇게 되다보니, 설계 단계에서부터 코드가 도메인의 본질을 명확히 반영하기 때문에, 코드 그 자체가 도메인의 문서 역할을 한다고 볼 수도 있을 것이다.

 

DDD는 크게 "전략적 설계" 와 "전술적 설계" 의 두 부분으로 나뉜다.

"전략적 설계" 는 시스템의 큰 그림을 그리는 것. 어떤 하위 시스템으로 나눌지, 그들 간의 관계는 어떻게 설정할지 등을 다루는 것이다.

"전술적 설계" 는 하나의 하위 시스템 내부를 코드로 어떻게 구현할지에 대한 구체적인 패턴들을 다룬다.

 

예를 들어 온라인 서점이라는 상위 도메인을 기반으로 애플리케이션을 개발할 때, 각 세부적인 기능을 '회원', '혜택', '주문', '결제', '리뷰', '배송' 등의 하위 도메인으로 나눌 수 있다. 이 과정을 통해 각 도메인이 무엇을 책임지고, 어떤 역할을 수행해야 하는지를 명확히 할 수 있다.

 

 

Q. 더 자세한 DDD의 기본적인 핵심 개념들?

 

우선 전술적 설계의 구성요소를 먼저 이해하는 것이 DDD를 체감하기에 가장 좋다.

 

1 유비쿼터스 언어 Ubiquitous Language

- 개념: 개발자와 도메인 전문가가 모두 함께 사용하는, 통일된 용어. 이 언어는 회의, 문서, 그리고 코드에까지 그대로 사용되어야 한다.

- 사용자가 말하는 '룰'과 개발자가 코드에 쓴 Rule 클래스가 정확히 같은 것을 의미해야 한다는 것.

- LC에서의 예시: 라이프사이클 룰, 버킷, 실행 히스토리 라는 용어를 정의했는데, 이것이 우리 프로젝트에서의 유비쿼터스 언어의 시작.

 

2 엔티티 Entity (즉, 도메인 Domain)

- 개념: 고유한 ID를 가지며, 생성부터 소멸까지 라이프사이클 동안 속성이 변하더라도 정체성이 유지되는 객체다.

- Bucket: 버킷의 라이프사이클 정책이 바뀌거나 프로젝트 ID가 변경되어도, '버킷 이름'이라는 고유 식별자로 정체성이 소멸되지 않는다.

- ExecutionHistory: 실행 상태가 '진행중'에서 '성공'으로 바뀌어도, HistoryID를 통해 '그 실행기록'임을 식별할 수 있다.

 

3 값 객체 Value Object

- 개념: 식별자 없이 속성 그 자체로 자신을 설명하는 객체. 두 객체의 속성값이 같다면, 둘은 같은 것으로 취급. 불변인 경우가 많다.

 

4 애그리거트 Aggregate

- 개념: 엔티티와 값 객체들의 묶음이다. 연관된 객체들을 하나로 묶어 데이터 일관성을 관리하는 단위가 된다.

- 애크리거트에는 root 엔티티가 하나 존재한다. 외부에서는 이 루트 엔티티를 통해서만 애그리거트 내부의 다른 객체에 접근할 수 있다. 또한, 데이터 정합성을 지키기 위해 하나의 트랜잭션에서는 하나의 애그리거트만 수정하는 것을 원칙으로 한다.

- 예를 들어, LC 시스템에 대해, Bucket을 애그리거트 루트로 설정 & 애그리거트 Bucket 엔티티와 그에 속한 ExecutionHistory 엔티티 목록을 포함 => 새로운 ExecutionHistory를 추가하려면, bucket.AddHistory와 같이 반드시 Bucket을 통해서만 추가해야 한다.

 

5 리포지토리 Repository

- 개념: 애그리거트를 저장하고 조회하는 역할을 하는 객체로, 도메인 모델과 데이터베이스 사이의 중재자 역할을 한다.

- 개발자는 복잡한 SQL 쿼리를 신경쓰지 않고, bucketRepository.FindByName("my-bucket") 처럼 도메인 관점에서 코드를 작성할 수 있다.

 

이를 좀 더 이해가 수월케 하기 위해 꼭 기억해야 하는 것 2가지는: Bounded ContextAggregate 이다.

 

 

Q 이 DDD의 실제 구현방식으로는 뭐뭐가 있지?

 

위와 같이 3개의 방식이 있다. 참고만 하는 걸로.

 

1) Layered:

2) Clean:

3) Hexagonal: