From df4d3b4345502b6d097b5ad93b4ab1584da54556 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Thu, 3 Sep 2026 16:44:12 +0900 Subject: [PATCH] docs: map published open questions to lab coverage and fix experiment order Co-Authored-By: Claude Opus 5 --- .../console-2026-09-03T07-41-09-675Z.log | 1 + .../console-2026-09-03T07-41-27-913Z.log | 1 + .../console-2026-09-03T07-41-40-247Z.log | 1 + .../console-2026-09-03T07-41-50-414Z.log | 1 + .../console-2026-09-03T07-42-00-948Z.log | 1 + .../page-2026-09-03T07-41-09-984Z.yml | 25 ++ .../page-2026-09-03T07-41-27-980Z.yml | 0 .../page-2026-09-03T07-41-40-313Z.yml | 0 .../page-2026-09-03T07-41-50-480Z.yml | 0 .../page-2026-09-03T07-42-01-011Z.yml | 0 docs/open-questions-coverage.md | 283 ++++++++++++++++++ 11 files changed, 313 insertions(+) create mode 100644 .playwright-mcp/console-2026-09-03T07-41-09-675Z.log create mode 100644 .playwright-mcp/console-2026-09-03T07-41-27-913Z.log create mode 100644 .playwright-mcp/console-2026-09-03T07-41-40-247Z.log create mode 100644 .playwright-mcp/console-2026-09-03T07-41-50-414Z.log create mode 100644 .playwright-mcp/console-2026-09-03T07-42-00-948Z.log create mode 100644 .playwright-mcp/page-2026-09-03T07-41-09-984Z.yml create mode 100644 .playwright-mcp/page-2026-09-03T07-41-27-980Z.yml create mode 100644 .playwright-mcp/page-2026-09-03T07-41-40-313Z.yml create mode 100644 .playwright-mcp/page-2026-09-03T07-41-50-480Z.yml create mode 100644 .playwright-mcp/page-2026-09-03T07-42-01-011Z.yml create mode 100644 docs/open-questions-coverage.md diff --git a/.playwright-mcp/console-2026-09-03T07-41-09-675Z.log b/.playwright-mcp/console-2026-09-03T07-41-09-675Z.log new file mode 100644 index 0000000..e06e4d0 --- /dev/null +++ b/.playwright-mcp/console-2026-09-03T07-41-09-675Z.log @@ -0,0 +1 @@ +[ 295ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0 diff --git a/.playwright-mcp/console-2026-09-03T07-41-27-913Z.log b/.playwright-mcp/console-2026-09-03T07-41-27-913Z.log new file mode 100644 index 0000000..8fcfb02 --- /dev/null +++ b/.playwright-mcp/console-2026-09-03T07-41-27-913Z.log @@ -0,0 +1 @@ +[ 125ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0 diff --git a/.playwright-mcp/console-2026-09-03T07-41-40-247Z.log b/.playwright-mcp/console-2026-09-03T07-41-40-247Z.log new file mode 100644 index 0000000..96bf008 --- /dev/null +++ b/.playwright-mcp/console-2026-09-03T07-41-40-247Z.log @@ -0,0 +1 @@ +[ 95ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0 diff --git a/.playwright-mcp/console-2026-09-03T07-41-50-414Z.log b/.playwright-mcp/console-2026-09-03T07-41-50-414Z.log new file mode 100644 index 0000000..d9ccac2 --- /dev/null +++ b/.playwright-mcp/console-2026-09-03T07-41-50-414Z.log @@ -0,0 +1 @@ +[ 91ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0 diff --git a/.playwright-mcp/console-2026-09-03T07-42-00-948Z.log b/.playwright-mcp/console-2026-09-03T07-42-00-948Z.log new file mode 100644 index 0000000..490cc1e --- /dev/null +++ b/.playwright-mcp/console-2026-09-03T07-42-00-948Z.log @@ -0,0 +1 @@ +[ 88ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0 diff --git a/.playwright-mcp/page-2026-09-03T07-41-09-984Z.yml b/.playwright-mcp/page-2026-09-03T07-41-09-984Z.yml new file mode 100644 index 0000000..997b2a2 --- /dev/null +++ b/.playwright-mcp/page-2026-09-03T07-41-09-984Z.yml @@ -0,0 +1,25 @@ +- generic [ref=f7e3]: + - link "본문으로 건너뛰기" [ref=f7e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f7e5]: + - generic [ref=f7e6]: + - link "TechLog 홈" [ref=f7e8] [cursor=pointer]: + - /url: / + - text: TechLog + - generic [ref=f7e9]: + - button "TechLog 검색 열기" [ref=f7e11] [cursor=pointer]: 검색 + - group [ref=f7e12]: + - generic "메뉴" [ref=f7e13] [cursor=pointer] + - generic [ref=f7e14]: + - paragraph [ref=f7e15]: 화면을 준비하고 있습니다. + - generic [ref=f7e16]: TechLog 로딩 중 + - contentinfo [ref=f7e17]: + - generic [ref=f7e18]: + - generic [ref=f7e19]: + - paragraph [ref=f7e20]: 동현 + - paragraph [ref=f7e21]: 문제를 재현하고 검증해 실제 운영에 적용할 수 있는 형태로 정리합니다. + - generic [ref=f7e22]: + - link "프로필" [ref=f7e23] [cursor=pointer]: + - /url: /profile + - link "변경 기록" [ref=f7e24] [cursor=pointer]: + - /url: /releases \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-03T07-41-27-980Z.yml b/.playwright-mcp/page-2026-09-03T07-41-27-980Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-09-03T07-41-40-313Z.yml b/.playwright-mcp/page-2026-09-03T07-41-40-313Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-09-03T07-41-50-480Z.yml b/.playwright-mcp/page-2026-09-03T07-41-50-480Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-09-03T07-42-01-011Z.yml b/.playwright-mcp/page-2026-09-03T07-42-01-011Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/docs/open-questions-coverage.md b/docs/open-questions-coverage.md new file mode 100644 index 0000000..f2a6bb3 --- /dev/null +++ b/docs/open-questions-coverage.md @@ -0,0 +1,283 @@ +# 열린 질문 커버리지 — 이 실험대로 답할 수 있는가 + +공개 기록(`hyeonworks.com/questions`)에 등록된 KeyCloak Patterns 열린 질문 +네 개를, 이 실험대가 실제로 검증할 수 있는지 대조한 결과. + +**결론 — 네 개 모두 이 실험대에서 재현 가능하다. 다만 로드맵에 빠진 항목이 +있고, 순서가 한 곳 뒤집혀 있다.** + +| # | 질문 | 게시 | 로드맵 커버 | +|---|---|---|---| +| Q1 | [서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가](https://hyeonworks.com/questions/server-session-pattern-multi-instance) | 2026.08.29 | **부분** | +| Q2 | [Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가](https://hyeonworks.com/questions/refresh-rotation-replica-contention) | 2026.08.26 | **부분** | +| Q3 | [BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가](https://hyeonworks.com/questions/bff-session-authorized-client-store) | 2026.08.30 | **부분** | +| Q4 | [Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가](https://hyeonworks.com/questions/edge-authorization-scope) | 2026.08.31 | **없음** | + +--- + +## 발견한 구조적 문제 + +### 1. 순서가 뒤집혀 있다 + +Q2가 명시한다. + +> 이 경쟁은 **저장소를 공유한 뒤에야 재현**되기 때문에 저장소 결정을 하고 +> 나서 해당 문제를 이어서 풀어보자. + +즉 **Q3(저장소 결정) → Q2(경쟁 재현)** 순서다. 그런데 로드맵은 +`refresh-token-concurrency`를 `redis-app-session-store`보다 **앞**에 두었다. + +**Q2를 먼저 시도하면 재현 자체가 불가능하다.** 저장소가 process-local이면 +두 replica가 같은 refresh token 항목을 보지 않기 때문이다. + +→ 로드맵 순서를 교정한다. + +### 2. Session과 Authorized Client는 조회 키가 다르다 + +Q3의 핵심이며 로드맵에 이 구분이 없었다. + +| 상태 | 조회 키 | 저장 위치(현재) | +|---|---|---| +| Application Session | **session ID** | 서블릿 컨테이너 in-memory | +| OAuth2AuthorizedClient | **client registration 이름 + principal name** | 자동구성 in-memory | + +**`session ID`가 조회 키에 없다.** 그래서 같은 사용자가 두 브라우저에서 +로그인하면 **동일한 authorized client 항목을 공유**한다. + +Q1의 제약이 이를 그대로 지적한다. + +> 여러 인스턴스가 같은 세션을 사용할 수 있도록 Session Store를 공유 +> 저장소로 변경하는 것만으로는 **충분하지 않다.** + +→ 실험을 "Redis 도입" 하나로 뭉뚱그리면 안 된다. **두 저장소를 각각 설계하고 +각각 검증해야 한다.** + +### 3. 이미 해결한 문제가 질문에도 있다 + +Q1의 제약: + +> Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록 +> **network에서 강제된 상태가 아니다.** + +이는 2홉 헤더 실험에서 마주친 **프록시 우회 경로**와 같은 문제이며, +NetworkPolicy로 닫는 방법을 이미 확립했다 +([`two-hop-proxy-header-contract.md`](two-hop-proxy-header-contract.md) 11절). + +→ Q1에 답할 때 그 패턴을 그대로 재사용한다. + +--- + +## Q1. 다중 인스턴스 운영 + +**질문이 요구하는 검증 5단계** + +| # | 검증 | 실험대 가능 | 로드맵 | +|---|---|---|---| +| 1 | 한쪽에서 로그인 후 **다른 인스턴스로 요청 시 200 유지** | 가능 | 없음 | +| 2 | 한 인스턴스 재시작 후 **같은 session cookie로 상태 유지** | 가능 | 없음 | +| 3 | 같은 사용자 두 브라우저 → **authorized client 덮어쓰는가** | 가능 | **없음** | +| 4 | 한쪽 logout 후 **다른 쪽 요청** | 가능 | 부분 (백채널 로그아웃) | +| 5 | **session 만료 ≠ token 만료** 각 경우의 응답과 화면 | 가능 | **없음** | + +**실험대 준비 상태** — BFF를 2 replica로 띄우면 전부 재현된다. 호스트 nginx의 +`ip_hash` 주석을 켜고 끄면 **스티키 유무 비교**까지 같은 구성에서 된다. + +**추가로 필요한 것** + +- BFF 이미지 (아직 `bff/` 디렉터리에 소스 없음) +- 로그아웃 전파를 관찰할 두 번째 앱 (`app2.hyeonworks.com` 이름은 확보) + +**3번이 특히 중요하다.** "Redis만 붙이면 해결"이라는 착각을 깨는 항목이고, +조회 키가 다르다는 사실의 실증이다. + +--- + +## Q2. Refresh Token Rotation 경쟁 + +**질문이 요구하는 검증 5단계** + +| # | 검증 | 실험대 가능 | 로드맵 | +|---|---|---|---| +| 1 | replica 두 대에서 **access token 만료 직후 동시 요청** | 가능 | 있음 | +| 2 | **이긴 쪽/지는 쪽 응답** 각각 기록 | 가능 | 부분 | +| 3 | 지는 쪽이 **저장된 새 token으로 재시도해 성공하는가** | 가능 | **없음** | +| 4 | **지는 쪽 사용자 화면**에 무엇이 보이는가 | 가능 | **없음** | +| 5 | **lock 유무를 같은 입력으로 비교** (실패율·지연) | 가능 | **없음** | + +**5번이 결론을 내는 기준이다.** + +> 실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다. + +로드맵에 없던 항목인데, **이것이 없으면 질문에 답할 수 없다.** + +**제약을 지켜야 한다** + +- rotation + 재사용 0회는 **전제로 고정**한다. 바꾸지 않고 답한다 +- 이미 발급된 access token은 만료 전까지 통하므로 **재현은 access token 만료 + 직후에 맞춰 실행**한다. 그렇지 않으면 실패가 화면에 보이지 않는다 + +**선행 조건** — Q3의 저장소 공유가 먼저다. + +--- + +## Q3. BFF 저장소 결정 + +**질문이 요구하는 검증 5단계** + +| # | 검증 | 실험대 가능 | 로드맵 | +|---|---|---|---| +| 1 | 인스턴스 두 대에서 **로그인 유지와 재시작 복구** | 가능 | 부분 | +| 2 | 저장소를 열어 **refresh token이 평문인가** | 가능 | **없음** | +| 3 | **session TTL ≠ token 만료** 그 순간의 응답과 화면 | 가능 | **없음** | +| 4 | logout 뒤 **두 store에 잔여 항목이 없는가** | 가능 | 부분 | +| 5 | **저장소를 끊은 상태**에서 로그인·API 호출 오류 | 가능 | 있음 | + +**로드맵에 없던 큰 항목 — 후보 비교** + +질문은 "Redis로 간다"가 아니라 **"Redis와 JDBC 중 무엇이 이 접근 패턴에 +맞는가"** 를 묻는다. + +> 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다. + +이 실험대에는 PostgreSQL이 이미 있으므로 **JDBC 후보를 같은 조건에서 비교할 +수 있다.** Redis만 붙이면 질문의 절반만 답하는 셈이다. + +**2번(평문 확인)의 실행 방법** + +```bash +kubectl -n exec -it deploy/redis -- redis-cli --scan --pattern 'spring:session:*' +kubectl -n exec -it deploy/redis -- redis-cli GET +``` + +저장소를 직접 열어 refresh token이 그대로 읽히는지 본다. 읽힌다면 +암호화 설계가 필요하고, 그 key 교체 절차는 별도 과제다. + +--- + +## Q4. Edge 인가 범위 — 로드맵에 전혀 없다 + +이 축을 A층(Keycloak)·B층(앱 세션) 중심으로 잡으면서 **AP4의 인가 범위 +질문을 빠뜨렸다.** + +**질문이 요구하는 검증** + +| 검증 | 실험대 가능 | +|---|---| +| role을 헤더에 담고 **다중 값 구분자·escaping** 확인 | 가능 | +| **헤더 크기 상한** 초과 시 proxy가 자르는가 요청이 거부되는가 | 가능 | +| role 변경 후 **몇 번째 요청부터 반영되는가** | 가능 | +| upstream이 헤더 존재만 보는가 값과 service identity까지 보는가 | 가능 | + +**이 실험대에서 특히 잘 맞는 이유** + +nginx의 헤더 처리 특성을 이미 실측했다. 질문이 지적한 + +> Nginx는 client가 보낸 동명 헤더를 merge하지 않고 **덮어쓴다.** + +는 2홉 헤더 실험에서 `proxy_set_header X-Forwarded-For $remote_addr`로 +확인한 그 동작이다. **`X-Auth-Request-*`도 같은 규칙을 따르는지**를 같은 +방법으로 검증할 수 있다. + +그리고 질문의 제약 + +> internal token 검사가 controller 한 곳에만 있다. 헤더를 늘리기 전에 이 +> 검사를 **공통 경계로 옮겨야** 된다. + +는 코드 변경이므로 `backend/`에서 진행한다. + +--- + +## 교정된 실험 순서 + +기존 로드맵의 순서를 질문의 의존 관계에 맞춰 조정한다. + +``` +✅ 환경 구축 +✅ 2홉 프록시 헤더 계약 +────────────────────────────────────────────────────────── +1. Keycloak 멀티노드 클러스터 형성 (선행 인프라) +2. persistent vs volatile 세션 (A층) +3. BFF 저장소 결정 → Q3 ★ Q2 의 선행 조건 +4. 다중 인스턴스 운영 → Q1 +5. Refresh Token 경쟁 → Q2 ★ 3 이후여야 재현됨 +6. Edge 인가 범위 → Q4 ← 새로 추가 +7. 장애 주입과 복구 (전 항목 공통) +``` + +**바뀐 점** + +- `refresh-token-concurrency`가 `redis-app-session-store` **뒤로** 이동 +- 저장소 결정이 **Redis 도입**이 아니라 **Redis vs JDBC 비교**로 확장 +- **Edge 인가 범위(Q4)** 신규 추가 + +## 브랜치 매핑 + +| 실험 | 브랜치 | 상태 | +|---|---|---| +| 멀티노드 클러스터 | `feature/keycloak-multinode-cluster-jdbc-ping` | 존재 | +| persistent vs volatile | `feature/keycloak-persistent-vs-volatile-sessions` | 존재 | +| BFF 저장소 (Q3) | `feature/keycloak-redis-app-session-store` | 존재 — **범위 확장 필요** | +| 다중 인스턴스 (Q1) | — | **없음** | +| refresh 경쟁 (Q2) | `feature/keycloak-refresh-token-concurrency` | 존재 | +| Edge 인가 (Q4) | — | **없음** | +| 장애 주입 | `feature/keycloak-failure-injection-recovery` | 존재 | + +**두 개를 새로 만들어야 한다.** + +```bash +git checkout develop-keycloak-session-store +git checkout -b feature/keycloak-multi-instance-session-operation +git checkout -b feature/keycloak-edge-authorization-scope +``` + +## 공통 선행 조건 — BFF 구현은 이미 있다 + +세 질문(Q1·Q2·Q3)이 모두 **BFF를 2 replica로 띄우는 것**을 전제한다. +`develop-keycloak-session-store`의 `bff/`에는 빌드 산출물만 있지만, +**`develop-keycloak-pattern3`에 구현이 완성되어 있다.** + +``` +bff/Dockerfile +bff/pom.xml +bff/src/main/java/com/example/keycloakpattern/bff/ + ├ BffApplication.java + ├ BffController.java + ├ CsrfController.java + ├ SecurityConfig.java + └ SpaCsrfTokenRequestHandler.java +bff/src/main/resources/application.yml +bff/src/main/resources/static/{index.html,app.js} +bff/src/test/java/.../BffControllerTest.java +``` + +→ 새로 구현할 필요가 없다. **AP3 브랜치에서 이 실험대로 가져온다.** + +```bash +git checkout develop-keycloak-session-store +git checkout develop-keycloak-pattern3 -- bff/ +``` + +가져온 뒤 확인할 것 — 질문들이 지목한 부분이 코드에 그대로 있는지. + +| 확인 | 어디를 볼 것인가 | +|---|---| +| Session 저장소가 in-memory 자동구성인가 | `SecurityConfig.java`, `application.yml`에 Spring Session 설정 부재 | +| `OAuth2AuthorizedClientService`가 in-memory인가 | Bean 정의 부재 → 자동구성 결과 확인 필요 | +| authorized client 조회에 session ID가 없는가 | Spring Security 기본 계약 | + +Q3가 "어떤 구현체가 실제로 쓰이는지는 자동구성 결과까지 확인해야 정확히 +알 수 있다"고 남긴 미지수를, **기동 후 Bean을 실제로 조회해서** 확정할 수 있다. + +```bash +kubectl -n exec deploy/bff -- \ + curl -s localhost:8082/actuator/beans | grep -i authorizedClientService +``` + +## 참고 + +| 문서 | 관계 | +|---|---| +| [`session-store-lab-roadmap.md`](session-store-lab-roadmap.md) | 이 문서가 그 순서를 교정한다 | +| [`two-hop-proxy-header-contract.md`](two-hop-proxy-header-contract.md) | Q1의 우회 경로 제약, Q4의 헤더 덮어쓰기 근거 | +| [`session-lab-operations.md`](session-lab-operations.md) | 실행 도구와 명령 | +| [`four-pattern-tradeoff-matrix.md`](four-pattern-tradeoff-matrix.md) | Q4가 되돌아가는 선택지(BFF)의 비교표 |