읽는데 약 3분
도메인 주도 개발 적용 경험
안녕하세요? 이번 시간에는 도메인 주도 개발 시작하기를 읽으며 배웠던 내용과 실제로 프로젝트에 어떻게 적용했는지 공유하고자 합니다.
스프링을 학습하고 첫 프로젝트를 진행할 때는 ORM 기술 덕분에 데이터베이스와 객체 사이의 불일치를 편리하게 해결했습니다. 이 과정에서 다른 애그리거트를 필드를 통해 참조했습니다.
애그리거트를 직접 참조할 때 발생할 수 있는 가장 큰 문제는 편리함을 오용할 수 있다는 것입니다. 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 됩니다. 이는 트랜잭션 범위와 관련된 문제를 야기할 뿐만 아니라 SRP 원칙도 위반하여 책임 분리가 제대로 이뤄지지 않습니다.
가장 중요한 문제는 확장입니다. 만약 사용자가 늘고 트래픽이 증가하면 자연스럽게 부하 분산을 위해 하위 도메인 별로 시스템을 분리합니다. 이 과정에서 도메인마다 서로 다른 DBMS를 사용할 때도 있습니다. 이 경우는 JPA와 같은 단일 기술을 사용할 수 없게 됩니다.
ID를 이용한 설계의 장점
따라서 ‘디클’ 프로젝트에서 도메인 주도 개발을 적용해 애그리거트를 설계했습니다. 프로젝트에서 중요한 애그리거트 중 하나인 Post.kt
를 살펴보겠습니다. User
애그리거트를 객체로 참조하는 것이 아니라 PK인 userId
로 참조하는 것을 볼 수 있습니다.
ID 참조를 사용하면 어떤 장점이 있을까요? 첫 번째는 구현 복잡도가 낮아집니다. 다른 애그리거트를 직접 참조하지 않으므로 애그리거트 간 참조를 지연 로딩으로 할지 즉시 로딩으로 할지 고민하지 않아도 됩니다.
위 코드는 하나의 Post와 관련된 댓글(Comment)과 대댓글(Reply)을 불러오는 코드입니다. Comment
와 Reply
를 다른 애그리거트로 분리했습니다. 분리하는 기준은 후술하겠습니다. 애그리거트가 필요한 시점에 응용 서비스에서 로딩하면 되므로 지연 로딩과 동일한 효과를 갖게 됩니다.
위 사진에서 댓글을 전부 조회한 후(1), 필요한 시점에 In
을 활용해 대댓글을 조회한 모습(2) 입니다. 처음 JPA를 사용하면 각 객체 간 모든 연관을 지연 로딩 또는 즉시 로딩으로 처리하고자 합니다. 하지만 두 개의 애그리거트 A, B가 딱 잘라 하나의 유형에 속하지 않을 수도 있습니다. 어떤 요구사항에서는 즉시 로딩이 어울릴수도, 지연 로딩이 어울릴 수도 있기 때문입니다.
경계 설정의 기준
그렇다면 애그리거트 간 구분하는 기준은 무엇일까요? 책에서는 다음과 같이 설명합니다.
동일한 라이프 사이클을 판단할 때는 Actor를 기준으로 생각해도 좋습니다. 흔히 ‘A’가 ‘B’를 갖는다. 라는 요구사항이 있다면 ‘A’와 ‘B’를 하나의 애그리거트로 묶어서 생각하기 쉽습니다.
‘댓글’은 ‘대댓글’을 갖지만 과연 같은 애그리거트 일까요? 댓글과 대댓글을 생성하는 Actor는 다를 뿐만 아니라 라이프 사이클 또한 다릅니다. 댓글이 존재한다고 해서 대댓글이 무조건 존재해야하는 것도 아니고 댓글이 삭제된다고 해서 해당 대댓글이 삭제되어야 하는 것도 아니기 때문입니다.
값 객체의 장점
도메인 주도 개발을 접하면 알게되는 또 다른 단어는 값 객체입니다. 간단하게 용어 정리를 하겠습니다.
- 엔티티 : 고유한 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 도메인 모델의 데이터를 포함한다.
- 밸류 : 고유한 식별자를 갖지 않는 객체. 개념적으로 하나인 값을 표현할 때 사용된다.
- 애그리거트 : 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다.
여기서 밸류와 값 객체는 같은 의미입니다. 프로젝트에서 사용한 값 객체는 대표적으로 Like
가 있습니다.
항상 코딩에 앞서 요구사항을 올바르게 이해하는 것이 중요합니다. 좋아요와 관련된 요구사항은 세 가지 였습니다.
- 사용자는 하나의 게시글에 한 번만 좋아요를 누를 수 있다.
- 한 번 누른 좋아요는 취소할 수 없다.
- 사용자는 자신의 게시글에 좋아요를 누를 수 없다.
첫 번째 요구사항을 만족하기 위해서는 각 게시글마다 어떤 사용자가 좋아요를 눌렀는지 저장하고 있어야합니다. 게시글과 좋아요가 일대다 관계를 맺게되며 구현하는 방법은 @OneToMany
또는 @ElementCollection
을 사용하는 방법이 있습니다.
ElementCollection
을 사용할 때 주의할 점은 식별자 개념이 없으므로 변경사항이 생기면 관련된 모든 행을 삭제하고 다시 삽입하게 됩니다. 좋아요 요구사항에서는 변경사항이 발생할 일도 없을 뿐만 아니라, 한번 INSERT된 이후 행을 삭제할 일이 없으므로 @ElementCollection
을 사용했습니다.
사용하며 느꼈던 장점은 다음과 같습니다.
- 타입이 의미를 가지므로 코드를 이해하는데 도움이 된다.
- 값 객체를 위한 기능을 추가할 수 있다.
단순하게 Long
타입으로 저장한다면 List<Long>
만 보고 다른 개발자가 즉각적으로 파악하기는 어렵지만 List<PostLike>
를 보면 좋아요를 담은 리스트라는 것을 한 눈에 파악할 수 있습니다.
뿐만 아니라 좋아요의 갯수는 몇 개인지 알려주는 count()
또는 새로운 사람이 좋아요를 눌렀을 때 리스트에 추가하는 add()
처럼 값 객체만을 위한 기능을 추가할 수 있습니다. 이를 통해 도메인 로직이 분산되지 않고 한 곳에 모여있을 수 있게 됩니다.
정리
위 내용이 도메인 주도 개발의 모든 내용을 포함하진 않습니다. 각 레이어의 책임, 트랜잭션 범위, 바운디드 컨텍스트 등 여러 개념들을 다룹니다. 프로젝트에서 적용했던 개념 중 아주 일부만 소개한 것이므로 규모가 큰 집단에서 적용하면 더 좋은 아키텍처를 가져갈 수 있을 것 같습니다.