Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/tactical-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
| 컨텍스트 | 애그리거트 루트 | 내부 엔티티 / 값 객체 / Projection | 트랜잭션 내 보장 불변식 | 현재 코드 평가 |
|---|---|---|---|---|
| Source / Ingestion | `TechBlog` | `RssFeedItem`은 DTO/ACL 결과 | `blogUrl`과 `rssUrl`은 유일해야 한다. `lastCrawledAt`은 `markCrawled(LocalDateTime)`으로만 갱신된다. 기술 블로그는 RSS 수집 대상의 기준이다. | `TechBlog`가 Source 컨텍스트의 루트로 적절하다. **`markCrawled(LocalDateTime)` 도메인 메서드 누락** — 현재 Anemic Model 위험. |
| 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 참조). |
| Post / Content | `Post` | `PostKeyword`, `PostDocument`, `ContentChunk` | URL은 유일해야 한다. 요약/짧은 요약은 `updateSummaries()`로만 교체된다. 키워드는 `clearKeywords() + addKeyword()` 조합으로만 교체된다. 임베딩 완료 시각은 `markAsEmbedded(LocalDateTime)`으로만 기록된다. 조회수 증가는 `PostViewCountCommandService`/`PostRepository`의 SQL atomic UPDATE 경로로만 처리한다. | `Post`가 핵심 애그리거트 루트다. `PostKeyword`는 `Post` 내부 컬렉션으로 보는 것이 자연스럽다. 조회수 증가는 aggregate 필드 증가가 아니라 command/repository 경로를 canonical write path로 본다 (§1.2 참조). |
| User Account | `User` | `UserInterestCategory`, `UserInterestKeyword` | `socialType + socialId` 조합은 유일해야 한다. 상태 전이는 `PENDING → ACTIVE → WITHDRAWN → PENDING(재활성화)` 경로만 허용된다. 관심 키워드는 반드시 선택된 관심 카테고리에 속해야 한다. 관심사 교체는 `replaceInterests()`로 단일 트랜잭션 내 불변식 검증과 함께 처리된다. | `User`가 루트다. 계정/온보딩/관심사 불변식을 소유한다. **`replaceInterests()` 도메인 메서드 누락** — 불변식 검증이 서비스 레이어에 산재. |
| Personalization Profile | 명시적 쓰기 애그리거트 없음 | `PersonalizationProfileDocument`, `UserActivityData` | 같은 `userId` 기준 현재 개인화 프로필 projection은 하나만 유지된다. 프로필 텍스트, 벡터, 핵심 키워드는 함께 재생성된다. | Personalization Profile은 aggregate보다 read model / application service 중심 컨텍스트다. 현재 `PersonalizationProfileService`가 생성 책임을 가진다. |
| Activity | `ReadPost`, `Bookmark`, `SearchHistory` | `FirstReadPost` | `Bookmark`는 `userId + postId` 조합이 유일해야 한다. `ReadPost`는 같은 사용자+게시글 중복 저장을 허용한다. `FirstReadPost`는 `userId + postId` 유니크 제약으로 "조회수 증가 자격"을 한 번만 부여하는 dedupe ledger 역할을 한다. `SearchHistory`는 같은 검색어를 중복 저장한다 (동일 검색어의 반복 횟수 자체가 개인화 관심 신호가 된다). 행동 기록은 삭제되지 않고 보존된다 (북마크 제외). | 각 행동 기록이 독립 record aggregate처럼 동작한다. 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `activity/<slice>` 아래에서 `presentation / application / domain / infrastructure` 구조로 정리되었다. `ReadPost`는 `SaveReadPostCommand`, `GetReadPostsQuery`, `ReadPostConverter`, `BookmarkLookupService`를 통해 저장/조회/북마크 여부 조합을 분담하고, `ReadPostFirstReadPolicy.markFirstRead()` + `first_read_posts` 유니크 제약으로 최초 조회수 증가를 보호한다. 목록 조회 `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` 같은 경계 재설계는 후속 단계로 미룬다. |
Expand Down Expand Up @@ -54,7 +54,7 @@
int incrementViewCount(@Param("id") Long id);
```
- `@Version` 낙관적 락은 재시도 비용이 발생하고 조회수 같은 통계성 필드에는 부적합하므로 채택하지 않는다.
- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostCommandService`/`PostRepository` 경로를 canonical write path로 둔다.
- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostViewCountCommandService`/`PostRepository` 경로를 canonical write path로 둔다.
- 사용자별 중복 카운트 방지는 `activity/readpost`의 `first_read_posts(user_id, post_id)` 유니크 제약으로 처리한다.
- `ReadPostCommandService`는 `ReadPostFirstReadPolicy.markFirstRead()`가 성공한 경우에만 조회수를 증가시킨다.
- `markFirstRead()`는 duplicate key만 "이미 읽음"으로 번역하고, 조회수 증가가 실패하면 `ReadPostErrorCode.READ_POST_VIEW_COUNT_INCREMENT_FAILED`를 던져 `first_read_posts` 마킹과 `read_posts` 저장이 함께 롤백되도록 한다.
Expand Down Expand Up @@ -161,7 +161,7 @@ createSocialUser() → PENDING
| P0 | 개인화 프로필이 생성됨 | `PersonalizedProfileGenerated` | `PersonalizationProfileService.generatePersonalizationProfileSync` | 추천 생성, 개인화 검색 준비 완료 | 현재 `PersonalizationProfileService`가 추천 생성을 직접 호출한다. 이벤트 분리 우선순위가 높다. |
| P0 | 추천이 생성됨 | `RecommendationsGenerated` | `LlmRecommendationService.generateRecommendationsForUser` | Notification, Analytics | 사용자에게 보여줄 현재 추천 목록이 바뀌는 핵심 이벤트다. |
| P1 | 기술 게시글을 읽음 | `TechnicalPostRead` | `ReadPostCommandService.saveReadPost` | 개인화 프로필 갱신, 추천 정책 | 읽기 행동은 개인화 프로필과 읽은 게시글 제외 정책의 핵심 입력이다. |
| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | `ReadPostFirstReadPolicy.markFirstRead` + `PostCommandService.incrementViewCount` | 인기순 정렬, 분석 | `first_read_posts` dedupe ledger를 통과한 최초 읽기에서만 발생하며 조회수 증가와 인기순 정렬에 직접 연결된다. |
| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | `ReadPostFirstReadPolicy.markFirstRead` + `PostViewCountCommandService.incrementViewCount` | 인기순 정렬, 분석 | `first_read_posts` dedupe ledger를 통과한 최초 읽기에서만 발생하며 조회수 증가와 인기순 정렬에 직접 연결된다. |
| P1 | 기술 게시글을 북마크함 | `TechnicalPostBookmarked` | `BookmarkCommandService.addBookmark` | 개인화 프로필 갱신, 추천 튜닝 | 강한 선호 신호로 개인화 품질에 중요하다. |
| P1 | 북마크가 해제됨 | `BookmarkRemoved` | `BookmarkCommandService.deleteBookmark` | 개인화 프로필 갱신, 추천 튜닝 | 선호 신호 제거로 볼 수 있다. |
| P1 | 검색어가 기록됨 | `SearchQueryRecorded` | `saveSearchHistory` | 개인화 프로필 갱신, 검색 분석 | 검색 의도는 개인화 프로필의 주요 입력이다. |
Expand Down
3 changes: 2 additions & 1 deletion docs/test-gap-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
현재 working tree에는 다음 untracked 테스트 디렉터리가 있다.

```text
src/test/java/com/techfork/domain/post/batch/
src/test/java/com/techfork/post/application/batch/
src/test/java/com/techfork/post/infrastructure/batch/
```

포함 파일:
Expand Down
2 changes: 1 addition & 1 deletion docs/ubiquitous-language/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
| 바운디드 컨텍스트 | 문서 | 현재 owning package | 메모 |
|---|---|---|---|
| Source / Ingestion | [`source-ingestion.md`](./source-ingestion.md) | `src/main/java/com/techfork/domain/source` | RSS 수집, 소스 블로그, 파이프라인 시작점 |
| Post / Content | [`post-content.md`](./post-content.md) | `src/main/java/com/techfork/domain/post` | 기술 게시글 본문, 요약, 키워드, 검색 projection |
| Post / Content | [`post-content.md`](./post-content.md) | `src/main/java/com/techfork/post` | 기술 게시글 본문, 요약, 키워드, 검색 projection |
| User Account | [`user-account.md`](./user-account.md) | `src/main/java/com/techfork/domain/useraccount` | 계정, 온보딩, 관심사, 계정 프로필 |
| Personalization Profile | [`personalization-profile.md`](./personalization-profile.md) | `src/main/java/com/techfork/domain/personalization` | 개인화 프로필 생성, 벡터, 핵심 키워드, 재생성 |
| Activity | [`activity.md`](./activity.md) | `src/main/java/com/techfork/activity` | 읽기/검색/북마크 행동 기록 |
Expand Down
14 changes: 7 additions & 7 deletions docs/ubiquitous-language/activity.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
|---|---|---|
| 읽기 기록 | `ReadPost` | 사용자와 기술 게시글의 읽기 이벤트 레코드 |
| 최초 읽기 마킹 | `ReadPostFirstReadPolicy.markFirstRead` | `first_read_posts` 유니크 제약을 이용해 조회수 증가 여부를 결정하는 정책 |
| 조회수 증가 위임 | `PostCommandService.incrementViewCount` | 첫 읽기일 때 Post 컨텍스트에 조회수 증가를 위임하는 command 경로 |
| 조회수 증가 위임 | `PostViewCountCommandService.incrementViewCount` | 첫 읽기일 때 Post 컨텍스트에 조회수 증가를 위임하는 command 경로 |
| 최초 읽기 ledger | `FirstReadPost`, `first_read_posts` | 사용자별 게시글 조회수 dedupe를 담당하는 보조 record |
| 북마크 레코드 | `Bookmark` (legacy name: `ScrabPost`) | 북마크 저장 레코드의 현재 표준 이름과 과거 이름을 함께 설명한다 |
| 검색 기록 레코드 | `SearchHistory` | 사용자 검색어를 시간순으로 남기는 레코드 |
Expand All @@ -58,13 +58,13 @@
## 현재 구조 메모

- 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `presentation / application / domain / infrastructure` 기준으로 정리되어 있다.
- `ReadPost` 저장은 `UserLookupService`, `PostLookupService`, `ReadPostFirstReadPolicy`, `PostCommandService`를 조합해 읽기 이력 저장과 조회수 증가를 분리한다.
- `ReadPost` 저장은 `UserLookupService`, `PostLookupService`, `ReadPostFirstReadPolicy`, `PostViewCountCommandService`를 조합해 읽기 이력 저장과 조회수 증가를 분리한다.
- `ReadPostFirstReadPolicy.markFirstRead()`는 `first_read_posts(user_id, post_id)` 유니크 제약을 first-read 판정의 단일 진실 원천으로 사용한다.
- `ReadPostCommandService`는 first-read 마킹이 성공했을 때만 `PostCommandService.incrementViewCount()`를 호출하고, 조회수 증가 실패 시 예외를 던져 전체 트랜잭션을 롤백한다.
- `ReadPostCommandService`는 first-read 마킹이 성공했을 때만 `PostViewCountCommandService.incrementViewCount()`를 호출하고, 조회수 증가 실패 시 예외를 던져 전체 트랜잭션을 롤백한다.
- `ReadPost` 조회는 `bookmark.infrastructure.BookmarkRepository`를 직접 참조하지 않고 `bookmark.application.query.lookup.BookmarkLookupService`를 통해 북마크 여부를 조합한다.
- `ReadPost` 목록 조회 `size`는 HTTP layer에서 `1..100`으로 검증한다.
- `SearchHistory` 저장은 `SearchHistoryRequest -> SaveSearchHistoryCommand -> ReadHistoryCommandService` 흐름을 따른다.
- Activity application 서비스의 cross-context 조회/명령 의존은 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`, `PostCommandService`를 통해 application 간 의존으로 정리되어 있다.
- Activity application 서비스의 cross-context 조회/명령 의존은 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`, `PostViewCountCommandService`를 통해 application 간 의존으로 정리되어 있다.
- aggregate/value object 강화, hexagonal architecture(포트/어댑터), `ManyToOne -> ID reference` 전환은 후속 정리 범위다.

## 주요 근거 파일
Expand All @@ -90,9 +90,9 @@
- `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkConverter.java`
- `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkController.java`
- `src/main/java/com/techfork/domain/useraccount/service/UserLookupService.java`
- `src/main/java/com/techfork/domain/post/service/PostLookupService.java`
- `src/main/java/com/techfork/domain/post/service/PostCommandService.java`
- `src/main/java/com/techfork/domain/post/service/PostKeywordLookupService.java`
- `src/main/java/com/techfork/post/application/query/lookup/PostLookupService.java`
- `src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java`
- `src/main/java/com/techfork/post/application/query/lookup/PostKeywordLookupService.java`
- `src/main/java/com/techfork/activity/readhistory/application/command/ReadHistoryCommandService.java`
- `src/main/java/com/techfork/activity/readhistory/application/command/SaveSearchHistoryCommand.java`
- `src/main/java/com/techfork/activity/readhistory/presentation/SearchHistoryRequest.java`
Expand Down
2 changes: 1 addition & 1 deletion docs/ubiquitous-language/admin-ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Owning packages

- `src/main/java/com/techfork/domain/admin`
- 연관 운영 서비스: `src/main/java/com/techfork/domain/source`, `src/main/java/com/techfork/domain/post/batch`
- 연관 운영 서비스: `src/main/java/com/techfork/domain/source`, `src/main/java/com/techfork/post/application/batch`, `src/main/java/com/techfork/post/infrastructure/batch`

## 표준 용어

Expand Down
28 changes: 14 additions & 14 deletions docs/ubiquitous-language/post-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Owning packages

- `src/main/java/com/techfork/domain/post`
- `src/main/java/com/techfork/post`

## 표준 용어

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

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

## 혼동 금지

Expand All @@ -66,11 +66,11 @@

## 주요 근거 파일

- `src/main/java/com/techfork/domain/post/entity/Post.java`
- `src/main/java/com/techfork/domain/post/entity/PostKeyword.java`
- `src/main/java/com/techfork/domain/post/service/PostCommandService.java`
- `src/main/java/com/techfork/domain/post/repository/PostRepository.java`
- `src/main/java/com/techfork/domain/post/document/PostDocument.java`
- `src/main/java/com/techfork/domain/post/document/ContentChunk.java`
- `src/main/java/com/techfork/domain/post/batch/PostSummaryProcessor.java`
- `src/main/java/com/techfork/domain/post/batch/PostEmbeddingProcessor.java`
- `src/main/java/com/techfork/post/domain/Post.java`
- `src/main/java/com/techfork/post/domain/PostKeyword.java`
- `src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java`
- `src/main/java/com/techfork/post/infrastructure/PostRepository.java`
- `src/main/java/com/techfork/post/domain/projection/PostDocument.java`
- `src/main/java/com/techfork/post/domain/projection/ContentChunk.java`
- `src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java`
- `src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java`
2 changes: 1 addition & 1 deletion docs/ubiquitous-language/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Owning packages

- `src/main/java/com/techfork/domain/search`
- 관련 read model: `src/main/java/com/techfork/domain/post/document`, `src/main/java/com/techfork/domain/personalization/document`
- 관련 read model: `src/main/java/com/techfork/post/domain/projection`, `src/main/java/com/techfork/domain/personalization/document`

## 표준 용어

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import com.techfork.activity.bookmark.domain.Bookmark;
import com.techfork.activity.bookmark.domain.BookmarkErrorCode;
import com.techfork.activity.bookmark.infrastructure.BookmarkRepository;
import com.techfork.domain.post.entity.Post;
import com.techfork.domain.post.service.PostLookupService;
import com.techfork.post.domain.Post;
import com.techfork.post.application.query.lookup.PostLookupService;
import com.techfork.domain.useraccount.entity.User;
import com.techfork.domain.useraccount.service.UserLookupService;
import com.techfork.global.exception.GeneralException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.techfork.activity.bookmark.infrastructure.BookmarkQueryRow;
import com.techfork.activity.bookmark.infrastructure.BookmarkRepository;
import com.techfork.domain.post.service.PostKeywordLookupService;
import com.techfork.post.application.query.lookup.PostKeywordLookupService;
import com.techfork.domain.useraccount.entity.User;
import com.techfork.domain.useraccount.service.UserLookupService;
import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer;
Expand Down
Loading