12 KiB
열린 질문 커버리지 — 이 실험대로 답할 수 있는가
공개 기록(hyeonworks.com/questions)에 등록된 KeyCloak Patterns 열린 질문
네 개를, 이 실험대가 실제로 검증할 수 있는지 대조한 결과.
결론 — 네 개 모두 이 실험대에서 재현 가능하다. 다만 로드맵에 빠진 항목이 있고, 순서가 한 곳 뒤집혀 있다.
| # | 질문 | 게시 | 로드맵 커버 |
|---|---|---|---|
| Q1 | 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 | 2026.08.29 | 부분 |
| Q2 | Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가 | 2026.08.26 | 부분 |
| Q3 | BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가 | 2026.08.30 | 부분 |
| Q4 | Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가 | 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 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번(평문 확인)의 실행 방법
kubectl -n <ns> exec -it deploy/redis -- redis-cli --scan --pattern 'spring:session:*'
kubectl -n <ns> exec -it deploy/redis -- redis-cli GET <key>
저장소를 직접 열어 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 |
존재 |
두 개를 새로 만들어야 한다.
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 브랜치에서 이 실험대로 가져온다.
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을 실제로 조회해서 확정할 수 있다.
kubectl -n <ns> exec deploy/bff -- \
curl -s localhost:8082/actuator/beans | grep -i authorizedClientService
참고
| 문서 | 관계 |
|---|---|
session-store-lab-roadmap.md |
이 문서가 그 순서를 교정한다 |
two-hop-proxy-header-contract.md |
Q1의 우회 경로 제약, Q4의 헤더 덮어쓰기 근거 |
session-lab-operations.md |
실행 도구와 명령 |
four-pattern-tradeoff-matrix.md |
Q4가 되돌아가는 선택지(BFF)의 비교표 |