데이터 모델링에서 DDD로의 여정

데이터 모델링에서 DDD로의 여정

일정의 압박과 도메인 설계의 현실적인 함정

새로운 프로젝트를 시작할 때, 많은 개발자가 도메인 주도 설계(DDD, Domain-Driven Design)의 가치를 머리로는 이해하면서도 현실적인 타협안을 선택하곤 합니다. 특히 비즈니스의 빠른 출시를 요구하는 일정 압박 속에서는 정교한 도메인 모델링에 오랜 시간을 쏟기 어렵습니다.

현장 인력 관리 플랫폼 프로젝트 역시 개발 초기부터 DDD 사상을 기반으로 비즈니스 코드를 작성하고자 시도했습니다. 하지만 촉박한 개발 일정 속에서 빠르게 도메인을 설계해 나가다 보니, 아키텍처적으로 중요한 핵심 고리들을 놓치게 되었습니다.

당시 겪었던 설계상의 한계는 크게 세 가지였습니다.

  1. 도메인 애그리게이트(Aggregate) 범위 설정 부재: 개별 도메인 객체들이 어떤 생명주기와 트랜잭션 범위를 공유해야 하는지 명확히 정의하지 못하였습니다.

  2. 값 객체(Value Object)의 무분별한 엔티티(Entity)화: 식별자가 필요 없고 단순히 속성들의 묶음으로 표현되어야 할 값 객체들조차 무작정 독립적인 엔티티로 설계되었습니다.

  3. 도메인 간 관계 설정 누락: 객체들이 서로 어떻게 연결되어 동작하는지 도메인 수준에서 명시적인 관계를 맺어두지 못했습니다.

이러한 상태에서 비즈니스 규칙이 추가되자 시스템의 복잡도가 급격히 올라가기 시작했습니다. 도메인 간의 관계가 코드에 선언되어 있지 않다 보니, 서비스 레이어(Domain Service)가 비즈니스 로직을 수행할 때마다 매번 사방에 흩어진 엔티티들을 직접 조회해 와서 코드로 관계를 수동 복원해야 했습니다. 자연스럽게 서비스 로직은 비대해졌고, 정합성을 검증하기는 까다로워졌습니다.

우리는 한 걸음 멈춰 서서 이를 바로잡기로 결정했습니다. 일정의 압박 속에서 남겨진 기술 부채를 해결하고, 도메인의 자율성을 회복하기 위해 진행한 리팩토링의 핵심 과정을 공유하고자 합니다.

용어와 개념의 정립: 유비쿼터스 언어(Ubiquitous Language)를 코드로

DDD의 첫 단추는 기획자, 디자이너, 개발자 등 프로젝트에 참여하는 모든 사람이 동일한 언어로 소통하는 유비쿼터스 언어(Ubiquitous Language)를 정립하는 것입니다. 리팩토링 이전의 프로젝트 코드는 기술적으로 편리한 용어나 모호한 단어들이 도메인 객체의 이름으로 쓰이고 있었습니다.

'기술의 단어'에서 '업무의 단어'로

대표적인 사례가 구인공고에 근로자가 지원하는 행위를 나타내는 객체였습니다. 기존 코드에서는 이를 단순히 Application으로 명명했습니다. 개발자 관점에서는 아주 자연스러운 단어였지만, 이 단어는 다음과 같은 문제를 안고 있었습니다.

  • Application은 프레임워크나 애플리케이션 자체를 뜻하는 기술 용어와 혼용되어 혼란을 주었습니다.

  • 인력 공급 비즈니스에서 실제로 쓰이는 용어는 현장에 등록하고 일하겠다는 의사 표시인 '지원(Enrollment)'에 가까웠습니다.

리팩토링 과정에서 우리는 이를 Enrollment라는 도메인 명칭으로 변경했습니다. 또한, 지원서 검토 과정에서 임시로 생성하던 파일 백업 객체인 ApplicationWorkerProfileSnapshot처럼 기술 중심적인 단어 Snapshot을 배제하고, 비즈니스 관점에서 직관적인 EnrollmentProfile과 SubmittedDocument로 명칭을 개선했습니다.

