Skip to content

Commit c189e00

Browse files
committed
docs: 문서 최신화
1 parent fac0070 commit c189e00

4 files changed

Lines changed: 17 additions & 7 deletions

File tree

docs/tactical-design.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
| 컨텍스트 | 애그리거트 루트 | 내부 엔티티 / 값 객체 / Projection | 트랜잭션 내 보장 불변식 | 현재 코드 평가 |
1414
|---|---|---|---|---|
1515
| Source / Ingestion | `TechBlog` | `RssFeedItem`은 DTO/ACL 결과 | `blogUrl``rssUrl`은 유일해야 한다. `lastCrawledAt``markCrawled(LocalDateTime)`으로만 갱신된다. 기술 블로그는 RSS 수집 대상의 기준이다. | `TechBlog`가 Source 컨텍스트의 루트로 적절하다. **`markCrawled(LocalDateTime)` 도메인 메서드 누락** — 현재 Anemic Model 위험. |
16-
| Post / Content | `Post` | `PostKeyword`, `PostDocument`, `ContentChunk` | URL은 유일해야 한다. 요약/짧은 요약은 `updateSummaries()`로만 교체된다. 키워드는 `clearKeywords() + addKeyword()` 조합으로만 교체된다. 임베딩 완료 시각은 `markAsEmbedded(LocalDateTime)`으로만 기록된다. `incrementViewCount()`는 비원자적 연산이므로 SQL atomic UPDATE 정책 적용 필요. | `Post`가 핵심 애그리거트 루트다. `PostKeyword``Post` 내부 컬렉션으로 보는 것이 자연스럽다. **`incrementViewCount()` 동시성 정책 미결정** (§1.2 참조). |
16+
| Post / Content | `Post` | `PostKeyword`, `PostDocument`, `ContentChunk` | URL은 유일해야 한다. 요약/짧은 요약은 `updateSummaries()`로만 교체된다. 키워드는 `clearKeywords() + addKeyword()` 조합으로만 교체된다. 임베딩 완료 시각은 `markAsEmbedded(LocalDateTime)`으로만 기록된다. 조회수 증가는 `PostCommandService`/`PostRepository`SQL atomic UPDATE 경로로만 처리한다. | `Post`가 핵심 애그리거트 루트다. `PostKeyword``Post` 내부 컬렉션으로 보는 것이 자연스럽다. 조회수 증가는 aggregate 필드 증가가 아니라 command/repository 경로를 canonical write path로 본다 (§1.2 참조). |
1717
| User Account | `User` | `UserInterestCategory`, `UserInterestKeyword` | `socialType + socialId` 조합은 유일해야 한다. 상태 전이는 `PENDING → ACTIVE → WITHDRAWN → PENDING(재활성화)` 경로만 허용된다. 관심 키워드는 반드시 선택된 관심 카테고리에 속해야 한다. 관심사 교체는 `replaceInterests()`로 단일 트랜잭션 내 불변식 검증과 함께 처리된다. | `User`가 루트다. 계정/온보딩/관심사 불변식을 소유한다. **`replaceInterests()` 도메인 메서드 누락** — 불변식 검증이 서비스 레이어에 산재. |
1818
| Personalization Profile | 명시적 쓰기 애그리거트 없음 | `PersonalizationProfileDocument`, `UserActivityData` | 같은 `userId` 기준 현재 개인화 프로필 projection은 하나만 유지된다. 프로필 텍스트, 벡터, 핵심 키워드는 함께 재생성된다. | Personalization Profile은 aggregate보다 read model / application service 중심 컨텍스트다. 현재 `PersonalizationProfileService`가 생성 책임을 가진다. |
1919
| Activity | `ReadPost`, `Bookmark`, `SearchHistory` | 없음 | `Bookmark`는 `userId + postId` 조합이 유일해야 한다. `ReadPost`는 같은 사용자+게시글 중복 저장을 허용하되 `ReadPostFirstReadPolicy.isFirstRead()`로 최초 읽기를 구분한다. `SearchHistory`는 같은 검색어를 중복 저장한다 (동일 검색어의 반복 횟수 자체가 개인화 관심 신호가 된다). 행동 기록은 삭제되지 않고 보존된다 (북마크 제외). | 각 행동 기록이 독립 record aggregate처럼 동작한다. 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `activity/<slice>` 아래에서 `presentation / application / domain / infrastructure` 구조로 정리되었다. `ReadPost`는 `SaveReadPostCommand`, `GetReadPostsQuery`, `ReadPostConverter`, `BookmarkLookupService`를 통해 저장/조회/북마크 여부 조합을 분담하고, 목록 조회 `size`는 HTTP layer에서 `1..100`으로 검증한다. `SearchHistory`는 `SearchHistoryRequest`, `SaveSearchHistoryCommand`, `ReadHistoryCommandService`로 저장 흐름을 분리했다. 또한 Activity application 서비스의 cross-context 조회는 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`를 통해 application 간 의존으로 정리되었다. aggregate/value object 강화, hexagonal port/adaptor 적용, `ManyToOne -> id reference` 같은 경계 재설계는 후속 단계로 미룬다. |
@@ -43,17 +43,18 @@
4343
- `PostKeyword`는 독립 루트라기보다 `Post`의 키워드 컬렉션이다.
4444
- `PostDocument`, `ContentChunk`는 Elasticsearch 검색/추천용 projection이지 RDB 애그리거트 루트가 아니다.
4545

46-
**`incrementViewCount()` 동시성 정책**
46+
**`viewCount` 동시성 정책**
4747

48-
현재 `viewCount++` 연산은 JPA dirty checking 기반 비원자적 업데이트다. 동시 요청 시 Lost Update가 발생한다.
48+
기존 `viewCount++` 연산은 JPA dirty checking 기반 비원자적 업데이트라 동시 요청 시 Lost Update가 발생한다.
4949

5050
- **결정**: SQL atomic UPDATE 방식 적용
5151
```java
5252
@Modifying
5353
@Query("UPDATE Post p SET p.viewCount = p.viewCount + 1 WHERE p.id = :id")
54-
void incrementViewCount(@Param("id") Long id);
54+
int incrementViewCount(@Param("id") Long id);
5555
```
5656
- `@Version` 낙관적 락은 재시도 비용이 발생하고 조회수 같은 통계성 필드에는 부적합하므로 채택하지 않는다.
57+
- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostCommandService`/`PostRepository` 경로를 canonical write path로 둔다.
5758
- 현재 `isFirstRead` 체크로 사용자 중복 카운트는 방지하고 있으나, 다수 사용자 동시 접근 시 레이스 컨디션은 여전히 존재한다.
5859

