지난 편에서 도메인 객체가 어떻게 올바르게 만들어지는지를 다루는 팩토리(Factory)를 살펴봤어요. 이번 편에서는 만들어진 객체를 시간에 걸쳐 안전하게 다루는 리포지토리(Repository)를 알아봅니다. 지금까지 다룬 모든 기본 요소들을 실무에서 점검할 수 있는 최종 체크리스트와 시리즈 핵심 정리도 담았어요.
1. 리포지토리란 무엇인가: 저장된 객체에 대한 관리
팩토리가 “이 객체가 존재할 수 있는 조건이 무엇인가?”에 대한 질문에 답한다면, 리포지토리는 다른 질문에 답해요.
“도메인 규칙을 깨뜨리지 않고, 시간에 걸쳐 이 객체를 어떻게 다루는가?”
객체가 존재하면, 제품은 한 번만 상호작용하는 경우가 드물어요. 여러 사용자 액션에 걸쳐 불러오고, 업데이트하고, 저장하고, 다시 방문하죠. 리포지토리는 이 지속적인 상호작용이 안전하게 이루어지는 방법을 정의해요.
리포지토리(Repository)는 도메인 객체가 생성된 후부터 더 이상 필요하지 않을 때까지 관리하는 도메인 구성 요소예요.
리포지토리는 시스템이 도메인 언어를 사용해 도메인 객체를 요청하고 작업할 수 있는 인터페이스를 제공하면서, 기저 저장 메커니즘의 복잡성을 숨기죠. 도메인은 객체가 SQL 데이터베이스에 저장되든, NoSQL에 저장되든, 캐시에 저장되든, 외부 API에 저장되든 알지도 신경 쓰지도 않아요.
리포지토리는 영속성 세부 사항을 숨기고 의도 중심 연산을 노출함으로써, 리포지토리가 클라이언트를 저장 관심사 대신 도메인 개념에 집중하게 해요. 이것이 저장 전략과 인프라스트럭처가 진화해도 도메인 로직을 안정적으로 유지하는 방법이에요.
리포지토리를 비유하면, 도서관 사서와 같아요. “DDD 관련 책 있나요?”라고 물으면 사서가 찾아다 줘요. 책이 3층 서가에 있는지, 지하 창고에 있는지, 다른 도서관에서 대여해 오는지는 이용자가 알 필요 없죠. 사서가 도서관의 물리적 구조를 숨기고, “도서 주제”라는 도메인 언어로 소통하는 거예요.
2. 리포지토리가 중요한 이유: 제품 쿼리, 성능, 규칙의 교차점
리포지토리는 중요한 교차점에 위치해요.
- 제품 요구사항: 무엇을 조회해야 하는가
- 성능 제약: 무엇이 빨라야 하는가
- 도메인 규칙: 무엇이 일관되게 유지되어야 하는가
리포지토리가 없으면 이 관심사가 섞이는 경향이 있어요. 도메인 로직이 쿼리 세부 사항에 의존하기 시작하고, 성능 최적화가 제품 규칙에 스며들고, 저장의 변경이 시스템 전체에 파급되죠.
리포지토리가 이 복잡성을 흡수하고 도메인에 깨끗하고 안정적인 표면을 제공해요.
리포지토리의 교차점을 비유하면, 음식 배달 플랫폼의 주문 시스템과 같아요. 사용자(제품 요구사항)는 “치킨 주문”만 하면 되고, 라이더 배치와 동선 최적화(성능)는 뒤에서 처리되고, “최소 주문 금액” 같은 규칙(도메인)은 항상 유지돼요. 이 세 가지를 섞으면 사용자가 라이더 동선을 신경 써야 하거나, 최소 금액 규칙이 성능 때문에 무시되는 상황이 생기죠.
3. 팩토리 vs 리포지토리: 탄생과 생명 주기
팩토리와 리포지토리는 둘 다 “객체를 다루기” 때문에 혼동되곤 해요. 하지만 매우 다른 일을 하죠.
| 팩토리 | 리포지토리 |
|---|---|
| 새 객체가 존재할 수 있는지 결정 | 이미 존재하는 객체를 관리 |
| 생성 시점 규칙을 강제 | 시간에 걸친 정확성을 보존 |
| 탄생에 집중 | 조회와 영속성에 집중 |
4. 리포지토리 설계 원칙
| 원칙 | 의미 | 왜 중요한가 |
|---|---|---|
| 애그리게이트 루트당 하나 | 각 리포지토리가 정확히 하나의 애그리게이트 루트를 관리. 내부 객체는 직접 불러오거나 조회하지 않음 | 애그리게이트의 일관성 경계를 보존하고 불변 조건 우회를 방지 |
| 도메인 중심 인터페이스 | 리포지토리 메서드가 쿼리 메커니즘이 아닌 비즈니스 의도를 표현. 질문이 제품 수준에서 인식 가능 | 도메인 언어를 깨끗하게 유지하고 데이터베이스 관심사 누출을 방지 |
| 트랜잭션 제어 없음 | 리포지토리가 언제 커밋할지, 여러 애그리게이트를 어떻게 조율할지를 결정하지 않음 | 접근에 집중하고, 오케스트레이션이나 플로우 제어를 분리 |
5. 예시: 구독 리포지토리
구독 리포지토리(Subscription Repository)는 이미 존재하는 구독을 다루기 위해 존재해요.
이런 질문에 답하죠.
- “이 구독은 어떤 것인가?” (ID로 조회)
- “이 계정에 이미 구독이 있는가?” (계정별 조회)
- “이 구독이 변경되었다. 저장할 수 있는가?” (업데이트된 구독 저장)
구독이 존재해야 하는지, 변경이 허용되는지는 결정하지 않아요. 그 결정은 팩토리와 애그리게이트에 속하죠.
Subscription 리포지토리
───────────────────────
┌──────────────────────────────────────────┐
│ Subscription 리포지토리 │
│------------------------------------------│
│ 답하는 도메인 질문: │
│ - ID로 구독 조회 │
│ - 계정의 구독 찾기 │
│ - 업데이트된 구독 저장 │
└───────────────┬──────────────────────────┘
│ 반환 / 저장
▼
┌──────────────────────────────────────────┐
│ Subscription (애그리게이트 루트) │
└──────────────────────────────────────────┘
리포지토리는 구독이 어떻게 저장되고 조회되는지에 대해 의도적으로 말하지 않아요. 저장 기술, 쿼리 언어, 성능 최적화는 실질적인 관심사이지만, 리포지토리가 제공하는 도메인 영역의 바깥에 위치하죠.
6. 도메인 모델 기본 구성 요소 실무 체크 리스트
제품의 복잡성이 늘어나고 있는 것 같을 때, 다음 체크리스트를 활용해보세요.
1) 언어와 개념
- 모든 핵심 용어를 한 문장으로 설명할 수 있다
- 핵심 용어가 PRD, 티켓, 엔지니어링 논의에서 일관되게 사용된다
- 의견 차이가 생길 때, 그것이 행동에 대한 것인지 정의에 대한 것인지 명확하다
2) 엔티티 vs 값 객체
- 각 개념에 대해: 변해도 여전히 “시간에 걸쳐 같은 것”인가 (엔티티)
- 연속성이 필요 없는 개념은 교체 가능한 묘사로 모델링되어 있다 (값 객체)
- 어떤 값 객체도 조용히 엔티티처럼 다뤄지지 않는다 (추적, 편집, 감사)
3) 관계
- 대부분의 관계가 기본적으로 단방향이다
- 큰 일대다 관계에 필요할 때 한정자(날짜, 역할, 상태)가 포함되어 있다
- 행동에 영향을 주지 않는 직접 연결은 제거되었다
- 양방향 관계는 실제 도메인 규칙에 의해 정당화될 때만 존재한다
4) 서비스
- 여러 개념에 걸친 비즈니스 결정이 단일 엔티티가 아닌 도메인 서비스에 산다
- 서비스 이름이 기술적 단계가 아닌 비즈니스 의도를 표현한다
- 도메인 서비스가 무상태이며 데이터를 소유하지 않는다
5) 모듈
- 모듈이 기술 계층이 아닌 제품 개념 중심으로 조직되어 있다
- 모듈 안의 개념들이 함께 바뀌는 경향이 있다
- 모듈 간 의존성이 제한적이고, 의도적이며, 방향성이 있다
6) 애그리게이트
- 각 애그리게이트 경계가 유효하게 유지하기 위해 함께 바뀌어야 하는 가장 작은 집합이다
- 불변 조건이 애그리게이트 경계 안에서 강제된다
- 외부 접근이 애그리게이트 루트로 제한된다
- 대부분의 트랜잭션이 하나의 애그리게이트만 수정한다
- 애그리게이트 간 조율이 공유 상태가 아닌 참조와 이벤트를 통해 이루어진다
7) 여러 애그리게이트
- 다른 제품 약속(계획, 소유권, 돈, 기록)이 분리되어 있다
- 실패 모드가 다른 애그리게이트가 독립적 생명 주기를 갖는다
- 장기적이거나 고위험 개념(결제, 예약)이 계획 개념과 섞이지 않는다
8) 팩토리 체크리스트
- 핵심 객체가 존재하게 되는 올바른 방법이 정확히 정의되어 있다
- 생성 시점 규칙(기본값, 유효성, 금지된 조합)이 중앙화되어 있다
- 생성이 완전히 성공하거나 완전히 실패한다
- 여러 생성 경로가 일관되지 않은 객체를 만들지 않는다
9) 리포지토리 체크리스트
- 각 애그리게이트 루트가 자체 리포지토리를 갖는다
- 내부 객체가 직접 불러오거나 조회되지 않는다
- 리포지토리 메서드가 데이터베이스 메커니즘이 아닌 비즈니스 질문을 반영한다
- 리포지토리가 트랜잭션이나 플로우 제어가 아닌 접근과 영속성에 집중한다
10) 빠른 점검 신호
실무에서 도메인 모델에 문제가 있다는 신호를 빠르게 포착하는 질문들이에요.
- “이 상태가 어떻게 가능한 거지?” → 불변 조건이나 생성 경계가 빠져 있다
- “같은 것이 어디서 만들어졌느냐에 따라 다르게 행동한다” → 팩토리 문제
- “작은 변경이 관련 없는 많은 영역에 영향을 준다” → 경계나 관계 문제
- “어디서든 깊은 내부 정보를 조회해야 한다” → 애그리게이트나 리포지토리 누출
- “모든 것이 즉시 일관되어야 한다” → 여러 애그리게이트가 인정되지 않음
팀이 무엇이 참이어야 하는지, 언제 참이어야 하는지, 누가 그것을 강제하는지를 명확히 말할 수 없다면, 코드가 아무리 깔끔해 보여도 도메인 모델은 무너질 거예요.
마무리: 이것이 중요한 이유
도메인 모델은 팀이 세 가지를 명확히 설명하지 못할 때 무너져요.
첫째, 기능이나 플로우와 관계없이 제품에서 항상 참이어야 하는 것이 무엇인지.
둘째, 그 규칙이 언제 적용되는지. 생성 시점에만인지, 업데이트 중인지, 전체 생명 주기에 걸쳐서인지.
셋째, 모델의 어떤 부분이 그것을 강제하는 책임을 지는지. 애그리게이트인지, 팩토리인지, 서비스인지.
이 답이 불명확하면, 규칙이 UI 로직, 서비스, 저장 코드에 천천히 퍼져요. 시스템은 여전히 작동할 수 있지만, 깨지기 쉽고 변경 비용이 비싸져요.
잘 설계된 도메인 모델은 이 책임들을 명시적으로 유지해서, 시스템이 진화해도 제품 결정이 이해 가능하게 남도록 해요.
도메인 주도 설계 시리즈

