You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/tactical-design.md
+26-1Lines changed: 26 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,7 +19,7 @@
19
19
| 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` 같은 경계 재설계는 후속 단계로 미룬다. |
20
20
| Search | 명시적 쓰기 애그리거트 없음 |`SearchResult` DTO, `PostDocument` read model | 검색어를 기반으로 검색 결과를 계산한다. 검색 결과는 저장되는 도메인 상태가 아니라 조회 결과다. | Search는 애그리거트보다 query service/read model 중심 컨텍스트다. |
21
21
| Recommendation |**표준: `RecommendationSet`** (현재 코드: `RecommendedPost` 단건) |`RecommendedPost`, `RecommendationHistory`| 같은 `userId + rankOrder` 조합은 유일해야 한다. 새 추천 저장 전 기존 추천은 모두 `RecommendationHistory`로 이동해야 한다. `rankOrder`는 1..N 연속이어야 한다. | 현재 `RecommendedPost` 단건이 루트 역할을 하지만 `RecommendationSet` 개념으로 리팩터링 대상이다 (코드 미반영, 유비쿼터스 언어 README의 문서-코드 동기화 상태 참조). |
22
-
| Auth / Security | 독립 애그리거트 없음 |Refresh Token 저장소, `UserPrincipal`| 토큰 발급/검증/갱신을 수행한다. 사용자 자체는 User Account 컨텍스트에 속한다. | Auth / Security는 도메인 애그리거트보다 보안 애플리케이션/인프라 컨텍스트다. |
22
+
| Auth / Security | 독립 애그리거트 없음 |`RefreshTokenStore`, `RefreshTokenCookieWriter`, `UserAuthCacheStore`, `UserPrincipal`, OAuth login handler | refresh token은 Redis 저장소와 cookie wire contract가 함께 맞아야 한다. OAuth 성공 리다이렉트는 access token을 URL에 포함하지 않고 refresh token cookie만 내려준다. access token은 `POST /api/v1/auth/refresh`에서만 재발급한다. 사용자 자체는 User Account 컨텍스트에 속한다. | Auth / Security는 도메인 애그리거트보다 보안 애플리케이션/인프라 컨텍스트다. OAuth success redirect, refresh/logout, 인증 캐시 무효화는 전술 seam으로 문서와 테스트를 고정한다. |
23
23
| Notification |`NotificationToken`| 없음 | 사용자별 알림 토큰과 활성 여부를 관리한다. 같은 사용자의 토큰은 하나만 활성 상태여야 한다. | 현재 행동은 약하지만 독립 루트 후보로 볼 수 있다. |
24
24
| Admin / Ops | 독립 애그리거트 없음 | Batch Job Execution, Webhook payload | 운영자가 배치를 수동 실행하거나 실패 알림을 보낸다. | 운영 유스케이스 컨텍스트이며 핵심 도메인 애그리거트는 없다. |
25
25
@@ -123,6 +123,30 @@ createSocialUser() → PENDING
123
123
124
124
현재 코드명 `markAsisClicked` → `markAsClicked`로 변경 필요. (`asis`는 오타)
125
125
126
+
#### Auth / Security
127
+
128
+
Auth / Security는 `User` 같은 도메인 애그리거트를 직접 소유하지 않는다. 전술 설계상 핵심은 토큰/쿠키/리다이렉트/인증 캐시를 다루는 보안 application/infrastructure seam을 명시적으로 고정하는 것이다.
129
+
130
+
**현재 canonical seams**
131
+
132
+
-`AuthCommandService.refreshToken()` / `logout()`은 refresh token cookie 값을 입력으로 받아 Redis의 `RefreshTokenStore`와 JWT type/유효성을 함께 검증한다.
133
+
-`RefreshTokenCookieWriter`는 refresh token의 `Set-Cookie` 작성/삭제 정책(`HttpOnly`, `Secure`, `SameSite=None`, domain, path, max-age)을 한 곳에 모은다.
134
+
-`OAuth2AuthenticationSuccessHandler`는 OAuth/OIDC 성공 후 `OAuth2LoginRefreshTokenIssuer`와 `OAuth2LoginRefreshTokenWriter`를 통해 refresh token만 발급·저장·쿠키화한다.
135
+
-`OAuth2LoginRedirectUrlFactory`는 `jwt.login-success-redirect-uri`를 query 없는 callback base URI로 사용하고 `registered`, `email` query만 조립·인코딩한다. `token` query나 `%s` access token template은 성공 리다이렉트 계약이 아니다.
136
+
-`UserAuthCacheInvalidationListener`는 User Account 이벤트를 받아 인증 캐시를 무효화한다. 온보딩 완료/재활성화는 `AFTER_COMMIT`, 회원 탈퇴는 보안 민감 seam이므로 `BEFORE_COMMIT + AFTER_COMMIT`으로 처리한다.
137
+
138
+
**브라우저 OAuth 성공 흐름**
139
+
140
+
1. Spring Security OAuth/OIDC 성공 후 `UserPrincipal`이 인증 주체가 된다.
3. 서버는 프론트 callback으로 redirect하되 URL에는 `registered`, `email`만 포함한다.
143
+
4. 프론트 callback은 cookie를 포함해 `POST /api/v1/auth/refresh`를 호출하고 응답 body의 access token을 사용한다.
144
+
145
+
**후속 분리 후보**
146
+
147
+
-`JwtProperties`는 현재 JWT secret/expiration과 OAuth 성공·실패 redirect URI를 함께 담는다. 브라우저 OAuth flow 설정이 더 늘어나면 `OAuthRedirectProperties` 같은 별도 configuration object로 분리할 수 있다.
148
+
- User Account 조회 seam은 현재 `UserPrincipal`/인증 캐시 중심으로 충분하지만, 물리 분리 시에는 최소 인증 프로필 published query 또는 ACL로 분리한다.
149
+
126
150
---
127
151
128
152
### 1.3 값 객체(Value Object) 후보
@@ -225,6 +249,7 @@ User Account 관련 이벤트는 Personalization/Auth 후처리와 분리되었
225
249
| Search → Post | 유지 | Search 는 `PostDocument` 를 검색 후보 projection 으로 소비한다. `PostRepository` 사용은 후보 탐색이 아니라 `viewCount` 같은 metadata 조합으로 한정한다. | metadata 용 별도 read model / query port |
226
250
| Recommendation → Post | 유지 | Recommendation 은 후보 탐색에는 `PostDocument` projection 을 사용하고, 저장 시점에만 `Post` reference 를 확보한다. 후보 탐색 seam 과 저장 seam 을 분리해서 해석한다. | 추천 저장용 최소 식별 공유, 이벤트 기반 추천 재생성 |
227
251
| Source → Post 생성 | 유지 |`RssFeedItem` 은 Source 가 외부 RSS 를 정제해 만든 **현재 monolith 내부 handoff DTO** 로 보고, `Post.create(RssFeedItem, TechBlog)` 경계를 유지한다. 아직 published language 로 분리된 상태는 아니다. | Post 소유 command/published language, `TechnicalPostDiscovered` 이벤트 handoff |
252
+
| Auth / Security → User Account 인증 프로필 | 유지 | Auth / Security 는 `UserPrincipal`과 인증 캐시를 위해 User Account 의 최소 인증 프로필(`id`, `role`, `status`, `email`)만 사용한다. User Account 상태 전이는 User Account 가 소유하고, Auth / Security 는 이벤트 기반 캐시 무효화로 후처리한다. | 최소 인증 프로필 published query, ACL, 별도 auth-read model |
0 commit comments