5960
**누락된 도메인 메서드**
@@ -158,7 +159,7 @@ createSocialUser() → PENDING
158159
| P0 | 개인화 프로필이 생성됨 | `PersonalizedProfileGenerated` | `PersonalizationProfileService.generatePersonalizationProfileSync` | 추천 생성, 개인화 검색 준비 완료 | 현재 `PersonalizationProfileService`가 추천 생성을 직접 호출한다. 이벤트 분리 우선순위가 높다. |
159160
| P0 | 추천이 생성됨 | `RecommendationsGenerated` | `LlmRecommendationService.generateRecommendationsForUser` | Notification, Analytics | 사용자에게 보여줄 현재 추천 목록이 바뀌는 핵심 이벤트다. |
160161
| P1 | 기술 게시글을 읽음 | `TechnicalPostRead` | `ReadPostCommandService.saveReadPost` | 개인화 프로필 갱신, 추천 정책 | 읽기 행동은 개인화 프로필과 읽은 게시글 제외 정책의 핵심 입력이다. |
161-
| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | `ReadPostFirstReadPolicy.isFirstRead` + `Post.incrementViewCount` | 인기순 정렬, 분석 | 조회수 증가와 인기순 정렬에 직접 연결된다. |
162+
| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | `ReadPostFirstReadPolicy.isFirstRead` + `PostCommandService.incrementViewCount` | 인기순 정렬, 분석 | 조회수 증가와 인기순 정렬에 직접 연결된다. |
162163
| P1 | 기술 게시글을 북마크함 | `TechnicalPostBookmarked` | `BookmarkCommandService.addBookmark` | 개인화 프로필 갱신, 추천 튜닝 | 강한 선호 신호로 개인화 품질에 중요하다. |
163164
| P1 | 북마크가 해제됨 | `BookmarkRemoved` | `BookmarkCommandService.deleteBookmark` | 개인화 프로필 갱신, 추천 튜닝 | 선호 신호 제거로 볼 수 있다. |
164165
| P1 | 검색어가 기록됨 | `SearchQueryRecorded` | `saveSearchHistory` | 개인화 프로필 갱신, 검색 분석 | 검색 의도는 개인화 프로필의 주요 입력이다. |

