목적: TechFork 서버를 DDD 관점으로 점진적으로 개선하면서, 아직 부족한 테스트 코드를 어떤 순서로 작성·개선할지 정리한다.
관련 문서:docs/ubiquitous-language/README.md
현재 프로젝트에서는 다음 순서를 추천한다.
1. DDD 목표 모델 확정
2. 테스트 갭 분석
3. 현재 동작 보호용 핵심 테스트 작성
4. 용어 리팩터링
5. 컨텍스트별 DDD 리팩터링
6. 테스트를 도메인/유스케이스 중심으로 재구성
7. 이벤트, ACL, 포트 기반으로 컨텍스트 결합 낮추기
중요한 점은 다음과 같다.
- 테스트를 전부 완성한 뒤 DDD로 전환하지 않는다.
- DDD 구조를 먼저 대규모로 바꾸지도 않는다.
- 대신 바꿀 슬라이스마다 현재 동작을 보호하는 테스트를 먼저 작성하고, 그 범위 안에서 DDD 리팩터링을 진행한다.
즉, 전략은 다음과 같다.
DDD 목표 지도 작성
→ 바꿀 영역 선택
→ 해당 영역의 현재 동작 보호 테스트 작성
→ 작은 리팩터링
→ 테스트 개선
→ 다음 영역으로 이동
현재 TechFork는 다음 특징을 가진다.
- 기능은 이미 여러 도메인으로 나뉘어 있다.
- 하지만 코드상 컨텍스트 간 직접 의존이 많다.
- 테스트 커버리지가 아직 충분하지 않다.
- 용어가 일부 혼재되어 있다.
- 예:
ScrabPost,scrap_posts,Bookmark - 예:
searchWord,query,keyKeywords,PostKeyword - 예: 계정 프로필과 개인화 프로필
- 예:
- Search, Recommendation, Personalization Profile(
PersonalizationProfileService) 쪽은 여러 컨텍스트와 외부 인프라가 얽혀 있어 리팩터링 위험이 크다.
따라서 안전한 전환 전략은 다음이다.
- 방향성은 DDD 문서로 먼저 고정한다.
- 현재 동작을 깨뜨리지 않도록 회귀 테스트를 작성한다.
- 작고 의미 있는 컨텍스트부터 정리한다.
- 테스트 구조를 점진적으로 DDD 스타일로 개선한다.
이미 진행된 작업이다.
산출물:
docs/ubiquitous-language/README.mddocs/ubiquitous-language/(컨텍스트별 glossary)
포함되어야 할 내용:
- 비즈니스 도메인
- 핵심/지원/일반 하위 도메인 분류
- 유비쿼터스 언어
- 바운디드 컨텍스트
- Context Map
- 컨텍스트 간 의존 방향
- 컨텍스트 간 통신 패턴
- Shared Kernel
- ACL
- Projection / Read Model
- Query Composition
- 동기 직접 호출
- 이벤트 후보
- 애그리거트 루트 식별
- 도메인 이벤트 후보와 우선순위
- 용어 결정사항
현재 결정된 주요 기준:
Post는 도메인 문서에서 기술 게시글로 설명한다.ScrabPost,scrap_posts,Bookmark는 북마크로 통일한다.TechBlog는 Source 컨텍스트의 RSS 소스/출처 애그리거트 루트로 유지한다.Post.company는TechBlog.companyName의 비정규화 스냅샷으로 본다.- 사용자 입력은 검색어/SearchQuery로 부른다.
- 프로필 대표어는 핵심 키워드/KeyKeyword로 부른다.
- 게시글 대표어는 게시글 키워드/PostKeyword로 부른다.
- 전략 문서와 glossary에서는
User Account와Personalization Profile을 분리한다. EDifficultyLevel은 실제 사용처가 없어 제거 완료된 상태로 본다.
DDD 리팩터링에 들어가기 전에 테스트 현황을 먼저 파악한다.
권장 산출물:
docs/test-gap-analysis.md
분석 항목:
1. 현재 존재하는 테스트
2. 컨텍스트별 누락 테스트
3. 리팩터링 전에 반드시 필요한 테스트
4. DDD 전환 후 다시 정리할 테스트
5. 외부 인프라 의존으로 인해 별도 전략이 필요한 테스트
컨텍스트별로 다음을 확인한다.
| 컨텍스트 | 확인할 테스트 |
|---|---|
| Activity | 읽기 기록, 조회수 증가, 북마크 추가/삭제, 검색 기록 저장 |
| Post / Content | 기술 게시글 생성, 요약 갱신, 키워드 갱신, 조회수 증가, PostDocument 생성 |
| User Account | 소셜 사용자 생성, 온보딩, 관심사 저장, 관심 키워드 검증, 계정 프로필 수정, 탈퇴 |
| Personalization Profile | 활동 데이터 기반 개인화 프로필 생성, 프로필 벡터/핵심 키워드 생성, 재생성 |
| Search | 일반 검색, 개인화 검색, fallback, RRF, 검색 결과 metadata 조립 |
| Recommendation | 후보군 생성, 읽은 게시글 제외, MMR, 기존 추천 이력화, 새 추천 저장 |
| Source / Ingestion | RSS 수집, 중복 URL 제거, RssFeedItem 변환, Post 저장 |
| Auth / Security | 토큰 발급, refresh, logout, 탈퇴 사용자 차단 |
이 단계의 목표는 전체 커버리지를 올리는 것이 아니다.
목표는 다음이다.
DDD 리팩터링 중 현재 동작이 깨지면 바로 알 수 있는 안전망을 만든다.
이 테스트들은 처음부터 완벽한 DDD 테스트일 필요는 없다.
서비스 메서드 중심 테스트라도 괜찮다.
리팩터링 전에 우선 작성해야 하는 테스트다.
| 우선순위 | 영역 | 테스트 목적 |
|---|---|---|
| P0 | Activity | 첫 읽기일 때만 조회수 증가 |
| P0 | Activity | 북마크 추가/중복 방지/삭제 |
| P0 | Activity | 검색 기록 저장 |
| P0 | User | 온보딩 완료 시 사용자 상태 ACTIVE 전환 |
| P0 | User | 관심 카테고리와 관심 키워드 저장 |
| P0 | User | 카테고리와 맞지 않는 관심 키워드 거부 |
| P0 | Post | RssFeedItem에서 기술 게시글 생성 |
| P0 | Post | 요약, 짧은 요약, 게시글 키워드 갱신 |
| P0 | Post | 임베딩으로 PostDocument 생성 (PostEmbeddingProcessorTest) |
| P0 | Post | Elasticsearch 색인 + embeddedAt 갱신 (PostEmbeddingWriterTest) |
| P0 | Personalization Profile | 활동 데이터 기반 개인화 프로필 생성 |
| P0 | Recommendation | 프로필 벡터 없으면 추천 생성하지 않음 |
| P0 | Recommendation | 기존 추천 이력화 후 새 추천 저장 |
| P0 | Search | 개인화 프로필 없으면 일반 검색 fallback |
- 리팩터링할 영역부터 테스트를 작성한다.
- 모든 테스트를 먼저 다 작성하려고 하지 않는다.
- 외부 API, LLM, Elasticsearch는 mock/fake/adapter 테스트로 분리한다.
- 복잡한 통합 테스트보다 도메인 규칙을 보호하는 단위 테스트를 우선한다.
- 현재 동작이 이상하더라도, 의도된 변경이 아니라면 우선 현재 동작을 고정한다.
DDD 전환의 첫 코드 변경은 대규모 구조 변경보다 용어 정리가 적합하다.
현재 혼재:
ScrabPost // 엔티티
scrap_posts // DB 테이블
Bookmark // API/DTO/제품 용어
결정:
표준 용어 = 북마크 / Bookmark
권장 순서:
1. 현재 북마크 동작 테스트 작성
2. ScrabPostRepository 테스트 작성
3. 엔티티/리포지토리/서비스 코드 용어를 Bookmark로 변경
4. API 응답과 기존 동작 유지
5. DB 테이블 rename은 별도 마이그레이션으로 분리하거나 legacy table로 유지
운영 리스크를 줄이려면 1차로는 다음처럼 갈 수 있다.
@Entity
@Table(name = "scrap_posts") // legacy table name
public class Bookmark {
...
}즉:
- 코드 용어:
Bookmark - 문서 용어: 북마크
- API 용어: 북마크
- DB 테이블: 당장은
scrap_posts유지 가능
현재 혼재:
SearchHistory.searchWord
SearchService.searchGeneral(String query)
keyKeywords
PostKeyword
표준 구분:
| 표준 용어 | 의미 |
|---|---|
| 검색어 / SearchQuery | 사용자가 직접 입력한 검색 문자열 |
| 핵심 키워드 / KeyKeyword | 개인화 프로필에서 추출한 대표 관심 키워드 |
| 게시글 키워드 / PostKeyword | 기술 게시글 요약 과정에서 추출된 대표 키워드 |
권장 순서:
1. SearchHistory 동작 테스트 작성
2. DTO/API 파라미터 문서에서 검색어/SearchQuery로 표현 통일
3. 코드 내부 변수명 query/searchQuery 정리
4. 운영 중이면 DB 컬럼 rename은 후순위로 분리하고, 초기 정리 단계라면 함께 처리 가능
현재 “프로필”이라는 말이 두 의미로 쓰인다.
| 표준 용어 | 코드상 표현 | 의미 |
|---|---|---|
| 계정 프로필 | User.nickName, description, profileImage |
사용자에게 보이는 기본 프로필 |
| 개인화 프로필 | PersonalizationProfileDocument.profileText, profileVector |
검색/추천에 쓰이는 활동 기반 LLM/임베딩 프로필 |
권장 순서:
1. 문서/API 설명에서 `User Account`와 `Personalization Profile` 경계를 고정
2. PersonalizationProfileService 테스트 작성
3. PersonalizationProfileDocument의 역할을 Personalization Profile projection으로 명확히 함
4. 필요하면 패키지/이벤트/포트 분리를 후속 단계에서 진행
2026-04-28 기준 실제 사용처가 없음을 확인했고, enum 삭제를 완료했다.
처리 결과:
1. 실제 사용처 없음 확인
2. `src/main/java/com/techfork/domain/post/enums/EDifficultyLevel.java` 삭제 완료
3. 난이도 기능이 필요해질 때 정책과 함께 재도입
전체 프로젝트를 한 번에 바꾸지 않는다.
추천 순서:
1. Activity
2. Post / Content
3. User Account
4. Personalization Profile
5. Recommendation
6. Search
7. Source / Ingestion
- 크기가 상대적으로 작다.
- 용어 부채가 명확하다.
ScrabPost → Bookmark전환으로 즉시 효과가 있다.- 테스트 작성이 쉽다.
Activity
- ReadPost
- Bookmark
- SearchHistory
ActivityCommandServiceTest
- 사용자가 기술 게시글을 처음 읽으면 조회수가 증가한다.
- 이미 읽은 기술 게시글을 다시 읽으면 조회수는 증가하지 않는다.
- 북마크를 추가할 수 있다.
- 이미 북마크한 기술 게시글은 다시 북마크할 수 없다.
- 북마크를 삭제할 수 있다.
- 검색 기록을 저장할 수 있다.
BookmarkTest
- 같은 사용자와 기술 게시글 조합은 한 번만 북마크 가능하다.
ScrabPost→BookmarkScrabPostRepository→BookmarkRepositoryscrappedAt→bookmarkedAt- Activity 서비스 내부 용어 통일
- 핵심 콘텐츠 모델이다.
- Source, Search, Recommendation이 모두 의존한다.
- 여기 정리가 되어야 다른 컨텍스트도 안정된다.
Post = 기술 게시글 aggregate root
PostKeyword = Post 내부 엔티티
PostDocument = 검색/추천용 read model
ContentChunk = 검색/추천용 projection 내부 값
PostTest
- RssFeedItem으로 기술 게시글을 생성한다.
- 요약과 짧은 요약을 갱신한다.
- 게시글 키워드를 추가한다.
- 게시글 키워드를 초기화한다.
- 조회수를 증가시킨다.
SummaryExtractionServiceTest
- LLM 응답에서 summary, shortSummary, keywords를 파싱한다.
- 잘못된 LLM 응답을 처리한다.
PostEmbeddingProcessorTest
- 제목/요약/본문 청크 임베딩으로 PostDocument를 생성한다.
- 도메인 문서에서
Post를 기술 게시글로 설명 Post.company를TechBlog.companyName의 비정규화 스냅샷으로 명시PostKeyword를Post내부 컬렉션으로 더 명확히 다룸PostDocument가 RDB 애그리거트가 아니라 projection임을 분리
- Auth / Security, Activity, Notification이 기대는 사용자 정체성 경계를 제공한다.
- 관심사 불변식이 현재 서비스 레이어에 산재되어 있어
Useraggregate 정리가 먼저다. - User Account가 정리되어야 Personalization Profile 경계도 명확해진다.
User Account
- User aggregate
- UserInterestCategory
- UserInterestKeyword
- 계정 프로필
UserTest
- 소셜 사용자 생성 시 기본 상태는 PENDING이다.
- 온보딩 완료 시 ACTIVE가 된다.
- 계정 프로필을 수정할 수 있다.
- 탈퇴 시 개인정보가 null 처리되고 WITHDRAWN이 된다.
- 재활성화 시 PENDING 상태가 된다.
InterestCommandServiceTest
- 관심 카테고리와 키워드를 저장한다.
- 카테고리와 맞지 않는 키워드는 거부한다.
- 관심사 저장 후 개인화 프로필 생성을 요청한다.
replaceInterests(List<EInterestCategory>, List<EInterestKeyword>)도메인 메서드 추가- 관심사 불변식("키워드는 선택된 카테고리에 속해야 한다") 검증을
Useraggregate 내부로 이동 InterestCommandService가 리포지토리를 직접 조작하는 대신User.replaceInterests()를 호출하도록 변경
- Personalization Profile은 Recommendation, Search와 강하게 얽혀 있다.
- User Account(4.3)가 먼저 정리되어야
UserInterestsChanged이벤트 흐름이 자연스럽게 정착된다. domain/user안에 User Account 책임과 Personalization Profile 책임이 함께 있어, User Account 정리 직후 분리한다.
Personalization Profile
- PersonalizationProfileDocument (개인화 검색/추천용 read model projection)
- PersonalizationProfileService (Personalization Profile 생성 서비스로 위치 재정의)
PersonalizationProfileServiceTest
- 관심사, 읽은 게시글, 북마크, 검색 기록을 모아 활동 데이터를 구성한다.
- LLM 응답에서 프로필 텍스트와 핵심 키워드를 파싱한다.
- 파싱 실패 시 fallback 정책을 따른다.
- 프로필 텍스트를 임베딩하여 개인화 프로필을 저장한다.
- 개인화 프로필 생성 후 추천 생성을 호출한다.
- 추천 생성 실패가 개인화 프로필 저장을 깨뜨리지 않는다.
현재 Personalization Profile 생성 서비스 의존:
PersonalizationProfileService
- User 관심사
- ReadPost
- Bookmark
- SearchHistory
- PostKeyword
- LLM
- Embedding
- Recommendation
정리 방향:
PersonalizationProfileDocument를 Personalization Profile projection으로 명확히 한다.PersonalizationProfileService를 User Account 서비스가 아닌 Personalization Profile 생성 서비스로 위치를 재정의한다.- 관심사 변경/온보딩 완료는 장기적으로
UserInterestsChanged,OnboardingCompleted이벤트로 분리한다.
분리 후보 (점진적으로 적용):
PersonalizedProfileGenerator
UserActivityReader
LlmProfileAnalyzer
PersonalizedProfileRepository
단, 바로 쪼개기보다 테스트를 먼저 작성하고 점진적으로 분리한다.
- 복잡도가 높다.
- Elasticsearch, Personalization Profile(
PersonalizationProfileDocument), Activity, Post에 모두 의존한다. - 테스트 없이 건드리면 위험하다.
현재 구현은 RecommendedPost 단건 중심이다.
DDD 관점에서는 다음 모델이 더 자연스럽다.
RecommendationSet 또는 UserRecommendations
- userId
- recommendedPosts
- generatedAt
현재 당장 엔티티를 바꾸지 않더라도, 도메인 개념은 “사용자별 추천 목록”으로 잡는다.
MmrServiceTest
- 후보가 비어 있으면 빈 결과를 반환한다.
- finalSize만큼 추천 결과를 반환한다.
- similarity와 diversity를 반영해 순위를 만든다.
LlmRecommendationServiceTest
- 프로필 벡터가 없으면 추천을 생성하지 않는다.
- 읽은 게시글은 추천 후보에서 제외한다.
- RRF 결과를 MMR 후보로 변환한다.
- 기존 추천은 이력화한다.
- 새 추천을 저장한다.
RecommendedPost단건 중심에서RecommendationSet개념 도입 검토- 읽은 게시글 제외 정책을 Activity repository 직접 호출이 아닌 정책 포트로 분리
PersonalizedProfileGenerated이벤트를 구독해 추천 생성- 추천 이력과 현재 추천 목록의 책임 분리
- 대부분 query/read model 중심이다.
- 애그리거트보다는 검색 orchestration에 가깝다.
- Elasticsearch 의존이 강해서 테스트 구성 비용이 있다.
Search는 aggregate 중심이 아니라 Query Service / Read Model 컨텍스트로 본다.
SearchServiceImplTest
- 일반 검색은 BM25 + Semantic 결과를 RRF로 결합한다.
- 개인화 프로필이 없으면 일반 검색 결과로 fallback한다.
- 개인화 프로필이 있으면 personalScore를 반영해 rerank한다.
- 검색 결과에 조회수와 북마크 여부를 붙인다.
- Elasticsearch 호출을 adapter로 감싸기
PostDocument를 검색 read model로 명시PersonalizationProfileDocument를 Personalization Profile read model로 명시- Activity의 북마크 여부 조회를 query composition으로 유지하되 포트 도입 검토
- 배치, 외부 RSS, 동시성, 실패 처리 영향이 있다.
- DDD보다 파이프라인 안정성이 더 중요하다.
- 테스트 작성 난이도가 있다.
TechBlog = Source aggregate root
RssFeedItem = 외부 RSS를 내부 언어로 변환한 ACL DTO
Post 생성 = Source와 Post 사이의 Published Language 또는 이벤트 분리 후보
RssFeedReaderTest
- RSS 엔트리를 RssFeedItem으로 변환한다.
- 기존 URL은 제외한다.
- 같은 실행 내 중복 URL은 제거한다.
- RSS 실패 시 전체 배치가 죽지 않고 빈 리스트로 처리한다.
RssToPostProcessorTest
- RssFeedItem을 기술 게시글로 변환한다.
RssFeedItem을 Source와 Post 사이 Published Language로 명확히 정의- RSS parsing adapter와 batch orchestration 분리
TechnicalPostDiscovered또는TechnicalPostSaved이벤트 도입 검토
초기 테스트는 서비스 메서드 중심이어도 된다.
하지만 DDD 전환이 진행되면 테스트 구조도 다음처럼 바꾸는 것이 좋다.
src/test/java/com/techfork/domain
activity
BookmarkTest
ReadPostTest
ActivityCommandServiceTest
post
PostTest
PostSummaryProcessorTest
PostEmbeddingProcessorTest
user
UserTest
InterestCommandServiceTest
PersonalizationProfileServiceTest
recommendation
MmrServiceTest
LlmRecommendationServiceTest
search
SearchServiceImplTest
source
RssFeedReaderTest
RssToPostProcessorTest
테스트 종류별 역할:
| 테스트 종류 | 목적 |
|---|---|
| Domain Unit Test | 애그리거트 불변식 검증 |
| Application Service Test | 유스케이스 흐름 검증 |
| Repository Test | 쿼리/영속성 검증 |
| Adapter Test | 외부 API, RSS, Elasticsearch, LLM 경계 검증 |
| Controller Test | API 계약 검증 |
| Integration Test | 주요 시나리오 end-to-end 검증 |
다음 조건이 모두 충족되면 이벤트 도입을 시작한다.
[ ] P0 테스트가 모두 존재하고 ./gradlew test -PexcludeIntegration 통과
[ ] PersonalizationProfileServiceTest로 Personalization Profile 생성 흐름 보호
[ ] MmrServiceTest + LlmRecommendationServiceTest로 추천 생성 핵심 흐름 보호
[ ] SearchServiceImplTest로 일반/개인화 검색 회귀 보호
[ ] User Account aggregate 책임과 Personalization Profile 생성 책임이
서비스 수준에서 구분되어 있음 (패키지 분리는 불필요)
조건 미충족 상태에서 이벤트를 먼저 도입하면, 이벤트 발행/구독 경로가 테스트 안전망 없이 추가되어 리팩터링 중 회귀를 감지하기 어려워진다.
처음부터 이벤트 기반으로 바꾸지 않는다.
권장 순서:
1. 테스트로 현재 동작 보호
2. 용어와 애그리거트 정리
3. 컨텍스트별 책임 분리
4. 이벤트/ACL/포트 도입
1차 이벤트 후보:
UserInterestsChanged
PersonalizedProfileGenerated
TechnicalPostIndexed
| 이벤트 | 기대 효과 |
|---|---|
UserInterestsChanged |
관심사 변경과 개인화 프로필 재생성을 분리 |
PersonalizedProfileGenerated |
Personalization Profile 생성과 추천 생성을 분리 |
TechnicalPostIndexed |
검색/추천 가능한 콘텐츠 상태를 명시 |
지금 당장 시작한다면 다음 순서를 추천한다.
1. 테스트 갭 분석 문서 작성
2. Activity 테스트 작성
3. ScrabPost → Bookmark 리팩터링
4. Post 도메인 테스트 작성
5. Post를 “기술 게시글” 기준으로 정리
6. User 관심사/온보딩 테스트 작성
7. PersonalizationProfileService 테스트 작성
8. Personalization Profile 책임 분리
9. Recommendation 테스트 작성
10. PersonalizedProfileGenerated 이벤트 도입
11. Search 테스트 작성
12. Source/Ingestion 테스트 작성
13. TechnicalPostIndexed 이벤트 도입
목표:
- ScrabPost 계열을 Bookmark로 통일한다.
선행 테스트:
- 북마크 추가
- 중복 북마크 방지
- 북마크 삭제
- 북마크 목록 조회
리팩터링:
- ScrabPost → Bookmark
- ScrabPostRepository → BookmarkRepository
- scrappedAt → bookmarkedAt
주의:
- DB 테이블 rename은 별도 결정
목표:
- Post를 기술 게시글 애그리거트로 명확히 한다.
선행 테스트:
- RssFeedItem에서 Post 생성
- 요약 갱신
- 키워드 갱신
- 조회수 증가
리팩터링:
- 문서/주석에서 기술 게시글 용어 사용
- PostKeyword를 Post 내부 엔티티로 명확히 관리
- EDifficultyLevel 제거
목표:
- User Account와 Personalization Profile 경계를 문서/서비스 책임 기준으로 분리한다.
선행 테스트:
- 온보딩 완료
- 관심사 저장
- 개인화 프로필 생성
- 핵심 키워드 파싱
리팩터링:
- PersonalizationProfileDocument를 Personalization Profile projection으로 명확히 함
- PersonalizationProfileService 책임 분리
- PersonalizedProfileGenerated 이벤트 도입 준비
목표:
- 현재 추천 목록과 추천 이력을 구분한다.
선행 테스트:
- 추천 후보 생성
- 읽은 게시글 제외
- MMR 결과 생성
- 기존 추천 이력화
- 새 추천 저장
리팩터링:
- RecommendationSet 또는 UserRecommendations 개념 도입 검토
- 추천 생성 트리거를 PersonalizedProfileGenerated 이벤트로 분리
목표:
- 임베딩 파이프라인을 테스트로 보호하고 PostDocument 생성 흐름을 명확히 한다.
선행 테스트:
- 제목/요약/본문 청크를 각각 임베딩한다.
- PostDocument에 titleEmbedding, summaryEmbedding, contentChunks를 채운다.
- 임베딩 실패 시 예외를 전파한다.
- Elasticsearch bulk index + embeddedAt 갱신이 원자적으로 처리된다.
리팩터링:
- PostEmbeddingWriter의 embeddedAt 갱신을 도메인 메서드 markAsEmbedded()로 위임
- TechnicalPostIndexed 이벤트 도입 준비 (Phase 6)
목표:
- Search 컨텍스트의 핵심 흐름을 일반 단위 테스트로 보호한다.
- evaluation suite와 별개로 빠른 회귀 감지 루프를 확보한다.
선행 테스트:
- 일반 검색은 BM25 + semantic 결과를 RRF로 결합한다.
- 개인화 프로필이 없으면 일반 검색 결과로 fallback한다.
- 개인화 프로필이 있으면 personalScore로 rerank한다.
- 검색 결과에 viewCount와 isBookmarked를 붙인다.
리팩터링:
- Elasticsearch 호출을 adapter 인터페이스로 감싸기
- PostDocument를 검색 read model로 명시
주의:
- evaluation suite(evaluation 태그)는 이 테스트와 분리 유지
목표:
- PersonalizationProfileService를 Personalization Profile 생성 서비스로 테스트로 고정한다.
선행 테스트:
- 관심사, 읽은 게시글, 북마크, 검색 기록을 활동 데이터로 수집한다.
- LLM 응답에서 profileText와 keyKeywords를 파싱한다.
- 파싱 실패 시 fallback 정책을 따른다.
- profileText를 임베딩하여 PersonalizationProfileDocument를 저장한다.
- 개인화 프로필 생성 후 추천 생성을 호출한다.
- 추천 생성 실패가 개인화 프로필 저장을 깨뜨리지 않는다.
리팩터링:
- PersonalizationProfileDocument를 Personalization Profile projection으로 명확히 함
- PersonalizationProfileService 책임 분리 (PersonalizedProfileGenerated 이벤트 도입 준비)
전제 조건:
- 5.1~5.7 작업 단위의 선행 테스트가 모두 존재하고 통과
- Phase 6 진입 조건 체크리스트 완료
목표:
- 세 이벤트를 순서대로 도입해 컨텍스트 결합도를 낮춘다.
도입 순서:
1. UserInterestsChanged
- InterestCommandService가 PersonalizationProfileService를 직접 호출하는 대신 이벤트 발행
- 리스너: @TransactionalEventListener(AFTER_COMMIT) + @Async
2. PersonalizedProfileGenerated
- PersonalizationProfileService가 LlmRecommendationService를 직접 호출하는 대신 이벤트 발행
- 리스너: 추천 생성 트리거
3. TechnicalPostIndexed
- PostEmbeddingWriter Step 완료 후 이벤트 발행 (StepExecutionListener.afterStep)
- 리스너: 검색/추천 콘텐츠 준비 완료 알림
검증:
- 이벤트 도입 전후 기존 테스트 전체 통과
- 각 이벤트에 대한 publisher/listener contract test 추가
가장 중요한 원칙은 다음이다.
테스트를 DDD 전환의 선행조건으로 전부 완성하려고 하지 말고, 리팩터링할 슬라이스마다 필요한 테스트를 먼저 깔고 바로 전환한다.
추천 순서:
DDD 기준선 확정
→ 테스트 갭 분석
→ Activity 보호 테스트
→ Bookmark 용어 리팩터링
→ Post 보호 테스트
→ User/Profile 보호 테스트
→ Recommendation 보호 테스트
→ 컨텍스트별 DDD 리팩터링
→ 이벤트/ACL/포트 기반 분리
이 방식이 현재 TechFork처럼 테스트가 아직 부족하고, 동시에 DDD 방향으로 구조를 개선해야 하는 프로젝트에서 가장 안전하다.