두께만큼이나 방대한 주제, 풍부한 예시, 그리고 디테일한 논의가 담겨 있는 책이다. DDD를 워낙 뭣도 모를 때 읽었고, 읽은지도 1~2년 정도 돼서 좀 걱정됐는데, 읽어보니 DDD 내용을 모르는 건 그리 큰 문제가 되지 않았다. 초반부의 전략적 설계 내용을 제외한 나머지 내용은 DDD에 국한되지 않는 이야기였다고 생각한다. 그만큼 범용적으로 활용될 수 있는 지식이었다는 뜻이다.
이 글에서는 내가 오래 기억하고 싶은 내용을 책의 목차에 맞춰 요약하고, 읽으면서 들었던 몇 가지 생각을 정리해보려고 한다.
전략적 설계
Ubiquitous Language (보편 언어)
보편 언어는 정말 중요한 개념이라고 생각한다. 특정 사람이 어떤 문제나 해결책을 묘사하는 언어는 곧 그 사람이 문제와 해결책을 이해하고 있는 방식을 표현한다. 따라서, 팀의 모든 인원이 참여하여 보편 언어를 구축해간다는 것은 곧 팀이 문제와 해결책을 바라보고 있는 시각과 서로가 가지고 있는 멘탈 모델을 일치시키는 것이다. 즉, 보편 언어를 정제해가면서 팀은 모두가 동의하는 엔티티와 로직을 설계하게 된다.
모두가 동의하는 설계를 가져야 하는 이유는 무엇인가? 나는 유지보수 비용이 적게 들기 때문이라고 생각한다. 보편 언어에는 도메인 전문가(혹은 기획자)의 지식이 충분히 들어가 있다. 이는 앞으로 어떤 변경이 발생할 가능성이 높은지에 대한 지식도 포함된다는 뜻이다. 지금은 아니지만 곧 추가될 것이 분명한 요구사항을 모두 수용하여 보편 언어를 구축할 수 있다. 따라서 팀적인 컨센서스가 맞춰진 보편 언어를 기반으로한 설계는 가장 유연할, 즉 변경에 빠르게 대응할 가능성이 높다.
꼭 “보편 언어”의 개념까지 나아가지 않더라도, 중요한 개념이나 동작에 대한 명칭은 각 팀에서 일치시키는 게 좋다고 생각한다. 커뮤니케이션 비용의 많은 부분은 상대방의 생각을 이해하는 데서 발생한다고 생각한다. 이 때 서로 핵심 개념에 대해 어떤 용어를 쓸지 미리 정의해놓으면 서로의 생각을 이해하기가 조금 더 수월해진다.
바운디드 컨텍스트와 거대한 엔티티
서비스를 유지보수하다 보면 어느샌가 비대해진 엔티티를 마주하게 된다. 새로운 기능이 필요하면 너무나도 쉽게 기존에 존재하는 엔티티에 필드를 한두 개씩 추가한다. 계속 새로운 기능을 도입하다 보면 얼마 가지 않아 거대해진 엔티티가 탄생한다.
이런 모델링 방식에 대해 아주 인상깊게 읽은 단락이 있어서 그대로 옮겨놓으려고 한다. (p.g. 122-123)
책의 수명주기가 다양한 단계로 나뉘어 나타나는 상황에서 출판 조직이 다뤄야 하는 모델링의 어려움을 생각해보자. 간단히 말하자면, 책이 이와 같은 여러 컨텍스트를 거치며 진행되는 상황처럼 출판사도 이와 비슷한 단계를 따라서 처리하게 된다.
- 책을 개념화하고 제안함
- 저자와 계약
- 책의 저작권 및 편집 프로세스 관리
- 그림 등 책의 레이아웃 디자인
- 다른 언어로 책을 번역
- 실제 인쇄 / 전자 책 출간
- 마케팅
- 대리점과 소비자에게 직접 책을 판매
- 대리점과 소비자들에게 실제 책을 배송
각 단계마다 책을 모델링하는 올바른 방법이 단 한 가지뿐인가? 절대 그렇지 않다. 각 단계마다 책은 다른 정의를 가진다. 책을 계약할 때가 돼야 임시의 책 제목을 가지며, 이는 편집하는 동안 변할 수 있다. 저술과 편집 과정에서 책은 코멘트와 수정이 포함된 초안들과 최종 원고를 거쳐간다. 그래픽 디자이너는 각 페이지 레이아웃을 만들어낸다. 인쇄 팀은 레이아웃, 인쇄 이미지를 위한 ‘청사진’, 그리고 제판을 사용한다. 마케팅은 사설이나 저작물이 필요하지 않으며, 아마 표지와 상세한 섦명 정도를 필요로 할 것이다. 배송을 위해 책은 식별자, 재고관리 위치, 남은 부수, 사이즈, 그리고 무게와 같은 정보만 갖는다.
만약 여러분이 이 수명주기의 전 단계를 관장하는 중앙 모델을 설계하려 하면 어떻게 될까? 아마 많은 혼란, 의견 불일치, 다툼이 있을 것이고 고객에게 약속한 소프트웨어는 나올 수 없을 것이다. 심지어 맞는 공통의 모델이 어쩌다 만들어졌다 해도, 모든 고객의 요구를 충족시키기 매우 어렵고 아주 가끔씩만 가능할 것이다.
이런 종류의 바람직하지 않은 혼돈을 해결하고자, 모델링을 위해 DDD를 사용하는 출판사는 각 수명주기마다 개별적인 바운디드 컨텍스트를 사용한다. 모든 바운디드 컨텍스트 하나하나는 책의 유형을 갖고 있다. 책 객체는 거의 혹은 모든 컨텍스트에 걸쳐 하나의 식별자를 공유하며, 아마도 이는 개념화 단계에서 최초로 설정됐을 것이다. 그러나 각 컨텍스트마다 책의 모델은 서로 다를 것이다. 이는 아무런 문제가 없으며, 오히려 이렇게 돼야 한다. 바운디드 컨텍스트 안에 있는 팀이 책에 관해 말할 땐 해당 컨텍스트에서 요구한 의미와 정확히 같은 의미를 나타낸다. 이 조직은 다양성에 관한 자연스러운 요구를 받아들이게 된다. 이런 긍정적인 결과가 쉽게 성취될 수 있다는 뜻은 아니다. 어쨌든 명시적 바운디드 컨텍스트를 활용하면 비즈니스적 요구를 정확히 반영하며 점진적으로 개선되는 소프트웨어를 주기적으로 배포할 수 있다.
처음부터 존재하지 않는 바운디드 컨텍스트를 기준으로 엔티티를 분리하는 것은 과한 일반화이고, 별로 바람직하지 않다고 생각한다. 하지만 조직이 성장함에 따라 바운디드 컨텍스트가 새롭게 생겨나면 그에 따라 엔티티를 쪼갤 수 있어야 하지 않을까?
비즈니스와 조직에 관심을 기울이자
도메인과 바운디드 컨텍스트가 무엇이고, 이들의 경계를 어떻게 찾아야 하는지에 대해서는 잘 이해하지 못했다. 하지만 결국 도메인과 바운디드 컨텍스트는 개발자에게 비즈니스와 조직 구조의 중요성을 강조하기 위해 생겨난 개념이라고 생각한다.
우선, 사업과 조직의 구조에 따라 코드를 분리하면 코드의 결합도를 합리적으로 낮출 수 있다. 한 팀 또는 같은 사업을 하는 팀끼리는 커뮤니케이션이 빠르고 빈번하게 이루어지지만, 그렇지 않은 팀끼리는 비교적 낮은 빈도로 커뮤니케이션이 이루어진다. 커뮤니케이션은 곧 코드상의 결합이다. 따라서 사업과 조직의 구조를 반영하여 소프트웨어 컴포넌트를 분리하면 결합도를 최소한으로 줄일 수 있다.
그렇다고 무조건 사업과 조직의 경계’만’ 따라서 소프트웨어를 분리하면 안 된다. 사업과 조직 구조는 언제든지 달라질 수 있다. 또한 소프트웨어를 나눠야 하는 이유가 반드시 결합도를 낮추기 위해서만 있는 것도 아니다. 장애 전파 방지, 독립적인 배포 사이클 및 scaling 등 다양한 기술적인 이유로도 소프트웨어를 분리할 수 있다. 어디까지나 사업과 조직의 경계는 소프트웨어 컴포넌트를 분리할 좋은 가이드라인 정도로만 삼아야 할 것이다.
다음으로, 비즈니스에 대한 이해도는 설계 퀄리티에 큰 영향을 미친다. 보편 언어 절에서 이야기했듯, 설계를 할 때 도메인 전문가의 이야기를 듣는 것은 굉장히 중요하다. 이를 통해 도메인에 대한 이해도를 높이고, 당장의 요구사항이 아닌 “앞으로 언제든 추가될 수 있는” 요구사항을 인지할 수 있기 때문이다. 개발자는 유연한 설계를 위해 비즈니스에 지속적인 관심을 기울이고 비즈니스가 어떤 구조로 이루어져 있고, 앞으로 어떤 방향으로 나아가려고 하는지를 파악하려고 노력해야 한다.
마지막으로, 회사의 핵심 비즈니스를 알아야 개발팀의 리소스를 효율적으로 사용할 수 있다. 회사는 동시에 다양한 사업을 진행하지만, 그 중에서는 회사의 존속에 아주 중요한 주력 사업도 있고, 상대적으로 덜 중요한 사업도 있다. 중요한 사업이 무엇인지, 해당 사업이 잘 되기 위한 핵심 지표가 무엇인지 아는 것은 개발팀이 올바르게 리소스를 투입하기 위해 꼭 필요하다. 더 중요한 사업에는 가장 잘하는 개발자를 투입하고, 사소한 개선보다는 핵심 지표의 개선에 많은 개발 리소스를 투자해야 한다.
아키텍처
계층 아키텍처
계층 아키텍처는 간단하지만 강력하고 효과적인 설계 방법이다. 계층이란, 같은 추상화 레벨을 가진 함수를 모아놓은 소프트웨어 컴포넌트의 집합이다. 즉, 계층 아키텍처는 소프트웨어를 몇 단계의 추상화 레벨로 분명하게 갈라놓는 것이라고 할 수 있다. 내가 생각하는 계층 아키텍처의 장점은 다음과 같다.
- 뛰어난 가독성 - 해당 계층에 존재하는 함수, 즉 동작을 모아놓으면 각 컴포넌트가 무엇을 추상화 한 건지가 더욱 명확하게 드러난다. 덕분에 코드가 컴포넌트의 역할과 전반적인 설계를 더 잘 표현하게 된다.
- 복잡도 감소 - 계층을 사용하면 의존성의 흐름과 함수의 호출 흐름이 위에서 아래로 흐른다. 덕분에 계층 아키텍처를 사용하면 컴포넌트간의 의존성을 상대적으로 덜 복잡하게 유지하고, 의존성이 과도하게 얽히는 것을 방지할 수 있다.
- 더 좋은 설계의 유도 - 계층을 나누면 아래 계층은 위 계층이 활용할 수 있는 API가 되며, 위 계층은 아래 계층의 API를 활용하는 클라이언트가 된다. 이 클라이언트(윗 계층)의 존재는 아래 계층의 인터페이스가 더 재사용성이 높은, 정제된 동작만을 가지도록 강제한다. 이는 테스트가 설계에 미치는 영향과 유사하다.
IDDD는 책 전반에 걸쳐서 두 개의 계층을 강조한다. 하나는 도메인 계층이고, 다른 하나는 어플리케이션 계층이다. 각각의 계층에 대한 IDDD의 설명은 다음과 같다.
- 도메인 계층
- 이 계층에는 정제된 도메인 지식이 살아 숨쉰다.
- 이 계층은 모든 비즈니스 로직을 처리한다.
- 어플리케이션 계층
- 이 계층의 각 함수는 유스 케이스 하나에 해당한다.
- 이 계층은 트랜잭션의 제어와 보안을 처리한다.
- 이 계층은 도메인 계층의 직접적인 클라이언트가 된다.
IDDD에서 배운 지식 중 가장 유용하게 활용했던 게 이 계층 아키텍처였다. 실제 서버 코드를 설계할 때 IDDD에 나온 것처럼 서버 로직을 도메인 계층과 어플리케이션 계층으로 분리해보았는데, 복잡한 코드가 깔끔하게 정리됐다. 어플리케이션 계층에는 어떤 어플리케이션 시나리오가 존재하는지가 분명하게 표현되어 있으며, 도메인 계층에는 여러 어플리케이션 시나리오에서 재사용할 수 있는 정제된 도메인의 동작만이 남는다. 어플리케이션 계층의 함수는 적절한 도메인의 동작을 조합하여 유즈 케이스에 맞는 비즈니스 가치를 달성한다.
도메인 계층-어플리케이션 계층 설계를 직접 적용할 때 발생하는 가장 어려운 문제는 어떤 함수가 어떤 계층에 속해야 하는지를 판단하는 일이었다. 특히나 도메인 계층이 그러한데, 도메인 지식이나 비즈니스 로직과 같은 말이 너무나도 추상적이어서 별 도움이 안 되기 때문이다. 그래서 개인적으로는 다음과 같은 규칙으로 설계를 하고 있다.
- 우선 어플리케이션 계층의 함수 목록을 정의해야 한다. 여기에는 다음과 같은 세 가지 타입의 메소드가 포함된다.
- 컨트롤러에서 호출되는 함수
- batch job에서 실행되는 함수
- 다른 도메인의 이벤트를 핸들링하기 위해 호출되는 함수
- 어플리케이션 계층의 함수가 공통으로 사용해야 하는 동작을 뽑아내고, 이를 도메인 계층의 함수로 정의한다.
즉, 비교적 개념이 잘 와닿는 어플리케이션 계층을 먼저 정의하고, 도메인 계층의 함수 = 어플리케이션 계층의 함수가 필요로 하는 동작으로 정의하고 있다.
CQRS와 이벤트 소싱
이 둘은 예전부터 궁금했던 내용이었는데, IDDD에서 마주쳐서 신기했다. 새롭게 공부한 개념이기에 따로 항목을 할당해서 가볍게 기록해두려고 한다.
CQRS는 Command-Query Responsibility Segregation(커맨드-쿼리 책임 분리)의 약자이다. 커맨드는 객체의 상태를 바꾸는 행동이고, 쿼리는 객체의 상태를 변경하지 않고 조회만 해오는 행동이다. CQRS는 말 그대로 커맨드와 쿼리의 책임을 데이터베이스 상에서 분리하는 것이다. CQRS 패턴을 사용하면 커맨드를 저장하는 모델과 각 뷰에서 보여줄 데이터를 위한 여러개의 쿼리 모델이 독립적으로 존재한다. 하나의 커맨드가 실행되면 각 쿼리 모델이 적절하게 업데이트된다. 클라이언트에서 특정 뷰가 켜지면 해당 뷰를 위한 쿼리 모델이 조회된다.
CQRS는 일반적인 어플리케이션에는 그다지 적합하지 않을 수 있다. 많은 어플리케이션에서는 여러 개의 쿼리 모델이 필요하지 않다. 적당히 엔티티를 조인해서 사용하면 된다. 이러한 경우 커맨드 모델과 쿼리 모델을 분리하고, 커맨드 모델이 추가될 때마다 이를 필요한 쿼리 모델에 반영하는 건 단순히 복잡성만 늘리는 행위이다. 그렇다면 CQRS 패턴은 언제 유용한가? CQRS 패턴은 복잡한 뷰가 많을 수록 효과를 발휘하는 패턴이다. CQRS 패턴을 사용하면 특정 뷰를 위한 DTO를 구성하려고 할 때 수많은 엔티티의 복잡한 조인을 수행할 필요 없이 뷰에 최적화된 쿼리 모델 하나만 조회하면 되기 때문이다.
이벤트 소싱이란 엔티티 - 객체의 최종 상태 - 가 아니라 이벤트 - 객체에게 발생한 조작 - 를 중심으로 데이터를 모델링하는 방식이다. 이벤트 소싱은 비즈니스가 데이터의 최종 상태 뿐만 아니라 데이터가 변경된 히스토리를 알아야 할 필요가 있을 때 빛을 발한다. 모든 이벤트가 데이터베이스에 저장되어 있기 때문이다. 하지만 이벤트 소싱의 문제점은 히스토리가 아니라 최종 상태를 조회하고 싶을 때 발생한다. 최종 상태를 조회하기 위해서는 모든 이벤트를 들고 와서 처음부터 하나하나 적용하는 과정이 필요하다. 이런 문제를 방지하기 위해 이벤트 소싱은 보통 CQRS와 함께 사용한다. 즉, 이벤트를 커맨드처럼 저장하고, 최종 상태를 조회할 때는 쿼리 모델을 조회하게 만든다. 이러한 특성 때문에 IDDD에서 저자는 CQRS는 이벤트 소싱 없이도 사용할 수 있지만, 그 반대는 보통 실용적이지 못하다고 이야기한다.
전술적 설계
분산 시스템
책을 읽다가 놀란 한 가지 포인트는 IDDD의 어마어마한 분량이 분산 시스템에 대한 내용이라는 점이었다. 그만큼 DDD에서 분산 시스템 개발이 중요하다는 뜻일 것이다. IDDD에서 이토록 분산 시스템을 강조하는 이유는 DDD의 구현이 자연스럽게 분산 시스템으로 귀결되기 때문이다. 각각의 바운디드 컨텍스트는 서로 다른 트랜잭션에서 자신의 작업을 처리하는데, 이 때문에 바운디드 컨텍스트가 통합된 하나의 어플리케이션은 필연적으로 분산 시스템이 된다. 분산 시스템 환경에서의 개발은 상당히 어렵지만, 그만큼 각 도메인의 개발을 자율적으로 실행할 수 있고, 높은 확장성과 유연함을 가질 수 있다는 장점이 있다.
책에서 지속적으로 등장하는 분산 시스템 패턴은 도메인 이벤트를 기반으로 한 서버간 연동이다. 도메인 이벤트란 말 그대로 도메인에서 어떤 이벤트가 발생했을 때 해당 도메인이 내/외부로 발행시키는 이벤트이다. 해당 이벤트를 다른 서버가 구독해서 처리하고 싶은 경우, 어떤 방식으로든 한 서버의 도메인 이벤트가 네트워크를 통해 다른 서버로 전파되어야 한다. 이 때 이 전파는 언제든지 실패할 수 있다. 이를 방지하는 방법은 이벤트의 전파를 at least once 방식으로 구현하고, 해당 이벤트의 클라이언트는 이벤트 핸들러를 멱등하게 구현하는 것이다.
흥미롭게 읽었던 부분은 장기 실행 프로세스 부분이었다. 장기 실행 프로세스란 여러 개의 서버에 걸쳐서 실행되어야 하는 프로세스를 의미한다. 즉, 도메인 이벤트를 적극적으로 활용하는 아키텍처에서는 일반적으로 다음과 같은 흐름이 될 것이다.
- 유저가 장기 실행 프로세스를 서버 A에 요청한다.
- 서버 A는 장기 실행 프로세스가 시작되었다고 기록하고, 이벤트 1을 발행한다.
- 서버 B가 이벤트 1을 수신하고 필요한 작업을 수행한 뒤 이벤트 2를 발행한다.
- 서버 C가 이벤트 2를 수신하고 필요한 작업을 수행한 뒤 이벤트 3을 발행한다.
- 서버 A가 이벤트 3을 수신하고 장기 실행 프로세스가 완료되었다고 기록한다.
이 내용이 흥미로웠던 이유는, 지금까지 외부 API를 연동하면서 이런 패턴의 구현을 이미 많이 활용하고 있었기 때문이다. 이런 패턴을 사용할 때 일반적으로 주의할 점 - 재시도, 멱등성과 이를 위한 이벤트 ID 할당, ACK, timeout 및 알람 - 에 대해 내가 몸으로 익힌 것과 책에서 이야기하는 주의사항이 크게 다르지 않아 안심했고, 명확한 어휘를 익혀서 좋았다.
고정자, 트랜잭션적 일관성 & 결과적 일관성
비즈니스 로직을 작성하다 보면 반드시 같이 일어나야 하는 엔티티의 변경이 존재한다. 간단한 예시로, 상태의 변경과 상태 변경이 발생한 시각의 기록은 반드시 같이 이루어져야 한다. 타다의 예시를 들면, 유저가 내리는 순간 상태가 하차 완료로 바뀌고, 내리는 위치와 시각이 기록되어야 하며, 드라이버의 상태를 운행 중에서 미운행 중으로 변경시켜야 한다. 이 모든 변경은 반드시 함께 일어나야 하는 것이다. 이처럼 일관적으로 유지되어야 하는 비즈니스 규칙을 IDDD에서는 고정자(invariant)라고 부른다.
고정자는 트랜잭션을 통해 구현할 수 있다. 같이 발생해야 하는 변경사항을 하나의 트랜잭션에서 수행하면 트랜잭션에 의해 모든 순간 고정자가 지켜짐을 보장받는다. 이렇게 트랜잭션에 의해 동기적으로 보장받을 수 있는 일관성을 트랜잭션적 일관성이라고 부른다.
한편, 서로 함께 일어나는 변경이지만 반드시 동시에 일어날 필요가 없는, 즉 비동기적으로 일어나도 괜찮은 변경이 있다. 이런 변경은 같은 트랜잭션에서 일어나지 않아도 괜찮다. 대신, 도메인 이벤트나 배치 업데이트 등의 기법을 활용하여 언젠가는 일관성이 지켜지도록 만들어야 한다. 이렇게 비동기적으로 지켜지는 지연된 일관성을 결과적 일관성이라고 한다.
고정자와 트랜잭션적 일관성 / 결과적 일관성 내용은 개념적으로는 간단했지만, 내게 중요한 깨달음을 몇 가지 안겨 주었다.
첫 번째로, 트랜잭션의 의미에 대해 다시 생각해보는 계기를 제공해주었다. 나는 지금까지 특별한 이유가 없으면 기계적으로 한 API 당 하나의 트랜잭션을 걸었다. 여기에 대해 아무런 의문을 품지 않았고, 트랜잭션을 쪼개야 한다는 생각을 가지지 않았다. 하지만 고정자와 일관성의 관점에서 트랜잭션을 바라보니 ‘한 API 당 한 트랜잭션’이라는 규칙은 무척이나 부자연스러운 것이었다. 트랜잭션의 경계는 API의 역할이 아니라 고정자가 무엇인지, 그리고 비즈니스 로직의 일관성이 얼마나 오래 깨져 있어도 되는지에 의해 결정되어야 하는 것이었다.
다음으로, 새로운 엔티티 설계 방법론을 배웠다. IDDD에서 저자는 트랜잭션의 경계가 곧 애그리게잇의 경계라고 말한다. 반드시 하나의 트랜잭션으로 묶여야 하는 고정자가 아닌 이상 같은 애그리게잇으로 설계될 필요가 없다는 뜻이다. 이를 읽고 타다 서버의 엔티티를 생각해보니, 거대한 엔티티를 가르는 선명한 경계가 여러 개 보였다. 엔티티와 트랜잭션을 나누는 게 무조건 좋은 일은 아니고, 지금 당장 이들을 고치려는 것도 아니지만, 앞으로 엔티티가 너무 많은 책임을 가지게 되는 것을 방지하는 데에 큰 도움이 될 지식을 익혔다.
마지막으로, 모든 것이 반드시 트랜잭션적 일관성에 의해 보장받을 필요는 없다는 사실을 깨달았다. 타다 서버는 대부분의 로직이 트랜잭션적 일관성을 활용하여 작성되어 있고, 결과적 일관성은 몇몇 외부 API 연동을 할 때 정도만 사용하고 있다. 그렇기에 트랜잭션적 일관성이 제공해주는 편리함에 상당히 익숙해져 있는데, 이 때문에 기획 / 개발을 할 때 결과적 일관성이라는 옵션에 대해 아예 고려하지 않았다. 하지만 일관성에 대한 내용을 읽고 나니 트랜잭션적 일관성이 아니라 결과적 일관성이 지켜지도록 개발했으면 더 편했을 기능이 여럿 떠올랐다. 앞으로는 트랜잭션적 일관성의 편리함 뿐만 아니라 결과적 일관성의 낮은 결합도 / 높은 확장성을 염두에 두고 개발적인 결정을 내릴 수 있을 것 같다.
생각들
Spring과 DDD의 관계 - Spring을 올바르게 사용하는 패턴?
예전에 Spring을 사용하다가 우연히 봤는데, @Repository
와 @Service
어노테이션의 javadoc을 보면 “Eric Evans의 DDD에서 정의한 개념을 지칭한다”라는 말이 써져 있다. 나는 Spring을 사용한지 2년이 지나서야 Spring이 DDD의 전술적 설계 방식을 어느 정도 표방하고 있다는 것을 처음 알게 됐다. 사실 IDDD를 읽게 된 계기 중 하나가 바로 여기서 나왔다. Spring이 DDD의 개념을 채택한 것이라면, DDD를 읽으면 Spring을 조금 더 올바르고 적합한 패턴으로 사용할 수 있지 않을까?
IDDD를 읽으면서 Spring을 사용하는 best practice를 나름대로 만들어보려고 했고, 지금 내가 생각하는 좋은 패턴들은 다음과 같다.
- 서비스는 도메인 레이어와 어플리케이션 레이어로 분리해서 사용한다.
- 레포지토리는 전역에서 접근 가능하게 설계된 저장소이기 때문에 서비스로 감싸지 않고 사용해야 한다.
- 레포지토리는 엔티티에서는 사용하지 말고, 서비스에서만 사용해야 한다.
- 하나의 엔티티만 포함하는 동작은 서비스가 아니라 엔티티에 있는 것이 좋다.
- 트랜잭션은 어플리케이션 서비스 레이어에서 조절해야 한다.
단순하다. 하지만 단순하다고 지키기 쉬운 것은 아니다. 그리고 단순하다고 효과가 적은 것도 아니다. 내 경우, 타다 코드 베이스의 컨벤션과 맞지 않는 것도 있어서 일단은 서비스를 도메인 레이어와 어플리케이션 레이어로 분리하는 것 정도만 적용하고 있다. 이거 하나를 지키는 것조차 굉장히 어렵다. 레이어를 나누기 위해서는 많은 고민이 필요하기 때문이다. 하지만 많은 고민이 녹아든 만큼 이를 제대로 지켰을 때 얻는 설계적 이득도 엄청나다는 것을 느끼고 있다.
또한, IDDD를 읽으면서 새롭게 생기거나 해소되지 않은 의문점들도 있다.
- 왜 Spring은 DDD의 많은 개념 중에서
@Repository
와@Service
개념만 채택한 걸까?@Aggregate
어노테이션은 왜 안 만들었을까? - Spring을 사용하면서 anemic domain model에 빠지지 않는 방법이 있을까? DI로 떡칠된 Spring 환경에서 도메인 모델에 로직을 넣는 것이 너무 어렵게 느껴진다.
스타트업과 DDD?
DDD는 훌륭한 통찰을 갖춘 설계 방법이라고 생각하지만, 내가 IDDD에서 읽은 그대로가 스타트업과 잘 어울리는지는 물음표가 붙는다.
첫 번째, 결과적 일관성이 스타트업에 잘 어울리는가?
DDD에서 기술적으로 가장 중요한 한 가지를 꼽으라고 한다면, 나는 도메인 이벤트를 기반으로 한 결과적 일관성을 꼽을 것이다. 위에서도 이야기했지만, IDDD에서는 분산 시스템 환경에서 결과적 일관성을 올바르게 달성하기 위한 방법을 논의하기 위해 어마어마한 분량을 할애하고 있다. 바운디드 컨텍스트와 어그리게잇은 서비스를 자연스럽게 분산 시스템으로 이끈다. 그리고 분산 시스템에서는 결과적 일관성을 통해 각 컴포넌트를 통합해야 한다.
하지만 이 결과적 일관성이라는 개념이 스타트업과 잘 맞는지 의문이 든다. 스타트업은 유연함과 속도가 생명이다. 지금 당장 product-market fit을 찾아야 하고, 지금 당장 수 배 성장해야만 살아남을 수 있다. 결과적 일관성은 장기적인 확장성과 유연성을 제공하긴 하지만, 단기적으로 빠른 개발을 하기에는 부적합한 설계 방식이다. 도메인 이벤트 처리를 위한 시스템을 구축해야 하고, 트랜잭션이 있었다면 발생하지 않을 기술적 예외 상황을 다뤄야 하며, 예상하지 않은 방향으로 요구사항이 급격하게 변화하는 것에 대응하기가 더 어렵다.
하지만 DDD에서 가르쳐주는 지혜 - 엔티티, 값 객체, 서비스, 레포지토리, 애그리게잇, 도메인 이벤트 - 는 더할나위 없이 훌륭하다. 여기서 할 수 있는 최선은 우선 트랜잭션적 일관성을 활용하되, 확장성과 유연성이 필요한 시점에 결과적 일관성으로 옮겨가기 편한 구조로 유지하는 것이라고 생각한다. 애그리게잇과 엔티티는 설계 규칙에 따라 엄격히 분리시키고, 도메인 이벤트를 발행해야 하는 시점에 다른 바운디드 컨텍스트의 어플리케이션 서비스를 바로 호출하게 구조를 유지한다. 나중에 확장성이 필요하여 서비스를 분리하고 결과적 일관성을 도입해야 하면 pubsub 시스템을 도입하고, 도메인 이벤트를 발행하도록 변경하고, 각 이벤트에 대한 핸들러에서 어플리케이션 서비스를 호출하도록 변경한다.
두 번째, 스타트업에서의 도메인 전문가는 누구인가?
이 질문은 DDD를 읽기 전에 내 동료가 해준 말인데, 아직까지도 머리에 맴돌고 있는 질문이다. 예전에는 타다의 도메인 전문가가 PM팀과 운영팀이라고 생각했다. 하지만 최근에 프로덕트 쪽에 가까운타다, 바로대리 등 기존에는 없던 기능을 만들면서 타다에 도메인 전문가가 없을 수도 있다는 생각이 들었다. 아무도 모르는 새로운 상품을 만들어서 판다면, 그 상품의 도메인 전문가가 과연 존재할까?
도메인 전문가가 없더라도, 도메인에 대한 이해도가 중요하지 않은 것은 아니다. 풍부한 도메인 지식이 훌륭한 설계를 이끌어내기 때문이다. 그렇기에, 오히려 도메인 전문가가 없으면 우리는 더 많은 대화를 나눠야 한다. 수많은 시나리오를 떠올리고, 도메인이 어떻게 생겨먹은 건지에 대한 심도 있는 토의를 거쳐서 도메인 지식을 축적해야 한다.