Domain Driven Desgin의 러닝 커브는 높다.
지난 2~3년을 DDD, OOP, MSA에 대해 고민하고, 적용하고, 레퍼런스들을 열심히 읽었지만,
누군가 “그래서 DDD/OOP가 뭔데?” 라고 물어보면 선뜻 대답하기 힘들다.
많이도 혼란스러웠고, 지금도 혼란스럽고, 앞으로도 혼란스러울것이다.
공부를 거듭하면서 앞으로의 생각은 계속 바뀌겠지만, 지금 가진 생각을 정리했다.
응집성과 결합도
응집력 있는 도메인 설계가 시스템의 다른 부분과 느슨하게 결합될 수 있다면
아마 그러한 아키텍처는 도메인 주도 설계를 지원할 수 있을 것이다
- 도메인 주도 설계 4장, 에릭 에반스
높은 응집성과 낮은 결합도는 어떠한 방법론, 디자인 패턴, 아키텍처를 평가하는 도구가 될 수 있다.
TDD의 “한 번에 하나만 테스트”,
Unix 철학 중 하나인 “각자 일을 잘 수행하는 작은 조각”,
등은 높은 응집성과 낮은 결합도를 충족한다.
응집도와 결합도가 중요한 이유는
- 수정이 필요한 코드 범위가 작아진다.
- 인지부하가 줄어든다.
- 의존하는 객체의 세부 구현을 잠깐 잊고 있어도 된다.
따라서 프로젝트가 커질 수록 높은 응집성과 낮은 결합도는 매우 중요하다.
상속과 믹스인 사용 멈춰!
Favor object composition over class inheritance. - GoF 디자인 패턴
객체 합성은 높은 응집성을 가진 객체를 낮은 결합도를 가지고 상호작용하기 위한 방법이다.
Spring, Django 같은 서버 프레임워크를 현란하게 사용해 본적이 있는가?
제공되는 클래스와 믹스인을 현란하게 상속 받고,
매우 긴 이름의 repo 메소드를 여러개 만들고 나면, 당시엔 굉장히 뿌듯하다.
내부 구조를 더 잘 이해해서 최대한 적은 양의 코드로 기능을 구현하는 것이 목표가 된다.
하지만 시간이 지나 요구 사항이 복잡해져, 프레임워크가 설계한 상속의 범주를 벗어나면 코드 수정이 매우 힘들어진다.
또한 나중에 프레임워크의 내부 구조를 까먹으면 다시 프레임워크의 방대한 코드를 확인해야한다.
상속을 이용해 코드를 재사용하는 것을 화이트박스 재사용이라고 한다.
블랙박스와는 반대로 내부 구조를 알고 있어야 한다는 뜻이다.
많은 코드를 봐야 한다는 것은 곧 낮은 응집도를 뜻한다.
“상속과 믹스인으로 제공하려던 기능들을, 별도의 클래스들로 분리하라”
분리한 클래스는 인자로 받아 활용하자.
이때 구현 클래스가 아닌 인터페이스에 의존한다면,
객체의 세부적인 구현을 몰라도 객체를 재사용할 수 있다.
따라서 객체 합성을 통한 재사용을 블랙박스 재사용이라고 한다.
blog_creator = BlogCreator(KafkaEvnetPublisher())
blog_creator.create(req)
blog_creator = BlogCreator(RabbmitEvnetPublisher())
blog_creator.create(req)
BlogCreator 관점에선 BlogCreated 이벤트가 어디에 Publishe 되는 것은 중요하지 않다.
EventPublisher 인터페이스를 구현한 객체들 받아 적절히 호출하는 것만 신경쓰면 된다.
인터페이스가 핵심이다
인터페이스가 곧 객체 관계이므로 매우 중요하다고 할 수 있다.
또한 인터페이스가 일단 정의되어 여러 곳으로 퍼지고 나면 수정이 힘들기 때문에 신중히 결정해야한다.
인터페이스는 항상 고민하는 영역이다.
도움이 되었던 생각들은 다음과 같다.
- 객체의 상태가 아닌 해야할 행동을 기반으로 정의한다.
- 일반적으로 객체 하나에 퍼블릭 인터페이스 하나를 권장한다.
- 해당 객체에 다형성이 필요할 때 불필요한 기능들을 구현하지 않아도 되도록
- 객체에 비슷한 메소드가 많아지고 네이밍이 현란해진다면 다형성을 고민한다
- 매개변수 타입은 만들기 쉬울수록 좋다.
- 원시 타입 > 엔티티 > DTO
- HTTP, Grpc, Worker, Test 등 실행 환경에 의존성이 없는게 좋다
- 패키지를 구성할 때부터 api, worker, batch, domain 등으로 분리해서 domain에는 외부 의존성이 없는 코드를 작성하는 것도 좋다
- openapi, protobuf 같이 스펙을 공유하고 강제할 수 있는 도구를 쓰자
- kafka, rabbitmq 같은 큐 메시지에도 스펙을 강제하면 좋다
- 엔티티 응답을 내려줄 땐 엔티티 이름을 키로 한 번 감싸는 걸 권한다
- Ex:
{"comment": {"id": 1, "body": "안녕하세요"}} - 어떤 요구사항이 추가될지 모르기 때문에 단순한 CRUD라도 이렇게 하는 게 좋다.
- 댓글 작성 시 올바른 커뮤니티 조성을 위해 가이드를 내려주는 요구 사항이 생길 수도 있다. 이때 {“comment”: Comment, “guide”: Guide}로 내려주는 편이 자연스럽다. 그러므로 처음부터 엔티티 이름으로 감싸자.
- Ex:
- Errors 필더에는 에러 타입을 키로, 에러 핸들링에 필요한 데이터를 값으로 넣자.
- Ex:
{"errors": "noEnoughMoney": {"currentMoney": 2000}} - 스펙을 작성할 때 타입을 명시하기 편하다.
- Ex:
- 비슷하게 생긴 코드라도 함께 변경하면 안 된다면 중복 코드가 아니다.
- 중복을 제거하는 이유는 코드 변경이 필요할 때, 누락되는 코드가 없도록 하기 위함이다.
- “사람도 전기자극으로 움직이니까 컴퓨터” 같은 식의 추상화가 되지 않도록 주의해야한다.
계층형 아키텍처가 아니야!
계층 아키텍처를 사용하고 있다고 말하는 많은 팀은 실제론 핵사고날을 사용하고 있다 - 도메인 주도 설계 구현 4장, 반 버논
계층형 아키텍처에선 아래 계층이 상위 계층을 참조하면 안 된다.
그러한 관점에서 Repository는 이상하고 볼 수 있다.
상위 계층인 도메인 모델을 반환하기 때문이다.
이를 정당화하기 위해 주장하는 것이 DIP이다.
이게 잘 와닿지 않아서 이런저런 생각이 참 많았다.
핵사고날 아키텍처 관점에서 이를 설명 할 수 있다.
계층형이 “UI -> application -> domain -> Infra” 모델을 제안했다면,
핵사고날은 이를 대칭적으로 접어서, UI와 Infra를 외부(OUTSIDE)로 정의한다.
따라서 Outside → (Application → Domain) 꼴이 된다.
애초에 Repository가 Domain 보다 위에 위치하게 되므로 Domain Model을 참조하는 것이 문제 되지 않는다.
그러나 사실 계층이 어쩌고는 크게 중요한 것은 아니라고 생각한다.
앞서 말했듯 인터페이스가 중요하다.
그런데 DDD에서 더 중요한 것은 도메인 계층의 인터페이스다.
따라서 실제 구현이 어떤 계층에 있든 기술 중심이 아닌 비즈니스 중심으로 기술하는 것이 중요하다.
도메인 계층을 기준으로 인터페이스가 구성되었다면, 그 구현이 아래 계층에서 발생하는 것은 문제가 되지 않는다. 심지어는 위의 계층에 구현체를 만들어도 무방하다.
도메인 모델에서 도메인 로직을 작성하는 데 어려움을 느꼈다.
이것은 도메인 로직 구현이 Pure 해야 한다는 오해 아닌 오해에서 비롯되었다.
확실히 도메인 모델은 Isolation된 환경을 가지고 있어야 한다.
도메인 서비스도 가능하면 외부에 의존하지 않는 것이 좋지만, 필요에 따라서 네트워크 호출을 할 수 있다.
앞서 반복적으로 이야기 한 것처럼 중요한 것은 인터페이스기 때문이다.
오히려 네트워크 호출을 하지 않으려고 메소드 인자에 이것저것 받아서 인터페이스가 더러워지는 경우를 피해야 한다.
그러나 도메인 모델에서 직접 네트워크 호출을 하는 것은 좋지 않다.
보통 도메인 모델에 다형성을 포함하지 않기 때문이다.
이러면 대표적으론 모델 테스트가 외부에 의존하기 때문에 곤란해진다.
그러므로 도메인 모델에서 네트워크 호출이 필요하다면, 우선 나는 필요한 부분을 도메인 서비스로 분리한다.
그리고 도메인 모델 메소드의 인자로 도메인 서비스를 주입 받아 처리한다.