docs/ubiquitous-language/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
| `markAsisClicked → markAsClicked` 오타 수정 | [`recommendation.md`](./recommendation.md) | 미반영 | 메서드명/호출부 수정 |
7474
| `TechBlog.markCrawled()` 추가 | [`source-ingestion.md`](./source-ingestion.md) | 미반영 | 도메인 메서드 추가 + 호출부 연결 |
7575
| `User.replaceInterests()` 추가 | [`user-account.md`](./user-account.md) | 미반영 | aggregate 불변식 검증을 도메인 메서드로 이동 |
76-
| `Post.incrementViewCount()` SQL atomic UPDATE | [`post-content.md`](./post-content.md) | 미반영 | Repository atomic update 도입 |
76+
| `Post viewCount` SQL atomic UPDATE 경로 정리 | [`post-content.md`](./post-content.md) | 반영 | `isFirstRead` race / idempotency는 별도 이슈로 해결 |
7777
| `EDifficultyLevel` 제거 | [`post-content.md`](./post-content.md) | 반영 | 필요 시 정책과 함께 재도입 검토 |
7878

7979
---

docs/ubiquitous-language/activity.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
|---|---|---|
3535
| 읽기 기록 | `ReadPost` | 사용자와 기술 게시글의 읽기 이벤트 레코드 |
3636
| 최초 읽기 판별 | `ReadPostFirstReadPolicy.isFirstRead` | 조회수 증가 여부를 결정하는 정책 |
37+
| 조회수 증가 위임 | `PostCommandService.incrementViewCount` | 첫 읽기일 때 Post 컨텍스트에 조회수 증가를 위임하는 command 경로 |
3738
| 북마크 레코드 | `Bookmark` (legacy name: `ScrabPost`) | 북마크 저장 레코드의 현재 표준 이름과 과거 이름을 함께 설명한다 |
3839
| 검색 기록 레코드 | `SearchHistory` | 사용자 검색어를 시간순으로 남기는 레코드 |
3940
| 북마크 여부 조합값 | `isBookmarked` | Search/Post/Recommendation 응답 조립 시 붙는 파생 값 |
@@ -56,10 +57,11 @@
5657
## 현재 구조 메모
5758

5859
- 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `presentation / application / domain / infrastructure` 기준으로 정리되어 있다.
60+
- `ReadPost` 저장은 `UserLookupService`, `PostLookupService`, `ReadPostFirstReadPolicy`, `PostCommandService`를 조합해 첫 읽기 판별과 조회수 증가를 분리한다.
5961
- `ReadPost` 조회는 `bookmark.infrastructure.BookmarkRepository`를 직접 참조하지 않고 `bookmark.application.query.lookup.BookmarkLookupService`를 통해 북마크 여부를 조합한다.
6062
- `ReadPost` 목록 조회 `size`는 HTTP layer에서 `1..100`으로 검증한다.
6163
- `SearchHistory` 저장은 `SearchHistoryRequest -> SaveSearchHistoryCommand -> ReadHistoryCommandService` 흐름을 따른다.
62-
- Activity application 서비스의 cross-context 조회는 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`를 통해 application 간 의존으로 정리되어 있다.
64+
- Activity application 서비스의 cross-context 조회/명령 의존은 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`, `PostCommandService`를 통해 application 간 의존으로 정리되어 있다.
6365
- aggregate/value object 강화, hexagonal architecture(포트/어댑터), `ManyToOne -> ID reference` 전환은 후속 정리 범위다.
6466

6567
## 주요 근거 파일
@@ -82,6 +84,7 @@
8284
- `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkController.java`
8385
- `src/main/java/com/techfork/domain/useraccount/service/UserLookupService.java`
8486
- `src/main/java/com/techfork/domain/post/service/PostLookupService.java`
87+
- `src/main/java/com/techfork/domain/post/service/PostCommandService.java`
8588
- `src/main/java/com/techfork/domain/post/service/PostKeywordLookupService.java`
8689
- `src/main/java/com/techfork/activity/readhistory/application/command/ReadHistoryCommandService.java`
8790
- `src/main/java/com/techfork/activity/readhistory/application/command/SaveSearchHistoryCommand.java`

docs/ubiquitous-language/post-content.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
| 수집일 | `crawledAt` | TechFork가 해당 기술 게시글을 수집한 시각 |
2222
| 임베딩 완료일 | `embeddedAt` | 기술 게시글이 임베딩되어 Elasticsearch에 색인된 시각 |
2323
| 조회수 | `viewCount` | 사용자가 처음 읽은 경우 증가하는 popularity 지표 |
24+
| 조회수 증가 경로 | `PostCommandService.incrementViewCount()` | production에서 조회수를 증가시키는 canonical command 경로 |
2425
| 검색 문서 | `PostDocument` | Elasticsearch `posts` 인덱스에 저장되는 기술 게시글 projection |
2526
| 콘텐츠 청크 | `ContentChunk` | 긴 본문을 임베딩 검색용으로 분할한 단위 |
2627
| 출처명 | `Post.company`, `TechBlog.companyName` | 기술 게시글이 어느 기술 블로그/회사에서 왔는지 표시하기 위한 이름 |
@@ -31,6 +32,8 @@
3132
- 도메인/기획 문서에서는 `Post`**기술 게시글**로 부른다.
3233
- `PostDocument`, `ContentChunk`는 aggregate가 아니라 **검색/추천용 projection**이다.
3334
- `Post.company`는 Source 컨텍스트의 출처명을 복사한 조회용 스냅샷이다.
35+
- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostCommandService`/`PostRepository`의 SQL atomic update를 canonical write path로 둔다.
36+
- `PostCommandService.incrementViewCount()`는 DB 값을 원자적으로 증가시키지만, 이미 로드된 managed `Post``viewCount`를 같은 트랜잭션 안에서 최신 상태로 동기화하지는 않는다.
3437
- `EDifficultyLevel`은 실제 사용처가 없어 제거되었다. 필요해지면 정책과 함께 재도입한다.
3538

3639
## 내부 glossary
@@ -43,6 +46,7 @@
4346
| 청크 projection | `ContentChunk` | 긴 본문을 분할한 임베딩 검색 단위 |
4447
| 임베딩 완료 시각 | `embeddedAt` | 검색/추천용 색인 준비 완료 시각 |
4548
| 인기 지표 | `viewCount` | 조회수 기반 popularity 지표 |
49+
| 조회수 증가 command | `PostCommandService.incrementViewCount` | 조회수 증가를 DB atomic update로 위임하는 application command |
4650

4751
## 혼동 금지
4852

@@ -63,6 +67,8 @@
6367

6468
- `src/main/java/com/techfork/domain/post/entity/Post.java`
6569
- `src/main/java/com/techfork/domain/post/entity/PostKeyword.java`
70+
- `src/main/java/com/techfork/domain/post/service/PostCommandService.java`
71+
- `src/main/java/com/techfork/domain/post/repository/PostRepository.java`
6672
- `src/main/java/com/techfork/domain/post/document/PostDocument.java`
6773
- `src/main/java/com/techfork/domain/post/document/ContentChunk.java`
6874
- `src/main/java/com/techfork/domain/post/batch/PostSummaryProcessor.java`

0 commit comments

Comments
 (0)