《도메인 주도 설계 철저 입문》 DDD, 코드 저 너머로


🚫 DDD 멈춰

우스갯소리지만 우아한테크코스에서는 DDD 관련 책들을 금서로 여긴다. 책을 꺼내드는 순간 객체 아저씨가 이놈~ 하고 쫓아온다나 뭐라나(자매품으로 토비의 스프링이 있다). 기본적인 언어 습득, 책임 분리도 잘 학습되지 않았는데 이런 디자인을 가져와서 어떻게 쓰겠냐는 이야기다. 레벨 2의 미션에서 아래와 같은 피드백을 받았다. 그땐 ‘이게 뭐길래 그러는 거지?’라는 생각이 스멀스멀 피어올랐지만, 지금은 어느정도 이해가 간다.

DDD는… 제 생각엔 이 미션에서 조금(x) 한참(o) 벗어난 주제라고 생각하여 앞서 고민해봤다고 남겼던거였어요. 일단 DDD는 프로젝트를 만들고 운영하는데 있어서 필수가 아닙니다. 개발자뿐만 아니라 다양한 직책의 팀원들 / 외부 인원이 참여하는 과정이 포함되기도하는데, 지금은 사실상 원맨 프로젝트다보니 그런 과정을 제대로 경험해볼 수 없는 것 같아요.

또한, 지금과 같은 규모의 프로젝트는 도메인이 그리 풍부하지도 않아서 말 그대로 “그냥 한 번 해보기 위해” 도입을 하는 느낌이 너무 강해요. 물론 학습과 경험을 위해 시도해보는 것은 좋지만, 위에서 말한 이유 때문에 막상 제대로 해보지 못하는거죠.

마지막으로, 지금 우테코에서 레벨 별 목표로 하는 학습내용만으로도 시간이 부족합니다. 수능을 보기 위해 미적분을 공부해야하는데 마치 선형대수학을 미리 보려고하는 고3 느낌이에요. 미적분 시험을 90점 맞았다면 95점, 100점을 위해 공부해야지, 선형대수학을 잡고 있는건 목표에서 많이 벗어난다고 보는거죠.

웬만한 개발자라면 신입에게 DDD를 모른다고? 라고는 하지 않겠네요. 하지만 DDD 를 “해봤다”고 말하면서 스프링과 JPA 의 기본적인 동작원리 와 같은 내용을 제대로 말하지 못하면 좀 많이 아쉬울 것 같아요 🤔

리뷰어 피케이의 피드백

최근에 이수로 놀러 자주 나가는데, 이수역 지하에 중고서점이 있어서 둘러봤다. 프로그래밍 책을 사러 간 건 아니었지만 결국 발길은 그곳에 머물렀고, 도메인 주도 설계 철저 입문이라는 책을 싸게 챙겨왔다. 면접 준비 때문에 제이슨의 DDD 이야기도 못 들어서 그 실체를 알아보고 싶었다.

책을 사온 건 어젠데 이틀만에 300쪽 분량을 다 읽었다. 꼼꼼히 읽어가면서 새겼다고 하면 거짓말이겠지만, 책을 사면 밑줄 긋고 글씨 적는 게 일상이라 벌써 지저분해졌다. 예제 코드는 C#으로 쓰여있어 Java를 익힌 나에게는 쉽게 이해할 수 있었다. 대부분의 내용이 우테코에서 지내면서 익혀 온 내용들로 차있어 놀랐다. 쉽게 읽히면서도 크루들과 토론했던 장면들이 떠올라서 재미있게 읽었다.

🤨 우리는 이미 알고 있는 이야기

우리는 지난 10개월동안 끊임없이 고민했다. 다른 크루들과 부딪히며 자신만의 철학을 다져나갔다.

  • 사용자가 무엇을 불편해하는 거지?
  • 우리의 의사선택으로부터 어떻게 사용자의 피드백을 받을 수 있을까?
  • 유지보수하기 쉬운 코드는 무엇이지?
  • 변경 전파는 어디까지 일어나며, 이를 어떻게 최소화하지?
  • 구체 클래스를 직접 참조해도 될까?
  • 이 객체의 책임은 어디까지지?
  • 코드가 복잡하네, 새로운 객체를 도출해서 리팩터링할 수는 없을까?
  • 이 코드는 테스트하기 어렵네, 어떻게 테스트하기 쉬운 코드로 만들 수 있을까?
  • 객체에 상태가 존재하니 변경을 추적하기 어렵네. 불변 객체를 고민할 수 있을까?
  • … 🤔

이외에도 정말 많은 고민을 리뷰어나 코치, 다른 크루들과 공유했다. 이 책은 간단한 표현 방식부터 시작해 도메인 주도 설계에 대한 내용을 바텀업 방식으로 설명하는데, 바텀에 존재하는 여러 고민들은 우리가 이미 시도하거나 고민한 내용이었다. 불변을 보장하며 책임을 가지는 값 객체, 영속성 계층과의 의존을 떼어내기 위한 레포지토리 등이 그렇다(엔티티와 같은 건 DDD에서 이야기하는 것과 거리가 있을 수도 있겠지만… 그래도 대부분의 내용이 이해하기 어렵지 않았다).

사실 이런 건 DDD에 가깝다기보다는 유지보수하기 쉬운 코드, 객체의 책임에 대해서 꾸준히 탐구하다 보면 떠올릴 수 있는 쟁점이라고 생각한다. 새로운 요구사항이 들어왔을 때 빠르게 대응할 수 있는가? 변경의 전파가 어디까지 미치는가? 이를 최소화할 수 없을까? 에 대한 고민이 모였다. 읽으면서도 ‘이거 맞아?’라는 기분이 들었다. 바텀업이다보니 실제 이해관계자 이야기가 나중에 나온 것도 한 몫 했다.

💡 ㄷㄷㄷ 모먼트

책을 읽으면서 다시 상기할 수 있는 부분이 많았다. 체화되어 있었지만 글로 재확인하니 다시금 머리가 환기됐다. 이 단락에서는 읽으면서 밑줄을 친 부분이나 고민한 부분, 새로 알게 된 지식을 적어둔다.

값 객체와 엔티티: Value Object, entity

불변하고 비교 가능한 값 객체를 활용한다면, 코드에서 더 풍부하게 나의 생각을 표현해낼 수 있다. 의도를 담은 코드는 이해하기 쉬우며 다른 사람들이 쉽게 수정할 수 있다. 정의되지 않은 행동은 곧 할 수 없다는 의미이며, 값 객체는 객체에 행동을 정의함으로써(메서드를 만들어서) 어떤 행위를 할 수 있는지를 명시한다.

엔티티와 값 객체와의 차이점이라면 생애주기와 연속성이다. 식별자를 기준으로 값을 구변하며, Identical한 것을 확인할 수 있어야 한다.

도메인 서비스: Domain service

우리가 흔히들 작성하는 @Service 어노테이션은 어떤 서비스를 의미하는 것일까. 이 책에서는 도메인 서비스와 애플리케이션 서비스를 구분한다. 지금까지는 나도 프로젝트에서 이를 구분하지는 않았지만, 유지보수를 위해 어떤 것을 기준으로 나눠야할지를 알게 되었다.

도메인 서비스는 해당 도메인 자체로 해결하기 어려운 요구사항을 쉽게 풀어나가도록 돕는다. ‘아이디 중복 금지’와 같은 개념이다. 회원이라는 도메인 안에서 아이디가 겹칠 수 없는 건 회원 도메인의 요구사항이므로, 회원 도메인 서비스에 해당 로직을 둔다. 회원 도메인 엔티티에 해당 내용을 넣는 순간 문제를 풀어나가기 어려워지기 때문이다. 다만 정말 도메인 엔티티로 해결할 수 없는 문제에 한해서만 서비스를 활용하자.

애플리케이션 서비스: Application Service

?? 도메인 서비스와는 무엇이 다를까? 앞선 도메인 서비스는 도메인 자체로 풀기 어려운 문제를 쉽게 풀어가도록 돕는다고 했다. 애플리케이션 서비스는 실제 유즈케이스를 풀어나간다. 회원을 등록하거나, 회원정보를 수정하는 등이다. 회원 등록은 애플리케이션 서비스의 책임, 회원 중복 확인은 도메인 서비스의 책임이다.

애플리케이션 서비스에서는 도메인 규칙을 기술해서는 안 된다. 이게 제일 실수하기 쉬운 부분이라고 생각하는데, 도메인 객체가 수행하는 일과 유즈케이스에만 집중하는 연습이 필요할 듯하다.

Service라는 하나의 클래스가 여러가지 일을 하는 것을 경계하고, 응집도를 매번 파악하자. 필드가 모든 메서드에서 쓰이고 있지 않다면, 해당 클래스는 한 개보다 많은 책임을 가지고 있을 가능성이 크다. 현재는 하나의 Service 클래스에 많은 로직이 들어가 있겠지만, 이를 분리하는 것은 SRP 관점으로 보았을 때 정확히 들어맞는다. 사용자 등록 서비스와 사용자 탈퇴 서비스는 서로 다른 객체로 관리되는 것이 당연하다.

💡 그럼 실제로 예외를 던져야 하는 곳은 어디일까? 도메인 객체에서 예외를 던지는 건 올바를까? ‘중복된 아이디로 가입할 수 없다’는 도메인 서비스의 책임일까, 애플리케이션 서비스의 책임일까?

  1. 도메인 서비스에서 exists()에 대한 결과로 boolean을 내려주고, 애플리케이션 서비스에서 예외 발생
  2. 도메인 서비스에서 예외 발생

도메인 서비스에서 예외를 발생시키는 것은 ‘아이디는 중복되면 안 된다’는 정책이 포함되어 있다. 이는 유즈케이스와 연관되어있는 항목이기도 하고, 무엇보다 해당 부분이 도메인 서비스에 존재한다면 ‘중복되면 안 된다’는 내용이 애플리케이션 코드에 드러나지 않는다. 코드에는 의도를 담아야 한다. 누구나 해당 코드를 읽었을 때 ‘아이디는 중복되면 안 되겠네요’를 유즈케이스를 구현한 곳에서 확인할 수 있어야겠다.

애그리거트: Aggregate

50쪽밖에 되지 않는 애그리거트와 명세 파트가 제일 어려웠다. 두세번 다시 돌아오기를 반복하고 이해하는 데 시간이 걸렸다. 책을 다시 펼 핑계가 생겼다. 😅

한 곳에 모아 관리해야 하는 규칙이 여러 곳에 파편화되면 중복된 코드가 발생한다. 규칙과 관련된 코드는 도메인에 남아 있어야 하며, 이 문제를 해결하기 위해 등장한 개념이 애그리거트다. 생각해보면 정말 당연한 이야기에 이름을 붙여둔 거라, 겁먹지 않는 것이 가장 중요하다!

여러 개의 객체가 모여 의미를 가지며, 이는 불변 조건을 만족한다. 애그리거트는 경계와 루트를 가진다. 루트 객체를 통해서만 외부에서 조작이 가능하다. 회원과 동아리가 있다면, 동아리에 회원을 추가하는 유즈케이스는 동아리 애그리거트에서 이루어져야 한다는 의미다. 우리가 오래 연습한 디미터(데메테르)의 법칙이 이곳에서도 중요하게 여겨진다.club.member.add(member)보다 club.join(member)가 더 유지보수에 용이하다. 애그리거트의 루트 객체로부터 조작을 이뤄야 한다.

관계있는 애그리거트끼리 객체로 참조하면 위 법칙을 깰 가능성이 존재한다. 이를 원천적으로 막기 위해 식별자로 참조하도록 한다. 불필요한 참조를 피하고 의존을 없애 더 가볍게 만든다.

애그리거트의 경계를 정하는 건 간단하지 않다. 도메인의 개념, 불변 조건과 도메인-시스템 간 균형을 이루는 지점을 명확하게 찾아내야 한다.

명세: Specification

로직이 복잡해진다면 일정 책임을 갖는 객체를 도출해낼 수 있다. 객체를 평가하는 코드들을 한데 모아 명세로 나타내는 것이다. 실생활에서의 규칙을 담은 객체는 복잡하고, 여러 규칙이 합쳐진 유즈케이스를 구현할 때 많은 어려움이 있다. 이때 복잡한 규칙을 하나의 객체에 담아내면 애플리케이션 서비스 코드가 가벼워질 수 있다. 테스트가 쉬워지는 건 덤이다.

지금까지 적어둔 게 DDD에서 자주 사용되는 용어긴 하지만, 충분히 객체지향과 책임 분리에 대해서 고민했다면 쉽게 받아들일 수 있는 내용이다. 기술이나 특정 패턴에 매몰되지 말자.

🏔️ 코드 너머 DDD

도메인 주도 설계는 혼자 이루어질 수 없다. 꾸준히 다른 팀원이나 이해관계자와의 협력을 통해 이루어내야 한다. 소통의 벽을 허물기 위해 공통의 언어Ubiquotus language가 탄생했고, 그 약속을 어디까지 지킬 지에 대한 경계Bounded context 개념도 생겨났다. 경계가 생겼으니 각각의 범위가 어떻게 소통하는지를 거시적으로 바라보기 위한 그림Context mapping이 도입되었고, 이런 기반 위에서 서비스를 이끌어나가는 모든 이해관계자가 무리없이 소통하며 유연하게 대응할 수 있게 되었다. DDD의 본질은 이곳에 있다고 생각했다. DDD는 코드 너머에 있다.

DDD가 우테코에서 금기시(?)되었던 이유는 글 초입에서 인용했던 리뷰어의 이야기로도 충분히 설명할 수 있다. ‘코드 너머에 존재하는’ 설계 방식을 직접 실천하기 어려운 환경이기 때문이었다. 또, Java 문법이나 클린 코드, TDD와 같은 여러 철학을 고민하면서 자연스럽게 유지보수에 대한 관점을 깨우치게 되는 학습 방법이 더 오랜 기간 몸에 머무를 것이라고 믿는다. 이해관계자가 많아지고 재빠르게 사용자의 요구사항을 받아들여야 하는 상황이 온다면, 다양한 방식으로 DDD를 실천하고 있지 않을까 기대하고 있다. 👋🏻

Categories