From 2fe3b7c42c5a75f889b59120e7e2bc5799551959 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Sun, 21 Jun 2026 07:01:57 +0900 Subject: [PATCH 1/4] =?UTF-8?q?docs:=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ddd-test-refactoring-roadmap.md | 46 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/ddd-test-refactoring-roadmap.md b/docs/ddd-test-refactoring-roadmap.md index 812c8800..fb32933f 100644 --- a/docs/ddd-test-refactoring-roadmap.md +++ b/docs/ddd-test-refactoring-roadmap.md @@ -2,7 +2,7 @@ > 목적: TechFork 서버를 DDD 관점으로 점진적으로 개선하면서, 아직 부족한 테스트 코드를 어떤 순서로 작성·개선할지 정리한다. > 관련 문서: [`docs/ubiquitous-language/README.md`](./ubiquitous-language/README.md) -> 기준 시점: **2026-06-03, #411 병합 후 `docs/#412` 시작 시점** +> 기준 시점: **2026-06-21, #432 Auth / Security 문서 동기화 시점** ## 1. 결론 @@ -35,7 +35,7 @@ DDD 목표 지도 작성 → 다음 영역으로 이동 ``` -2026-06-03 현재 상태를 요약하면 다음과 같다. +2026-06-21 현재 상태를 요약하면 다음과 같다. ```text [완료] Phase 0: DDD 기준선 문서화 @@ -45,7 +45,11 @@ DDD 목표 지도 작성 [완료] Phase 3: Bookmark / SearchQuery 용어 정리, EDifficultyLevel 제거 반영 [완료] Activity 4.1, User Account, Personalization Profile 1차 DDD 경계 정리 [완료] PersonalizedProfileGeneratedEvent로 Personalization → Recommendation 후처리 분리 -[다음] 문서/유비쿼터스 언어 최신화 후 RecommendationSet, MMR, Search/Recommendation 후속 모델 정리 +[완료] Auth / Security 1차 DDD 경계 정리 + - `auth` 최상위 컨텍스트와 `auth/security` shared kernel 정리 + - User Account 조회 seam, auth cache 이벤트 seam, OAuth/OIDC 책임, DTO 경계 정리 + - OAuth 성공 redirect access token 제거와 refresh token cookie 기반 access token 재발급 계약 반영 +[다음] RecommendationSet, MMR, Search/Recommendation 후속 모델 정리 ``` --- @@ -855,24 +859,24 @@ global/util/ContentCleaner -> Source / Post shared content support 후 ##### 권장 회수 순서 -현재 시점 기준 추천 순서는 다음과 같다. +현재 시점 기준 권장 회수 상태와 후속 순서는 다음과 같다. ```text -1. User Account 4.3 - - 필요 시 Auth / Security shared seam(UserAuthCacheStore, UserPrincipal)만 함께 정리 - - Auth / Security 전체 경계 이동은 별도 이슈에서 진행한다 +1. User Account 4.3 — 완료 + - Auth / Security shared seam(UserAuthCacheStore, UserPrincipal)과의 이벤트/조회 경계를 정리했다 -2. Personalization Profile 4.4 - - User Account와의 직접 호출 책임을 먼저 정리 +2. Personalization Profile 4.4 — 완료 + - User Account와의 직접 호출 책임을 이벤트 기반으로 분리했다 -3. Auth / Security +3. Auth / Security — 1차 완료 - `auth` 최상위 컨텍스트와 `auth/security` shared kernel로 정리 - - 이후 문서/테스트 경로가 현재 구조를 따르도록 유지 + - User Account aggregate 소유권은 유지하고 인증 최소 정보, principal/cache/token 정책만 소유 + - #427, #428, #429, #430, #431, #433, #436, #440, #442 기준 완료 상태를 문서/테스트 경로에 반영 -4. Recommendation / Search +4. Recommendation / Search — 다음 - VectorQueryBuilder, LinearTimeDecayStrategy, RRF/검색 정책 support를 owning context로 회수 -5. Source / Post shared content support +5. Source / Post shared content support — 후속 - InitialDataConfig, ContentCleaner, 일부 converter/util을 owning context나 shared support로 재배치 ``` @@ -880,7 +884,7 @@ global/util/ContentCleaner -> Source / Post shared content support 후 - Activity와 Post는 1차 정리가 끝났더라도, 지금 남아 있는 `global` 의존 중 상당수는 진짜 shared support이거나 다른 컨텍스트 소유다. - 따라서 **Activity/Post 관련 `global` 정리를 먼저 별도 작업으로 시작하지 않는다.** -- 대신 이후 `Auth / Security`, `Recommendation / Search`, `Source`를 정리할 때 각각의 소유 코드를 회수한다. +- Auth / Security 1차 정리는 완료되었으므로, 이후 `Recommendation / Search`, `Source`를 정리할 때 각각의 소유 코드를 회수한다. --- @@ -1098,12 +1102,22 @@ src/test/java/com/techfork [완료] 8-2. Personalization → Recommendation 이벤트 경계 분리 - `PersonalizedProfileGeneratedEvent` 도입 - Recommendation 리스너가 `AFTER_COMMIT`에서 이벤트 스냅샷 기반 추천 생성 +[완료] 8-3. Auth / Security 1차 경계 정리 + - #427: Auth / Security DDD 리팩터링 전 회귀 테스트 보강 + - #428: `auth` 최상위 컨텍스트와 `auth/security` shared kernel 소유 표면 정리 + - #429: Auth 서비스의 UserRepository 직접 의존 경계 정리 + - #430: User Account 이벤트 기반 auth cache 무효화 seam 안정화 + - #431: OAuth/OIDC 보안 인프라 책임 분리 + - #433: OAuth2/OIDC redirect/cookie 브라우저 보안 정책 정리 + - #436: Auth DTO를 Command/Response 경계로 분리 + - #440: OAuth 성공 redirect access token 제거 및 refresh 연동 계약 정리 + - #442: auth security 내부 store/cache/cookie 책임 명명 정리 [부분 진행] 9. Recommendation/Search 회귀 테스트 보강 - LlmRecommendationServiceTest, SearchServiceImplTest 반영 - MmrServiceTest 추가 필요 -[후속] 10. global 소유권 회수 +[후속] 10. Recommendation/Search 및 Source/Post shared support 소유권 회수 - global-first가 아니라 owner-context-first 방식으로 진행 - - Auth / Security -> Recommendation/Search -> Source/Post shared support 순으로 회수 + - Auth / Security 1차 회수 이후 Recommendation/Search -> Source/Post shared support 순으로 회수 [다음] 11. 컨텍스트 1차 정리 후 전술 모델 2차 정리 - aggregate / value object / ID reference / 엔티티 경계 정교화 [다음] 12. Phase 6 진입 조건 충족 후 이벤트/포트 분리 시작 From 10c17f35063a13a48c439daaa6dff1f67372e0cd Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Sun, 21 Jun 2026 07:03:27 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/test-gap-analysis.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/test-gap-analysis.md b/docs/test-gap-analysis.md index 11a43c4b..a76fa8c2 100644 --- a/docs/test-gap-analysis.md +++ b/docs/test-gap-analysis.md @@ -445,29 +445,44 @@ SearchControllerIntegrationTest | `AuthCommandServiceTest` | unit/mock | refresh, logout, developer token command use cases | | `KakaoLoginCommandServiceTest` | unit/mock | iOS 직접 Kakao login 신규/기존 사용자 | | `KakaoOAuthServiceTest` | unit/mock | Kakao user info response mapping success/failure | +| `KakaoSocialIdTest` | unit | Kakao REST/OIDC social id 정규화 | | `AuthControllerIntegrationTest` | integration | refresh/logout/kakao login API | | `DeveloperTokenControllerIntegrationTest` | integration | developer token 생성, 권한/인증 실패 | | `SecurityIntegrationTest` | integration | 인증/인가, 토큰 오류, 권한, 탈퇴 사용자 | +| `JwtUtilTest` | unit | refresh token 단독 발급과 token type | | `JwtAuthenticationFilterTest` | unit/mock | access token filter, cache hit/miss, invalid token | -| `OAuth2AuthenticationSuccessHandlerTest` | unit/mock | OIDC 로그인 성공 redirect/cookie | +| `RefreshTokenCookieWriterTest` | unit | refresh token cookie 작성/삭제 wire contract | +| `OAuth2AuthenticationSuccessHandlerTest` | unit/mock | OAuth2 로그인 성공 refresh token 발급/저장과 redirect | | `OAuth2AuthenticationFailureHandlerTest` | unit/mock | OIDC 로그인 실패 redirect | +| `OAuth2LoginRedirectUrlFactoryTest` | unit | success/failure redirect URL 조립, token query 제거, email encoding | +| `OAuth2LoginRefreshTokenIssuerTest` | unit/mock | OAuth2 로그인 성공 경로의 refresh token 단독 발급 | +| `OAuth2LoginRefreshTokenWriterTest` | unit/mock | OAuth2 로그인 refresh token 저장과 cookie writer 위임 | | `CustomOidcUserServiceTest` | unit/mock | Kakao/Apple OIDC 사용자 생성/재사용/재활성화 | +| `OidcSocialIdentityExtractorTest` | unit | Kakao/Apple OIDC claim에서 소셜 식별자 추출 | | `HttpCookieOAuth2AuthorizationRequestRepositoryTest` | unit/mock | OAuth authorization request cookie 저장/로드/삭제 | | `UserAuthCacheStoreTest` | unit/mock | auth cache serialization/deserialization | | `UserAuthCacheInvalidationListenerTest` | unit/mock | User Account 이벤트 기반 auth cache eviction | #### 평가 -Auth/Security는 비교적 안정적이다. -DDD 전환의 핵심 경로는 아니지만, User 컨텍스트 리팩터링 시 인증 모델이 깨지지 않도록 유지해야 한다. +Auth / Security는 1차 DDD 경계 정리 이후 비교적 안정적이다. +`auth` 최상위 컨텍스트와 `auth/security` shared kernel은 테스트로 대부분 보호되어 있고, +OAuth/OIDC handler, redirect URL factory, refresh token issuer/writer, cookie writer가 단위 테스트 가능한 seam으로 분리되었다. + +OAuth 성공 redirect는 access token을 URL에 싣지 않고 refresh token cookie를 발급한 뒤, +callback 이후 `/api/v1/auth/refresh`로 access token을 재발급받는 계약으로 정리되었다. +Cookie 속성은 `RefreshTokenCookieWriterTest`로 단위 보호가 생겼지만, +실제 브라우저 cross-origin 환경에서의 통합 검증은 별도 경량 경로가 필요하다. #### 남은 갭 | 우선순위 | 갭 | 이유 | |---|---|---| -| P1 | Auth/Security 통합 테스트의 경량 MySQL+Redis 기반 검증 경로 | 현재 `IntegrationTestBase`는 Elasticsearch까지 기동하므로 auth-only 검증 비용이 크다 | +| P1 | Auth / Security 통합 테스트의 경량 MySQL+Redis 기반 검증 경로 | 현재 auth controller/security integration도 Elasticsearch Testcontainers까지 기동할 수 있어 auth-only 회귀 검증 비용과 실패면이 크다 | | P1 | withdraw/reactivate 이후 refresh token/auth cache 통합 정책 테스트 | User 상태 전환과 보안 정책 연결 | -| P2 | Cookie 속성(SameSite/Secure/Domain/Max-Age) 통합 검증 | 프론트 연동에 직접 영향이 있는 보안 쿠키 계약 보호 | +| P2 | OAuth success redirect + refresh 연동 통합 계약 | success redirect에는 `registered`, `email`만 남고 access token은 refresh API로만 받는 end-to-end 계약 보호 | +| P2 | 운영 env/secret 변경 계약 문서화 | `JWT_LOGIN_SUCCESS_REDIRECT_URI(_DEV)` 누락 시 OAuth callback 장애가 배포 후 발견될 수 있다 | +| P3 | Cookie 속성(SameSite/Secure/Domain/Max-Age) 통합 검증 | 단위 테스트는 있으나 실제 브라우저/cross-origin 쿠키 전달 계약은 e2e 또는 경량 통합 경로가 필요 | --- From f49b6cdf31caa4fb378e7a9188efc84e2377a313 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Sun, 21 Jun 2026 07:05:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=EC=9C=A0=EB=B9=84=EC=BF=BC?= =?UTF-8?q?=ED=84=B0=EC=8A=A4=20=EC=96=B8=EC=96=B4=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ubiquitous-language/auth-security.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/ubiquitous-language/auth-security.md b/docs/ubiquitous-language/auth-security.md index 48cd3d58..4e0f9f9e 100644 --- a/docs/ubiquitous-language/auth-security.md +++ b/docs/ubiquitous-language/auth-security.md @@ -22,6 +22,9 @@ | 액세스 토큰 | `accessToken` | API 인증용 JWT | | 리프레시 토큰 | `refreshToken` | 액세스 토큰 재발급용 토큰. Cookie와 Redis 저장소를 사용한다. | | 토큰 갱신 | `refreshToken` API | 리프레시 토큰을 검증하고 새 액세스/리프레시 토큰을 발급하는 행위 | +| OAuth 성공 리다이렉트 | `OAuth2AuthenticationSuccessHandler` | OAuth/OIDC 인증 성공 후 refresh token cookie를 내려주고 프론트 callback으로 이동시키는 브라우저 흐름 | +| 로그인 성공 리다이렉트 base URI | `jwt.login-success-redirect-uri` / `JWT_LOGIN_SUCCESS_REDIRECT_URI` | query 없는 프론트 callback base URI. 서버가 `registered`, `email` query를 조립하고 인코딩한다. | +| 리프레시 토큰 쿠키 기반 재발급 | `POST /api/v1/auth/refresh` | HttpOnly refresh token cookie로 access token을 재발급받는 계약. cross-origin 프론트는 cookie 전송을 위해 `credentials: include`가 필요하다. | | 로그아웃 | `logout` | 리프레시 토큰을 삭제하고 쿠키를 제거하는 행위 | | 개발자 토큰 | `DeveloperTokenResult` / `DeveloperTokenResponse` | 관리자 API에서 발급하는 장기 액세스 토큰 성격의 토큰 | | 사용자 주체 | `UserPrincipal` | Spring Security 인증 컨텍스트에서 사용자를 나타내는 객체 | @@ -31,6 +34,8 @@ - Auth / Security의 현재 물리 경계는 최상위 `auth` 패키지다. - `auth/security`는 Auth / Security 내부에 있지만, `UserPrincipal`, JWT 필터, OAuth 핸들러처럼 여러 컨텍스트가 기대는 앱 전역 인증/인가 shared kernel 역할을 한다. - `User` aggregate 자체는 User Account 컨텍스트 소속이며, Auth / Security는 필요한 최소 사용자 식별/권한 정보만 공유한다. +- OAuth 성공 리다이렉트는 access token을 URL에 포함하지 않는다. 성공 직후 내려간 refresh token cookie를 사용해 `POST /api/v1/auth/refresh`에서 access token을 발급받는다. +- `JwtProperties`는 현재 JWT 만료/secret뿐 아니라 OAuth 성공/실패 리다이렉트 URI까지 포함하는 Auth / Security 런타임 설정이다. 브라우저 흐름 설정이 더 늘어나면 별도 properties 분리가 후보가 된다. ## 내부 glossary @@ -40,6 +45,9 @@ | 리프레시 토큰 저장소 | `RefreshTokenStore` | Redis 기반 리프레시 토큰 저장/검증/삭제 store | | 인증 필터 | `JwtAuthenticationFilter` | 요청에서 JWT를 읽어 인증 컨텍스트를 채우는 필터 | | OAuth 요청 저장소 | `HttpCookieOAuth2AuthorizationRequestRepository` | OAuth 인증 요청 상태를 쿠키로 보관하는 구성요소 | +| OAuth 성공 리다이렉트 URL factory | `OAuth2LoginRedirectUrlFactory` | 성공 callback URL에 `registered`, `email` query만 붙이고 `UriComponentsBuilder.encode()`로 인코딩하는 factory | +| OAuth 로그인 refresh token issuer | `OAuth2LoginRefreshTokenIssuer` | OAuth 성공 경로에서 access token 없이 refresh token만 발급하는 구성요소 | +| OAuth 로그인 refresh token writer | `OAuth2LoginRefreshTokenWriter` | OAuth 성공 경로에서 refresh token을 Redis에 저장하고 `Set-Cookie` 작성을 위임하는 구성요소 | | 리프레시 토큰 쿠키 writer | `RefreshTokenCookieWriter` | refresh token `Set-Cookie` 작성/삭제 정책을 담당하는 응답 writer | | 사용자 인증 캐시 저장소 | `UserAuthCacheStore` | 로그인/토큰 갱신 이후 사용자 인증 조회를 보조하는 Redis cache store | | 인증 캐시 무효화 리스너 | `UserAuthCacheInvalidationListener` | User Account 이벤트를 받아 사용자 인증 캐시를 무효화하는 transactional event adapter | @@ -50,6 +58,8 @@ - Auth / Security는 사용자 계정 소유 컨텍스트가 아니다. 사용자 생성/상태 전이는 User Account 컨텍스트가 소유하고, 개인화 프로필 생성은 Personalization Profile 컨텍스트가 맡는다. - `UserPrincipal`은 인증 컨텍스트 표현이지 `User` aggregate 그 자체가 아니다. - 온보딩 완료 캐시 무효화는 `AFTER_COMMIT` 후처리로 충분하지만, 회원 탈퇴 캐시 무효화는 보안 민감 seam이므로 `BEFORE_COMMIT`에서 실패 시 탈퇴 트랜잭션을 롤백하고 `AFTER_COMMIT`에서 한 번 더 무효화한다. +- 로그인 성공 리다이렉트 URI는 query template이 아니라 base URI다. `token` query를 붙이거나 `%s` template으로 access token을 주입하지 않는다. +- 프론트 callback은 URL에서 access token을 읽지 않는다. callback 진입 후 cookie를 포함해 `POST /api/v1/auth/refresh`를 호출하고 응답 body의 access token을 사용한다. ## 금지 표현 / 권장 표현 @@ -58,6 +68,8 @@ | Auth | Auth / Security | 인증 API뿐 아니라 `auth/security`의 JWT/OAuth/filter/config/cookie/cache 표면까지 포함하기 때문 | | 유저 객체 | 사용자 주체 / `UserPrincipal` / `User` | 인증 컨텍스트 객체와 aggregate를 구분해야 한다 | | 로그인 토큰 | 액세스 토큰 / 리프레시 토큰 | 토큰 수명과 역할을 구분해야 한다 | +| OAuth callback token query | refresh token cookie + 토큰 갱신 API | access token을 URL에 노출하지 않는 계약을 유지해야 한다 | +| success redirect template / `jwt.redirect-uri` | 로그인 성공 리다이렉트 base URI / `jwt.login-success-redirect-uri` | success callback은 query 없는 base URI를 설정하고 서버가 query를 조립한다 | | 관리자 JWT | 개발자 토큰 | 현재 운영 기능의 명칭과 맞춘다 | ## 주요 근거 파일 @@ -76,9 +88,14 @@ - `src/main/java/com/techfork/auth/security/AuthSecurityConstants.java` - `src/main/java/com/techfork/auth/security/config/SecurityConfig.java` - `src/main/java/com/techfork/auth/security/jwt/JwtUtil.java` +- `src/main/java/com/techfork/auth/security/jwt/JwtProperties.java` - `src/main/java/com/techfork/auth/security/token/RefreshTokenStore.java` - `src/main/java/com/techfork/auth/security/cache/UserAuthCacheStore.java` - `src/main/java/com/techfork/auth/security/cookie/RefreshTokenCookieWriter.java` - `src/main/java/com/techfork/auth/security/util/HeaderUtil.java` - `src/main/java/com/techfork/auth/security/cache/UserAuthCacheInvalidationListener.java` - `src/main/java/com/techfork/auth/security/oauth/UserPrincipal.java` +- `src/main/java/com/techfork/auth/security/handler/login/OAuth2AuthenticationSuccessHandler.java` +- `src/main/java/com/techfork/auth/security/handler/login/OAuth2LoginRedirectUrlFactory.java` +- `src/main/java/com/techfork/auth/security/handler/login/OAuth2LoginRefreshTokenIssuer.java` +- `src/main/java/com/techfork/auth/security/handler/login/OAuth2LoginRefreshTokenWriter.java` From 21ad75db34ecff22c082458f45fbf0b66f72d194 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Sun, 21 Jun 2026 07:07:52 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20=EC=A0=84=EC=88=A0=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20=EB=AC=B8=EC=84=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tactical-design.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/tactical-design.md b/docs/tactical-design.md index cc3b5a4d..9a3bc810 100644 --- a/docs/tactical-design.md +++ b/docs/tactical-design.md @@ -19,7 +19,7 @@ | Activity | `ReadPost`, `Bookmark`, `SearchHistory` | `FirstReadPost` | `Bookmark`는 `userId + postId` 조합이 유일해야 한다. `ReadPost`는 같은 사용자+게시글 중복 저장을 허용한다. `FirstReadPost`는 `userId + postId` 유니크 제약으로 "조회수 증가 자격"을 한 번만 부여하는 dedupe ledger 역할을 한다. `SearchHistory`는 같은 검색어를 중복 저장한다 (동일 검색어의 반복 횟수 자체가 개인화 관심 신호가 된다). 행동 기록은 삭제되지 않고 보존된다 (북마크 제외). | 각 행동 기록이 독립 record aggregate처럼 동작한다. 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `activity/` 아래에서 `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` 같은 경계 재설계는 후속 단계로 미룬다. | | Search | 명시적 쓰기 애그리거트 없음 | `SearchResult` DTO, `PostDocument` read model | 검색어를 기반으로 검색 결과를 계산한다. 검색 결과는 저장되는 도메인 상태가 아니라 조회 결과다. | Search는 애그리거트보다 query service/read model 중심 컨텍스트다. | | Recommendation | **표준: `RecommendationSet`** (현재 코드: `RecommendedPost` 단건) | `RecommendedPost`, `RecommendationHistory` | 같은 `userId + rankOrder` 조합은 유일해야 한다. 새 추천 저장 전 기존 추천은 모두 `RecommendationHistory`로 이동해야 한다. `rankOrder`는 1..N 연속이어야 한다. | 현재 `RecommendedPost` 단건이 루트 역할을 하지만 `RecommendationSet` 개념으로 리팩터링 대상이다 (코드 미반영, 유비쿼터스 언어 README의 문서-코드 동기화 상태 참조). | -| Auth / Security | 독립 애그리거트 없음 | Refresh Token 저장소, `UserPrincipal` | 토큰 발급/검증/갱신을 수행한다. 사용자 자체는 User Account 컨텍스트에 속한다. | Auth / Security는 도메인 애그리거트보다 보안 애플리케이션/인프라 컨텍스트다. | +| 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으로 문서와 테스트를 고정한다. | | Notification | `NotificationToken` | 없음 | 사용자별 알림 토큰과 활성 여부를 관리한다. 같은 사용자의 토큰은 하나만 활성 상태여야 한다. | 현재 행동은 약하지만 독립 루트 후보로 볼 수 있다. | | Admin / Ops | 독립 애그리거트 없음 | Batch Job Execution, Webhook payload | 운영자가 배치를 수동 실행하거나 실패 알림을 보낸다. | 운영 유스케이스 컨텍스트이며 핵심 도메인 애그리거트는 없다. | @@ -123,6 +123,30 @@ createSocialUser() → PENDING 현재 코드명 `markAsisClicked` → `markAsClicked`로 변경 필요. (`asis`는 오타) +#### Auth / Security + +Auth / Security는 `User` 같은 도메인 애그리거트를 직접 소유하지 않는다. 전술 설계상 핵심은 토큰/쿠키/리다이렉트/인증 캐시를 다루는 보안 application/infrastructure seam을 명시적으로 고정하는 것이다. + +**현재 canonical seams** + +- `AuthCommandService.refreshToken()` / `logout()`은 refresh token cookie 값을 입력으로 받아 Redis의 `RefreshTokenStore`와 JWT type/유효성을 함께 검증한다. +- `RefreshTokenCookieWriter`는 refresh token의 `Set-Cookie` 작성/삭제 정책(`HttpOnly`, `Secure`, `SameSite=None`, domain, path, max-age)을 한 곳에 모은다. +- `OAuth2AuthenticationSuccessHandler`는 OAuth/OIDC 성공 후 `OAuth2LoginRefreshTokenIssuer`와 `OAuth2LoginRefreshTokenWriter`를 통해 refresh token만 발급·저장·쿠키화한다. +- `OAuth2LoginRedirectUrlFactory`는 `jwt.login-success-redirect-uri`를 query 없는 callback base URI로 사용하고 `registered`, `email` query만 조립·인코딩한다. `token` query나 `%s` access token template은 성공 리다이렉트 계약이 아니다. +- `UserAuthCacheInvalidationListener`는 User Account 이벤트를 받아 인증 캐시를 무효화한다. 온보딩 완료/재활성화는 `AFTER_COMMIT`, 회원 탈퇴는 보안 민감 seam이므로 `BEFORE_COMMIT + AFTER_COMMIT`으로 처리한다. + +**브라우저 OAuth 성공 흐름** + +1. Spring Security OAuth/OIDC 성공 후 `UserPrincipal`이 인증 주체가 된다. +2. 서버는 refresh token을 Redis에 저장하고 HttpOnly cookie로 내려준다. +3. 서버는 프론트 callback으로 redirect하되 URL에는 `registered`, `email`만 포함한다. +4. 프론트 callback은 cookie를 포함해 `POST /api/v1/auth/refresh`를 호출하고 응답 body의 access token을 사용한다. + +**후속 분리 후보** + +- `JwtProperties`는 현재 JWT secret/expiration과 OAuth 성공·실패 redirect URI를 함께 담는다. 브라우저 OAuth flow 설정이 더 늘어나면 `OAuthRedirectProperties` 같은 별도 configuration object로 분리할 수 있다. +- User Account 조회 seam은 현재 `UserPrincipal`/인증 캐시 중심으로 충분하지만, 물리 분리 시에는 최소 인증 프로필 published query 또는 ACL로 분리한다. + --- ### 1.3 값 객체(Value Object) 후보 @@ -225,6 +249,7 @@ User Account 관련 이벤트는 Personalization/Auth 후처리와 분리되었 | Search → Post | 유지 | Search 는 `PostDocument` 를 검색 후보 projection 으로 소비한다. `PostRepository` 사용은 후보 탐색이 아니라 `viewCount` 같은 metadata 조합으로 한정한다. | metadata 용 별도 read model / query port | | Recommendation → Post | 유지 | Recommendation 은 후보 탐색에는 `PostDocument` projection 을 사용하고, 저장 시점에만 `Post` reference 를 확보한다. 후보 탐색 seam 과 저장 seam 을 분리해서 해석한다. | 추천 저장용 최소 식별 공유, 이벤트 기반 추천 재생성 | | Source → Post 생성 | 유지 | `RssFeedItem` 은 Source 가 외부 RSS 를 정제해 만든 **현재 monolith 내부 handoff DTO** 로 보고, `Post.create(RssFeedItem, TechBlog)` 경계를 유지한다. 아직 published language 로 분리된 상태는 아니다. | Post 소유 command/published language, `TechnicalPostDiscovered` 이벤트 handoff | +| 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 | 명시적 제외: