블로그
도메인 주도 설계: 소프티어 개발자의 DDD 걸음마 떼기
- 등록일
- 2023-11-01 13:50:47
- 조회수
- 1,495
안녕하세요. Softeer 백엔드 개발자 송제영입니다.
Domain Driven Design (도메인 주도 설계) 이라는 말은 참 많이 들었습니다. 관련한 책도 읽어 봤지만 그 깊이와 양으로 인해 그리고 추상적인 표현으로 인해 그 내용을 깊이 공감하기는 다소 어려웠습니다. 특히 실제로 어떻게 적용하는지 경험할 기회가 없었습니다. 이 글에서 저는 실제로 저희 소프티어 리팩토링을 진행하며 DDD를 적용해 본 경험을 들어 -다소 소박하지만- 어떤 부분이 다른지, 왜 이렇게 하는지를 도메인 주도 설계 관점에서 기술하려고 합니다.
배경
Softeer는 현재 작동 중인 코드의 많은 문제를 개선하고 더 좋은 서비스가 되기 위해 바닥부터 다시 만드는 작업을 진행 중입니다. 현재 코드의 주요 문제점 중 하나는 바로 유지보수가 어려운 것입니다. 유지보수가 어려운 원인은 다양하지만 DDD를 통해 그 중 많은 부분을 개선할 수 있을 것이라고 생각합니다.
Domain Driven Design
DDD 에는 정말 많은 개념이 등장하는데요. 다 알기는 어렵기 때문에 먼저 꼭 알아야 하는 도메인 주도 설계의 핵심 개념을 간단히 알아봅시다.
Domain
도메인이란 해결하고자 하는 문제입니다. 요구사항으로 표현될 수 있습니다.
Domain Model
도메인 모델은 도메인을 개념적으로 표현한 것입니다. 소프트웨어의 변경은 곧 도메인 모델의 변경입니다. 모든 소프트웨어 개발자는 도메인 모델러라고 할 수 있습니다.
Ubiquitous Language
보편 언어는 도메인 전문가와 개발자 간에 공통으로 (일치하게) 사용하는 언어를 말합니다. 도메인을 설명하고 도메인 모델을 기술할 때 사용되는 언어입니다. 보편 언어는 풍부하고 정교해야 하며, 도메인이 바뀜에 따라 함께 바뀌어야 합니다.
Entity
엔티티는 식별할 수 있는 데이터 모델입니다. 예를 들어 사용자 같은 것입니다. 고유의 로직(행위, 메서드)을 가질 수 있습니다.
Value Object
값 객체는 Immutable 한 (변경할 수 없는) 스냅샷 성격의 데이터에 대한 모델입니다. 예를들어 배송지 같은 것입니다. 배송지가 변경 가능하다고 생각할 수 있지만 사실 배송지를 바꾸기 위해서는 새 배송지 값을 만들면 그만입니다. 핵심은 엔티티와는 다르게 값 객체는 식별할 필요가 없다는 것입니다. (물론 도메인에 따라 배송지가 엔티티일 수도 있습니다.) 엔티티와 마찬가지로 고유의 로직을 가질 수 있습니다.
Aggregate
애그리거트는 도메인 룰을 지키는 (불변식을 만족하는) 가장 작은 Entity(+ Value Object)의 묶음입니다. 일관성이 유지되어야 하는 (한 트랜잭션으로 묶이는) 단위입니다.
Aggregate Root
애그리거트 루트는 외부에서 유일하게 직접 접근 가능한 (Visible한) Entity입니다. 애그리거트를 캡슐화합니다. 이를 통해 애그리거트의 도메인 룰을 지킬 수 있습니다.
Bounded Context
바운디드 컨텍스트는 유효하고 일관성 있게 관리되어야 하는 도메인 단위입니다. 관심사를 분리하여 원치 않은 의존성을 방지하고 각 부분을 독립적으로 개발하기 위해서 그러한 경계를 짓는다고 생각하면 좋습니다. 마이크로 서비스 아키텍처에서 배포의 단위라고 할 수 있습니다.
Bounded Context를 지킨다는 것이 무엇일까요. 같은 데이터소스를 참조하는 Entity라고 하더라도 Context 마다 관심사(관심있는 속성)가 다릅니다. 따라서 Context 마다 관심사에 따른 Entity를 설계하여 사용할 수 있습니다. 예를 들어 게시판 Context에서 필요한 User 속성(사용자명 등)과 평가 Context에서 필요한 User 속성(휴대폰 번호 등)은 다른 것처럼 말입니다.
Domain Service
도메인 서비스는 도메인 로직을 캡슐화한 것입니다. 엔티티나 값 객체 스스로 이행할 수 없는 둘 이상의 서로 다른 도메인 객체 간의 행위를 조직화하거나 도메인 룰을 적용하는 역할을 합니다. Stateless (무상태) 하다는 특징이 있습니다.
레거시 코드와의 비교
여기 한 가지 기존 코드를 살펴보겠습니다.
ErrorCode.java
라는 파일 내용의 일부입니다. (이 글의 코드에는 Lombok Annotation 을 사용합니다.)
@Getter
public enum ErrorCode {
// Common
INVALID_INPUT_VALUE(400, "C001", "유효하지 않은 값입니다."),
//...
// 평가 (Event)
FINISHED_EVENT(400, "E001", "종료된 평가입니다."),
//...
// 사용자 (User)
NOT_MATCH_USER(400, "U001", "정상적으로 접근하여 주세요."),
//...
// Survey
ALREADY_SUBMITTED_USER(400, "S001", "이미 설문조사를 완료하였습니다.");
//...
}
이 한 파일 안에서도 DDD 관점에서 불편한 부분이 많이 보이죠?
첫 번째로 제 눈에 가장 잘 뜨인 부분은 보편 언어를 사용하고 있지 않은 것입니다. 보편적으로 Event 라는 단어를 듣고 평가를 떠올릴 수 있을까요? 처음 코드를 보고 이해하는데 한참 걸렸습니다. 이처럼 도메인에서 실제로 사용하는 단어와 일치하지 않는 언어 사용은 이해하는데 걸림돌이 되고 실무자와 개발자, 기존 개발자와 새로 들어온 개발자와의 소통에 방해가 됩니다.
어떻게 고쳐볼 수 있을까요?
현재 리빌딩하는 프로젝트에서는 함께 공유하는 Project Wiki 상에 용어사전 을 마련해두고 도메인에서 사용할 단어를 합의하고 정의했습니다. 진행에 따라 유연하게 추가되거나 변경되기도 하죠.
평가를 의미하는 용어는 Assessment 로 합의하였습니다. 도메인에 이벤트 (보편적인 Event) 가 따로 있기 때문이죠.
또 어떤 불편한 부분이 보이시나요?
바로 Bounded Context 입니다. 서로 독립적으로 다루어야 하는 도메인 모델인 평가, 사용자, 설문 등의 Context 모델이 한 파일 안에서 다루어지고 있습니다. Context 의 울타리가 없어 모든 Context 에서 해당 모델을 통해 임의의 다른 Context 모델에 접근하는 것이 가능합니다.
ErrorCode
는 말 그대로 에러가 발생했을 때 그것을 핸들링하는 쪽에서 그에 맞는 에러코드를 사용하려고 만든 것입니다. 즉, 어떤 Context 에서든 임의의 다른 Context 의 에러코드에 접근하고 사용할 수 있다면 그런 코드를 믿고 사용하기 힘들겠죠. 어떤 Context 에서만 접근 · 사용하고 싶은 ErrorCode
가 있다면 그것을 그 Context 내에서 만들어서 사용하는 것이 바람직할 것입니다. 주제 밖이지만 날것의 int
형 응답 코드를 사용하는 것도 문제입니다. (응답 코드에 익숙하지 않다면 일일이 찾아 봐야 하고 가독성 측면에서도 좋지 않습니다.)
이것을 개선한 To-Be 시스템의 일부를 보겠습니다.
/**
* 403
* 요청한 사용자와 작성자가 다를 경우 이 예외를 던집니다.
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
public class AuthorNotMatchedException extends RuntimeException {
public AuthorNotMatchedException(Long expectedUserId, Long actualUserId) {
super("Expected User ID: %d, Actual: %d".formatted(expectedUserId, actualUserId));
}
}
이렇게 어떠한 예외 모델이 필요한 시점에 해당 Context 내에서 필요한 예외를 정의합니다. 이 모델은 Context 내에서 자연스럽게 스스로 본인의 역할을 충분히 설명하고 있습니다. 군데군데 주석으로 지저분한 설명을 붙일 필요가 없죠. 주제 밖이지만 @ResponseStatus(HttpStatus.FORBIDDEN)
처럼 선언적으로 응답 코드를 지정함으로써 가독성 또한 좋아진 것은 덤입니다.
왜 DDD 를 해야 할까요?
유지보수에 좋습니다.
도메인은 (문제, 요구사항은) 가만히 있지 않습니다. 시간이 지나며 바뀌기 마련입니다. 도메인의 변경은 곧 도메인 모델의 변경, 소프트웨어의 변경입니다. DDD 를 함으로써 유연한 대응을 할 수 있습니다. 여기서 유연한 대응은 결합도, 응집도 관점에서의 그것입니다. DDD 를 잘 따랐다면 관심사가 분리되어 있어 어떤 변경이 필요할 때 실제로 건드려야 하는 모델이 제한적일 것입니다. 그 도메인의 Aggregate Root가 무엇인지 합리적으로 추론하면 되기 때문입니다. 해당 도메인 룰과 그것을 컨트롤하는 주체가 무엇인지 잘 이해하고 있다면 말입니다.
느낀 점
정말 소박한 예시를 들었지만 그래도 글을 쓰기 위해서 더 좋은 Practice 에 대해 고민하는 유익한 시간을 보냈습니다. 거기에서 느낀 것은 DDD 는 객체지향 패러다임의 정수가 아닐까 하는 것입니다. 객체지향적 개념이 그 기저에 뿌리 깊이 깔려 있습니다. 외부에서는 Aggregate Root 로만 접근해야 한다 는 원칙만 생각해봐도 그렇습니다. 그 목적은 불변식 유지, 일관성 유지이고 그 방법은 캡슐화입니다. 이는 객체지향을 잘 이해하고 있지 않다면 뜬 구름 잡는 이야기로 들릴 수 있겠다고 생각합니다.
DDD 원칙과 원리를 항상 최우선으로 해야 한다는 이야기는 아닙니다. 소프트웨어 개발에 경험이 많지는 않지만 저도 개발을 하면서 '이렇게 해도 되고, 저렇게 해도 되는데 왜 이렇게 할까?' 하는 의문이 종종 들었습니다. DDD 는 그 중 일부에 대한 명료한 대답을 제시하는 하나의 기준이라고 생각합니다.
처음부터 DDD 를 잘 실천할 수 있는 개발자는 드물 것입니다. 경험과 실수, 갈등을 통해 DDD 를 체화하고 적극적인 토론을 통해 팀 전체의 이해도를 올리면서 발전해 나가는 것이 중요할 것입니다. 개발은 혼자 하는 것이 아니니까요.
References
1. 도메인 주도 설계: 소프트웨어의 복잡성을 다루는 지혜, 에릭 에반스, 이대엽(번역), 2011, ISBN: 9788992939850
1-1. 원문: Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans, 2003, ISBN: 9780132181273
2. DDD(Domain Driven Design)
3. 도메인 원정대 #우아콘2021 #둘째날_새로운여정