[Before] 기술 중심 네이밍

JobApplication ──> ApplicationWorkerProfileSnapshot ──> ApplicationDocumentSnapshot

[After] 비즈니스(유비쿼터스 언어) 중심 네이밍

Enrollment ──> EnrollmentProfile ──> SubmittedDocument

개발자는 이제 코드를 짜면서 기획서의 흐름을 머릿속으로 번역할 필요가 없어졌습니다. 코드를 읽는 것 자체가 비즈니스 시나리오를 읽는 것과 같아졌기 때문입니다. 도메인 모델을 보았을 때 업무 장면이 그대로 상상되는 아키텍처, 그것이 유비쿼터스 언어의 의미라는 것을 이해했습니다.

애그리게이트(Aggregate) 경계 설정: 무분별한 엔티티화를 극복하기

초기 설계에서 가장 큰 부채는 '무분별하게 독립 엔티티로 설계된 객체들'이었습니다. 객체지향과 DDD에서 엔티티는 고유한 식별자(Identity)를 갖고 전 생명주기에 걸쳐 추적되어야 하는 객체입니다. 반면 값 객체(Value Object / Value Group)는 식별자 없이 단순히 속성만을 표현하며, 부모 객체의 생명주기에 종속됩니다.

일정에 쫓겨 모든 것을 테이블 형태의 엔티티로 구현해 놓자, 각 엔티티는 자신만의 독립적인 테이블, 리포지토리, 그리고 로직을 갖게 되었습니다. 이는 데이터베이스 조인을 남발하게 만들고, 정합성 유지 비용을 폭발적으로 증가시켰습니다.

JobPost와 JobPostRecruitment 사례

대표적인 것이 구인공고 정보를 담는 JobPost와, 그 공고 안에서 구체적인 직종별 모집 인원 및 단가를 나타내는 JobRecruitment였습니다. 이들은 원래 공고 하나에 완전히 종속된 하위 개념이었으나, 초기 설계에서는 각각 독립된 StageEntity로 구현되었습니다. 이로 인해 다음과 같은 비효율이 발생했습니다.

  • 구인 공고를 수정할 때 모집 조건 데이터를 수정하려면, 트랜잭션 내에서 JobRecruitment 엔티티를 별도로 조회하여 수정해야 했습니다.

  • 공고와 모집 조건의 생명주기가 완전히 일치함에도 불구하고, 내부 데이터를 직접 수정할 수 있는 통로가 외부에 열려 있어 정합성이 깨질 위험이 존재했습니다.

리팩토링 과정에서 우리는 JobRecruitment를 독립된 엔티티가 아닌, 식별자가 없는 값 객체(Value Object) 그룹인 ValueGroup으로 격하시켰습니다. 그리고 이를 JobPost 애그리게이트 루트 내부에 완전 내장했습니다.

이제 모집 조건의 추가, 수정, 삭제는 오직 부모인 JobPost를 통해서만 일어납니다. 외부에서 개별 모집 조건을 함부로 조작할 수 없게 설계됨으로써, "모집 공고가 열려 있을 때만 모집 인원을 수정할 수 있다"는 비즈니스 규칙을 JobPost 내부에서 원자적으로 완벽히 보장할 수 있게 되었습니다.

도메인 간 관계 설정과 서비스 레이어 다이어트

초기 설계 단계에서 도메인 엔티티 간의 유기적인 연관 관계가 명확히 선언되지 못했던 탓에, 비즈니스 로직을 처리하는 서비스 레이어(Domain Service)에는 상당한 비효율이 존재했습니다.

[초기 서비스 로직의 형태]

1. A 엔티티를 ID로 조회

2. B 엔티티의 외래 식별자가 필요한 상황이지만 관계가 없으므로, A 엔티티의 특정 필드를 직접 꺼냄

3. 꺼낸 필드 값을 기준으로 B 엔티티를 별도로 DB에서 쿼리해서 가져옴

4. C 엔티티 역시 같은 방식으로 수동으로 조립하여 로직을 처리

도메인 간 관계 설정(@FieldSourceId 등 외래 키 및 연관 필드 규약)이 유기적이지 않으면, 엔티티들은 단지 독립된 모래알처럼 흩어지게 됩니다. 이 상태에서 로직을 완성하려면 서비스 레이어는 다음과 같은 고된 작업을 수행해야 했습니다.

도메인 관계가 없으면 서비스 로직이 매번 관계를 수동으로 복원해야 하고, 이는 결국 서비스 레이어의 비대화와 가독성 저하로 이어졌습니다. "로직의 길이가 30줄을 넘어가면 도메인 설계가 잘못된 것입니다"라는 지적은 바로 이 수동 관계 복원 코드 때문에 발생한 것이었습니다.

관계 설정과 풍부한 도메인 모델로의 전환

우리는 도메인 간의 외래 참조 식별자와 관계를 모델 수준에서 명확히 다시 걸었습니다. 각 엔티티에 연관된 외래 키 관계를 지정하고 엔티티 내부에서 필요한 하위 association을 명시했습니다. 이와 더불어 엔티티 외부에서 필드를 무분별하게 변경할 수 없도록 setter의 남용을 차단하고, 객체의 상태 수정은 명확히 정의된 행위 메서드인 modifyAttributes를 통해서만 이루어지도록 캡슐화했습니다.

이처럼 도메인이 명확한 관계를 품고 스스로 유효성을 검증하며 상태를 변경하게 되자, 서비스 레이어는 더 이상 "데이터를 수집해서 조립하는 코디네이터" 역할을 할 필요가 없어졌습니다. 서비스 코드는 단지 영속성 컨텍스트에서 루트 엔티티를 꺼내와 비즈니스 행위를 지시하고, 이벤트를 발행하는 오케스트레이터 역할로 극적인 다이어트를 달성할 수 있었습니다.

도메인의 생명력을 코드로 불어넣다

이번 프로젝트의 도메인 리팩토링 과정은 단순히 데이터베이스 컬럼 이름을 바꾸거나 패키지 구조를 변경하는 단순 정리 작업이 아니었습니다. 그것은 "시스템이 비즈니스를 바라보는 관점의 전환"이었습니다.

일정의 촉박함 속에서 빠르게 달리다 보면 설계 부채가 쌓이는 것은 어쩌면 불가피한 일입니다. 그러나 이를 방치한 채 서비스가 거대해지면 결국 아키텍처는 마비됩니다. 이번 리팩토링을 통해 값 객체의 분리, 올바른 애그리게이트 설계, 도메인 간의 명확한 연관 관계 설정이 가져오는 강력한 이점을 직접 체감할 수 있었습니다.

  • 유지보수의 효율성: 새로운 비즈니스 규칙이 추가되거나 정책이 변경되었을 때, 어떤 객체의 코드를 고쳐야 하는지 고민할 필요가 없어졌습니다. 규칙의 소유 주체인 애그리게이트를 찾아 그 안의 검증 로직만 수정하면 되기 때문입니다.

  • 의사소통의 일치: 기획자와 개발자 간의 소통 오류가 크게 줄었습니다. 기획서 상의 흐름과 도메인 클래스 다이어그램이 1:1로 매핑되면서, 서로 다른 언어를 해석하기 위해 낭비되던 소통 비용이 사라지게 되었습니다.

  • 예측 가능성과 안정성: 캡슐화와 엄격한 ID 설계, 애그리게이트 격리를 통해 예기치 못한 사이드 이펙트(Side Effect)가 원천 차단되었습니다. 시스템은 훨씬 예측 가능해졌고, 견고해졌습니다.

일정에 쫓겨 일단 달렸더라도, 한 템포 쉬어가며 도메인을 정비하는 일은 향후 프로젝트 속도를 몇 배 더 빠르게 만들어 줍니다. 설계에 대해 고민하는 동료 개발자들에게도, 무작정 생성된 엔티티들을 값 객체와 애그리게이트로 구조화하고 도메인 간의 관계를 바로잡는 이 여정을 꼭 경험해 보시라고 적극 권하고 싶습니다.

informalife

Site footer