Skip to content

Commit d4dd0cc

Browse files
authored
[docs/#432] Auth / Security DDD 개선 문서 동기화 (#448)
* docs: 로드맵 정리 * docs: 테스트 문서 정리 * docs: 유비쿼터스 언어 갱신 * docs: 전술 경계 문서 정리
1 parent 59aacad commit d4dd0cc

4 files changed

Lines changed: 93 additions & 22 deletions

File tree

docs/ddd-test-refactoring-roadmap.md

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> 목적: TechFork 서버를 DDD 관점으로 점진적으로 개선하면서, 아직 부족한 테스트 코드를 어떤 순서로 작성·개선할지 정리한다.
44
> 관련 문서: [`docs/ubiquitous-language/README.md`](./ubiquitous-language/README.md)
5-
> 기준 시점: **2026-06-03, #411 병합 후 `docs/#412` 시작 시점**
5+
> 기준 시점: **2026-06-21, #432 Auth / Security 문서 동기화 시점**
66
77
## 1. 결론
88

@@ -35,7 +35,7 @@ DDD 목표 지도 작성
3535
→ 다음 영역으로 이동
3636
```
3737

38-
2026-06-03 현재 상태를 요약하면 다음과 같다.
38+
2026-06-21 현재 상태를 요약하면 다음과 같다.
3939

4040
```text
4141
[완료] Phase 0: DDD 기준선 문서화
@@ -45,7 +45,11 @@ DDD 목표 지도 작성
4545
[완료] Phase 3: Bookmark / SearchQuery 용어 정리, EDifficultyLevel 제거 반영
4646
[완료] Activity 4.1, User Account, Personalization Profile 1차 DDD 경계 정리
4747
[완료] PersonalizedProfileGeneratedEvent로 Personalization → Recommendation 후처리 분리
48-
[다음] 문서/유비쿼터스 언어 최신화 후 RecommendationSet, MMR, Search/Recommendation 후속 모델 정리
48+
[완료] Auth / Security 1차 DDD 경계 정리
49+
- `auth` 최상위 컨텍스트와 `auth/security` shared kernel 정리
50+
- User Account 조회 seam, auth cache 이벤트 seam, OAuth/OIDC 책임, DTO 경계 정리
51+
- OAuth 성공 redirect access token 제거와 refresh token cookie 기반 access token 재발급 계약 반영
52+
[다음] RecommendationSet, MMR, Search/Recommendation 후속 모델 정리
4953
```
5054

5155
---
@@ -855,32 +859,32 @@ global/util/ContentCleaner -> Source / Post shared content support 후
855859

856860
##### 권장 회수 순서
857861

858-
현재 시점 기준 추천 순서는 다음과 같다.
862+
현재 시점 기준 권장 회수 상태와 후속 순서는 다음과 같다.
859863

860864
```text
861-
1. User Account 4.3
862-
- 필요 시 Auth / Security shared seam(UserAuthCacheStore, UserPrincipal)만 함께 정리
863-
- Auth / Security 전체 경계 이동은 별도 이슈에서 진행한다
865+
1. User Account 4.3 — 완료
866+
- Auth / Security shared seam(UserAuthCacheStore, UserPrincipal)과의 이벤트/조회 경계를 정리했다
864867
865-
2. Personalization Profile 4.4
866-
- User Account와의 직접 호출 책임을 먼저 정리
868+
2. Personalization Profile 4.4 — 완료
869+
- User Account와의 직접 호출 책임을 이벤트 기반으로 분리했다
867870
868-
3. Auth / Security
871+
3. Auth / Security — 1차 완료
869872
- `auth` 최상위 컨텍스트와 `auth/security` shared kernel로 정리
870-
- 이후 문서/테스트 경로가 현재 구조를 따르도록 유지
873+
- User Account aggregate 소유권은 유지하고 인증 최소 정보, principal/cache/token 정책만 소유
874+
- #427, #428, #429, #430, #431, #433, #436, #440, #442 기준 완료 상태를 문서/테스트 경로에 반영
871875
872-
4. Recommendation / Search
876+
4. Recommendation / Search — 다음
873877
- VectorQueryBuilder, LinearTimeDecayStrategy, RRF/검색 정책 support를 owning context로 회수
874878
875-
5. Source / Post shared content support
879+
5. Source / Post shared content support — 후속
876880
- InitialDataConfig, ContentCleaner, 일부 converter/util을 owning context나 shared support로 재배치
877881
```
878882

879883
##### Activity / Post 1차 정리 이후의 해석
880884

881885
- Activity와 Post는 1차 정리가 끝났더라도, 지금 남아 있는 `global` 의존 중 상당수는 진짜 shared support이거나 다른 컨텍스트 소유다.
882886
- 따라서 **Activity/Post 관련 `global` 정리를 먼저 별도 작업으로 시작하지 않는다.**
883-
- 대신 이후 `Auth / Security`, `Recommendation / Search`, `Source`를 정리할 때 각각의 소유 코드를 회수한다.
887+
- Auth / Security 1차 정리는 완료되었으므로, 이후 `Recommendation / Search`, `Source`를 정리할 때 각각의 소유 코드를 회수한다.
884888

885889
---
886890

@@ -1098,12 +1102,22 @@ src/test/java/com/techfork
10981102
[완료] 8-2. Personalization → Recommendation 이벤트 경계 분리
10991103
- `PersonalizedProfileGeneratedEvent` 도입
11001104
- Recommendation 리스너가 `AFTER_COMMIT`에서 이벤트 스냅샷 기반 추천 생성
1105+
[완료] 8-3. Auth / Security 1차 경계 정리
1106+
- #427: Auth / Security DDD 리팩터링 전 회귀 테스트 보강
1107+
- #428: `auth` 최상위 컨텍스트와 `auth/security` shared kernel 소유 표면 정리
1108+
- #429: Auth 서비스의 UserRepository 직접 의존 경계 정리
1109+
- #430: User Account 이벤트 기반 auth cache 무효화 seam 안정화
1110+
- #431: OAuth/OIDC 보안 인프라 책임 분리
1111+
- #433: OAuth2/OIDC redirect/cookie 브라우저 보안 정책 정리
1112+
- #436: Auth DTO를 Command/Response 경계로 분리
1113+
- #440: OAuth 성공 redirect access token 제거 및 refresh 연동 계약 정리
1114+
- #442: auth security 내부 store/cache/cookie 책임 명명 정리
11011115
[부분 진행] 9. Recommendation/Search 회귀 테스트 보강
11021116
- LlmRecommendationServiceTest, SearchServiceImplTest 반영
11031117
- MmrServiceTest 추가 필요
1104-
[후속] 10. global 소유권 회수
1118+
[후속] 10. Recommendation/Search 및 Source/Post shared support 소유권 회수
11051119
- global-first가 아니라 owner-context-first 방식으로 진행
1106-
- Auth / Security -> Recommendation/Search -> Source/Post shared support 순으로 회수
1120+
- Auth / Security 1차 회수 이후 Recommendation/Search -> Source/Post shared support 순으로 회수
11071121
[다음] 11. 컨텍스트 1차 정리 후 전술 모델 2차 정리
11081122
- aggregate / value object / ID reference / 엔티티 경계 정교화
11091123
[다음] 12. Phase 6 진입 조건 충족 후 이벤트/포트 분리 시작

docs/tactical-design.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
| 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` 같은 경계 재설계는 후속 단계로 미룬다. |
2020
| Search | 명시적 쓰기 애그리거트 없음 | `SearchResult` DTO, `PostDocument` read model | 검색어를 기반으로 검색 결과를 계산한다. 검색 결과는 저장되는 도메인 상태가 아니라 조회 결과다. | Search는 애그리거트보다 query service/read model 중심 컨텍스트다. |
2121
| 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으로 문서와 테스트를 고정한다. |
2323
| Notification | `NotificationToken` | 없음 | 사용자별 알림 토큰과 활성 여부를 관리한다. 같은 사용자의 토큰은 하나만 활성 상태여야 한다. | 현재 행동은 약하지만 독립 루트 후보로 볼 수 있다. |
2424
| Admin / Ops | 독립 애그리거트 없음 | Batch Job Execution, Webhook payload | 운영자가 배치를 수동 실행하거나 실패 알림을 보낸다. | 운영 유스케이스 컨텍스트이며 핵심 도메인 애그리거트는 없다. |
2525

@@ -123,6 +123,30 @@ createSocialUser() → PENDING
123123

124124
현재 코드명 `markAsisClicked``markAsClicked`로 변경 필요. (`asis`는 오타)
125125

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`이 인증 주체가 된다.
141+
2. 서버는 refresh token을 Redis에 저장하고 HttpOnly cookie로 내려준다.
142+
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+
126150
---
127151

128152
### 1.3 값 객체(Value Object) 후보
@@ -225,6 +249,7 @@ User Account 관련 이벤트는 Personalization/Auth 후처리와 분리되었
225249
| Search → Post | 유지 | Search 는 `PostDocument` 를 검색 후보 projection 으로 소비한다. `PostRepository` 사용은 후보 탐색이 아니라 `viewCount` 같은 metadata 조합으로 한정한다. | metadata 용 별도 read model / query port |
226250
| Recommendation → Post | 유지 | Recommendation 은 후보 탐색에는 `PostDocument` projection 을 사용하고, 저장 시점에만 `Post` reference 를 확보한다. 후보 탐색 seam 과 저장 seam 을 분리해서 해석한다. | 추천 저장용 최소 식별 공유, 이벤트 기반 추천 재생성 |
227251
| 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 |
228253

229254
명시적 제외:
230255

docs/test-gap-analysis.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -445,29 +445,44 @@ SearchControllerIntegrationTest
445445
| `AuthCommandServiceTest` | unit/mock | refresh, logout, developer token command use cases |
446446
| `KakaoLoginCommandServiceTest` | unit/mock | iOS 직접 Kakao login 신규/기존 사용자 |
447447
| `KakaoOAuthServiceTest` | unit/mock | Kakao user info response mapping success/failure |
448+
| `KakaoSocialIdTest` | unit | Kakao REST/OIDC social id 정규화 |
448449
| `AuthControllerIntegrationTest` | integration | refresh/logout/kakao login API |
449450
| `DeveloperTokenControllerIntegrationTest` | integration | developer token 생성, 권한/인증 실패 |
450451
| `SecurityIntegrationTest` | integration | 인증/인가, 토큰 오류, 권한, 탈퇴 사용자 |
452+
| `JwtUtilTest` | unit | refresh token 단독 발급과 token type |
451453
| `JwtAuthenticationFilterTest` | unit/mock | access token filter, cache hit/miss, invalid token |
452-
| `OAuth2AuthenticationSuccessHandlerTest` | unit/mock | OIDC 로그인 성공 redirect/cookie |
454+
| `RefreshTokenCookieWriterTest` | unit | refresh token cookie 작성/삭제 wire contract |
455+
| `OAuth2AuthenticationSuccessHandlerTest` | unit/mock | OAuth2 로그인 성공 refresh token 발급/저장과 redirect |
453456
| `OAuth2AuthenticationFailureHandlerTest` | unit/mock | OIDC 로그인 실패 redirect |
457+
| `OAuth2LoginRedirectUrlFactoryTest` | unit | success/failure redirect URL 조립, token query 제거, email encoding |
458+
| `OAuth2LoginRefreshTokenIssuerTest` | unit/mock | OAuth2 로그인 성공 경로의 refresh token 단독 발급 |
459+
| `OAuth2LoginRefreshTokenWriterTest` | unit/mock | OAuth2 로그인 refresh token 저장과 cookie writer 위임 |
454460
| `CustomOidcUserServiceTest` | unit/mock | Kakao/Apple OIDC 사용자 생성/재사용/재활성화 |
461+
| `OidcSocialIdentityExtractorTest` | unit | Kakao/Apple OIDC claim에서 소셜 식별자 추출 |
455462
| `HttpCookieOAuth2AuthorizationRequestRepositoryTest` | unit/mock | OAuth authorization request cookie 저장/로드/삭제 |
456463
| `UserAuthCacheStoreTest` | unit/mock | auth cache serialization/deserialization |
457464
| `UserAuthCacheInvalidationListenerTest` | unit/mock | User Account 이벤트 기반 auth cache eviction |
458465

459466
#### 평가
460467

461-
Auth/Security는 비교적 안정적이다.
462-
DDD 전환의 핵심 경로는 아니지만, User 컨텍스트 리팩터링 시 인증 모델이 깨지지 않도록 유지해야 한다.
468+
Auth / Security는 1차 DDD 경계 정리 이후 비교적 안정적이다.
469+
`auth` 최상위 컨텍스트와 `auth/security` shared kernel은 테스트로 대부분 보호되어 있고,
470+
OAuth/OIDC handler, redirect URL factory, refresh token issuer/writer, cookie writer가 단위 테스트 가능한 seam으로 분리되었다.
471+
472+
OAuth 성공 redirect는 access token을 URL에 싣지 않고 refresh token cookie를 발급한 뒤,
473+
callback 이후 `/api/v1/auth/refresh`로 access token을 재발급받는 계약으로 정리되었다.
474+
Cookie 속성은 `RefreshTokenCookieWriterTest`로 단위 보호가 생겼지만,
475+
실제 브라우저 cross-origin 환경에서의 통합 검증은 별도 경량 경로가 필요하다.
463476

464477
#### 남은 갭
465478

466479
| 우선순위 || 이유 |
467480
|---|---|---|
468-
| P1 | Auth/Security 통합 테스트의 경량 MySQL+Redis 기반 검증 경로 | 현재 `IntegrationTestBase`는 Elasticsearch까지 기동하므로 auth-only 검증 비용이 크다 |
481+
| P1 | Auth / Security 통합 테스트의 경량 MySQL+Redis 기반 검증 경로 | 현재 auth controller/security integration도 Elasticsearch Testcontainers까지 기동할 수 있어 auth-only 회귀 검증 비용과 실패면이 크다 |
469482
| P1 | withdraw/reactivate 이후 refresh token/auth cache 통합 정책 테스트 | User 상태 전환과 보안 정책 연결 |
470-
| P2 | Cookie 속성(SameSite/Secure/Domain/Max-Age) 통합 검증 | 프론트 연동에 직접 영향이 있는 보안 쿠키 계약 보호 |
483+
| P2 | OAuth success redirect + refresh 연동 통합 계약 | success redirect에는 `registered`, `email`만 남고 access token은 refresh API로만 받는 end-to-end 계약 보호 |
484+
| P2 | 운영 env/secret 변경 계약 문서화 | `JWT_LOGIN_SUCCESS_REDIRECT_URI(_DEV)` 누락 시 OAuth callback 장애가 배포 후 발견될 수 있다 |
485+
| P3 | Cookie 속성(SameSite/Secure/Domain/Max-Age) 통합 검증 | 단위 테스트는 있으나 실제 브라우저/cross-origin 쿠키 전달 계약은 e2e 또는 경량 통합 경로가 필요 |
471486

472487
---
473488

0 commit comments

Comments
 (0)