Skip to content

Commit 69d4508

Browse files
authored
[refactor/#369] Activity Bookmark 슬라이스 분리 및 테스트 구조 정리 (#372)
* docs: 본격적인 DDD 적용 전 문서 최신화 * test: Bookmark 단위 테스트 보강 * refactor: Bookmark 서비스를 별도로 분리 * refactor: 북마크 관련 파일들을 bookmark 패키지 아래로 이동 * docs: 변경 내역 문서 최신화 * docs: 엔티티 객체간 연결을 끊는 것은 별도 이터레이션으로 분리 * refactor: 에러도 별도 애그리게이트 에러로 분리 * refactor: 통합 테스트도 별도 패키지로 분리 * refactor: 테스트를 Nested 구조로 변경 * chore: 로컬에서 사용하기 위한 ES 없는 ITBase 생성
1 parent b4ba84c commit 69d4508

43 files changed

Lines changed: 2205 additions & 1532 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/ddd-test-refactoring-roadmap.md

Lines changed: 262 additions & 80 deletions
Large diffs are not rendered by default.

docs/tactical-design.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
| Source / Ingestion | `TechBlog` | `RssFeedItem`은 DTO/ACL 결과 | `blogUrl``rssUrl`은 유일해야 한다. `lastCrawledAt``markCrawled(LocalDateTime)`으로만 갱신된다. 기술 블로그는 RSS 수집 대상의 기준이다. | `TechBlog`가 Source 컨텍스트의 루트로 적절하다. **`markCrawled(LocalDateTime)` 도메인 메서드 누락** — 현재 Anemic Model 위험. |
1616
| Post / Content | `Post` | `PostKeyword`, `PostDocument`, `ContentChunk` | URL은 유일해야 한다. 요약/짧은 요약은 `updateSummaries()`로만 교체된다. 키워드는 `clearKeywords() + addKeyword()` 조합으로만 교체된다. 임베딩 완료 시각은 `markAsEmbedded(LocalDateTime)`으로만 기록된다. `incrementViewCount()`는 비원자적 연산이므로 SQL atomic UPDATE 정책 적용 필요. | `Post`가 핵심 애그리거트 루트다. `PostKeyword``Post` 내부 컬렉션으로 보는 것이 자연스럽다. **`incrementViewCount()` 동시성 정책 미결정** (§1.2 참조). |
1717
| User Account | `User` | `UserInterestCategory`, `UserInterestKeyword` | `socialType + socialId` 조합은 유일해야 한다. 상태 전이는 `PENDING → ACTIVE → WITHDRAWN → PENDING(재활성화)` 경로만 허용된다. 관심 키워드는 반드시 선택된 관심 카테고리에 속해야 한다. 관심사 교체는 `replaceInterests()`로 단일 트랜잭션 내 불변식 검증과 함께 처리된다. | `User`가 루트다. 계정/온보딩/관심사 불변식을 소유한다. **`replaceInterests()` 도메인 메서드 누락** — 불변식 검증이 서비스 레이어에 산재. |
18-
| Personalization Profile | 명시적 쓰기 애그리거트 없음 | `UserProfileDocument`, `UserActivityData` | 같은 `userId` 기준 현재 개인화 프로필 projection은 하나만 유지된다. 프로필 텍스트, 벡터, 핵심 키워드는 함께 재생성된다. | Personalization Profile은 aggregate보다 read model / application service 중심 컨텍스트다. 현재 `UserProfileService`가 생성 책임을 가진다. |
19-
| Activity | `ReadPost`, `Bookmark`, `SearchHistory` | 없음 | `Bookmark``userId + postId` 조합이 유일해야 한다. `ReadPost`는 같은 사용자+게시글 중복 저장을 허용하되 `isFirstRead`로 최초 읽기를 구분한다. `SearchHistory`는 같은 검색어를 중복 저장한다 (동일 검색어의 반복 횟수 자체가 개인화 관심 신호가 된다). 행동 기록은 삭제되지 않고 보존된다 (북마크 제외). | 각 행동 기록이 독립 record aggregate처럼 동작한다. 현재 코드명 `ScrabPost`표준 용어 `Bookmark`로 통일 대상이다 (코드 미반영, 유비쿼터스 언어 README의 문서-코드 동기화 상태 참조). |
18+
| Personalization Profile | 명시적 쓰기 애그리거트 없음 | `PersonalizationProfileDocument`, `UserActivityData` | 같은 `userId` 기준 현재 개인화 프로필 projection은 하나만 유지된다. 프로필 텍스트, 벡터, 핵심 키워드는 함께 재생성된다. | Personalization Profile은 aggregate보다 read model / application service 중심 컨텍스트다. 현재 `PersonalizationProfileService`가 생성 책임을 가진다. |
19+
| Activity | `ReadPost`, `Bookmark`, `SearchHistory` | 없음 | `Bookmark``userId + postId` 조합이 유일해야 한다. `ReadPost`는 같은 사용자+게시글 중복 저장을 허용하되 `isFirstRead`로 최초 읽기를 구분한다. `SearchHistory`는 같은 검색어를 중복 저장한다 (동일 검색어의 반복 횟수 자체가 개인화 관심 신호가 된다). 행동 기록은 삭제되지 않고 보존된다 (북마크 제외). | 각 행동 기록이 독립 record aggregate처럼 동작한다. `Bookmark``domain/activity/bookmark` slice 아래로 분리되었고 `BookmarkCommandService`/`BookmarkQueryService`/`BookmarkConverter``bookmarks`/`bookmarked_at` rename migration까지 반영되어 있다. `ManyToOne -> id reference` 같은 aggregate 경계 재설계는 별도 이슈로 다루는 편이 안전하다. |
2020
| Search | 명시적 쓰기 애그리거트 없음 | `SearchResult` DTO, `PostDocument` read model | 검색어를 기반으로 검색 결과를 계산한다. 검색 결과는 저장되는 도메인 상태가 아니라 조회 결과다. | Search는 애그리거트보다 query service/read model 중심 컨텍스트다. |
2121
| Recommendation | **표준: `RecommendationSet`** (현재 코드: `RecommendedPost` 단건) | `RecommendedPost`, `RecommendationHistory` | 같은 `userId + rankOrder` 조합은 유일해야 한다. 새 추천 저장 전 기존 추천은 모두 `RecommendationHistory`로 이동해야 한다. `rankOrder`는 1..N 연속이어야 한다. | 현재 `RecommendedPost` 단건이 루트 역할을 하지만 `RecommendationSet` 개념으로 리팩터링 대상이다 (코드 미반영, 유비쿼터스 언어 README의 문서-코드 동기화 상태 참조). |
2222
| Auth / Security | 독립 애그리거트 없음 | Refresh Token 저장소, `UserPrincipal` | 토큰 발급/검증/갱신을 수행한다. 사용자 자체는 User Account 컨텍스트에 속한다. | Auth / Security는 도메인 애그리거트보다 보안 애플리케이션/인프라 컨텍스트다. |
@@ -86,11 +86,11 @@ createSocialUser() → PENDING
8686

8787
- `replaceInterests(List<EInterestCategory> categories, List<EInterestKeyword> keywords)` — 관심사 교체 시 "키워드는 선택된 카테고리에 속해야 한다"는 불변식을 Aggregate 내에서 검증하고 단일 트랜잭션으로 처리해야 한다. 현재 `InterestCommandService``UserInterestCategory`/`UserInterestKeyword` 리포지토리를 직접 조작하며 불변식 검증이 서비스 레이어에 산재되어 있다.
8888

89-
#### `UserProfileDocument` / Personalization Profile
89+
#### `PersonalizationProfileDocument` / Personalization Profile
9090

9191
- Personalization Profile 컨텍스트의 핵심 projection/read model.
9292
- 활동 데이터, 관심사, 게시글 신호를 바탕으로 검색/추천용 개인화 프로필을 생성한다.
93-
- 현재는 독립 write aggregate보다 `UserProfileService`가 생성하는 파생 모델에 가깝다.
93+
- 현재는 독립 write aggregate보다 `PersonalizationProfileService`가 생성하는 파생 모델에 가깝다.
9494
- 장기적으로는 `OnboardingCompleted`, `UserInterestsChanged`, `PersonalizedProfileGenerated` 이벤트 기반으로 분리할 후보다.
9595

9696
#### `ReadPost`, `Bookmark`, `SearchHistory`
@@ -130,7 +130,7 @@ createSocialUser() → PENDING
130130
| `User` | `socialType + socialId` 조합 | `SocialIdentity` | 두 필드가 항상 함께 쓰이고, 조합 유일성이 불변식이다. VO로 묶으면 `equals`/유일성 검증이 명확해진다. |
131131
| `User` | `nickName`, `email`, `profileImage`, `description` | `AccountProfile` | 계정 프로필 수정(`updateProfile()`)과 탈퇴 시 null 처리(`withdraw()`)가 같은 필드 묶음에 적용된다. |
132132
| `RecommendedPost` | `rankOrder` | `RankOrder` | 1..N 연속 불변식을 생성 시점에 검증하는 VO로 표현할 수 있다. |
133-
| `UserProfileDocument` | `profileVector` (float[]) | `EmbeddingVector` | 벡터 차원 수 검증, 유사도 계산 메서드를 VO에 위치시킬 수 있다. |
133+
| `PersonalizationProfileDocument` | `profileVector` (float[]) | `EmbeddingVector` | 벡터 차원 수 검증, 유사도 계산 메서드를 VO에 위치시킬 수 있다. |
134134
| `Post` | `summary`, `shortSummary` | `PostSummary` | `updateSummaries()` 단일 메서드로만 교체되는 두 필드를 VO로 묶으면 불변식이 명확해진다. |
135135

136136
> 주의: VO 추출은 "불변식 검증 책임이 어디에 있어야 하는가"를 기준으로 판단한다. 단순히 필드를 묶기 위한 추출은 오히려 복잡도를 높인다.
@@ -155,7 +155,7 @@ createSocialUser() → PENDING
155155
| P0 | 기술 게시글이 색인됨 | `TechnicalPostIndexed` | `PostEmbeddingWriter` | Search, Recommendation | 검색/추천 가능한 콘텐츠가 되었음을 나타내는 핵심 이벤트다. |
156156
| P0 | 온보딩이 완료됨 | `OnboardingCompleted` | `UserCommandService.completeOnboarding` | 개인화 프로필 생성 | 사용자 상태가 ACTIVE가 되고 관심사가 저장되어 개인화 프로필 생성이 가능해진다. |
157157
| P0 | 사용자 관심사가 변경됨 | `UserInterestsChanged` | `InterestCommandService.updateUserInterests` | 개인화 프로필 재생성, 추천 재생성 | 현재도 관심사 변경 후 개인화 프로필 생성이 호출된다. 이벤트로 분리하기 좋은 지점이다. |
158-
| P0 | 개인화 프로필이 생성됨 | `PersonalizedProfileGenerated` | `UserProfileService.generateUserProfileSync` | 추천 생성, 개인화 검색 준비 완료 | 현재 `UserProfileService`가 추천 생성을 직접 호출한다. 이벤트 분리 우선순위가 높다. |
158+
| P0 | 개인화 프로필이 생성됨 | `PersonalizedProfileGenerated` | `PersonalizationProfileService.generatePersonalizationProfileSync` | 추천 생성, 개인화 검색 준비 완료 | 현재 `PersonalizationProfileService`가 추천 생성을 직접 호출한다. 이벤트 분리 우선순위가 높다. |
159159
| P0 | 추천이 생성됨 | `RecommendationsGenerated` | `LlmRecommendationService.generateRecommendationsForUser` | Notification, Analytics | 사용자에게 보여줄 현재 추천 목록이 바뀌는 핵심 이벤트다. |
160160
| P1 | 기술 게시글을 읽음 | `TechnicalPostRead` | `ActivityCommandService.saveReadPost` | 개인화 프로필 갱신, 추천 정책 | 읽기 행동은 개인화 프로필과 읽은 게시글 제외 정책의 핵심 입력이다. |
161161
| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | 첫 읽기일 때 `Post.incrementViewCount` | 인기순 정렬, 분석 | 조회수 증가와 인기순 정렬에 직접 연결된다. |
@@ -187,7 +187,7 @@ createSocialUser() → PENDING
187187

188188
### 3.1 결정
189189

190-
현재 코드는 JPA `@ManyToOne`으로 다른 Aggregate의 Entity를 직접 참조하고 있다 (예: `Post → TechBlog`, `ScrabPost/Bookmark → User, Post`, `RecommendedPost → User, Post`).
190+
현재 코드는 JPA `@ManyToOne`으로 다른 Aggregate의 Entity를 직접 참조하고 있다 (예: `Post → TechBlog`, `Bookmark → User, Post`, `RecommendedPost → User, Post`).
191191

192192
**결정: 현재 모놀리스 구조에서는 JPA 직접 참조를 유지한다. 단, 서비스 레이어에서 다른 Aggregate를 변경하는 코드는 금지한다.**
193193

@@ -205,7 +205,7 @@ createSocialUser() → PENDING
205205

206206
### 3.3 예외 케이스
207207

208-
- `UserProfileService`는 현재 Personalization Profile 쪽 생성 책임을 맡는 Application Service다. `User`, `Activity`, `Recommendation` 등 여러 컨텍스트를 조합하지만, 각 Aggregate의 상태 변경은 해당 Aggregate의 도메인 메서드를 통해서만 수행해야 한다.
208+
- `PersonalizationProfileService`는 현재 Personalization Profile 쪽 생성 책임을 맡는 Application Service다. `User`, `Activity`, `Recommendation` 등 여러 컨텍스트를 조합하지만, 각 Aggregate의 상태 변경은 해당 Aggregate의 도메인 메서드를 통해서만 수행해야 한다.
209209
- 향후 컨텍스트 분리가 필요해지면 그 시점에 직접 참조를 ID 참조로 전환하고 ACL 또는 Anti-Corruption Layer를 도입한다.
210210

211211
---
@@ -277,7 +277,7 @@ skip count 임계값 초과 시 Step을 `FAILED`로 끝내는 skip limit 설정
277277

278278
### 4.4 Elasticsearch Projection 일관성 정책
279279

280-
`PostDocument`, `UserProfileDocument`는 RDB Aggregate의 read model projection이다.
280+
`PostDocument`, `PersonalizationProfileDocument`는 RDB Aggregate의 read model projection이다.
281281

282282
**현재 정책: 동기 즉시 갱신 (RDB 저장과 같은 트랜잭션 외부에서 ES 저장 직접 호출)**
283283

0 commit comments

Comments
 (0)