docs(keycloak): adopt the decomposition contract, fix the redirect URI, strip evaluative prose

- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중
  17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로
  tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8
- 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html
  (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다
- 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference)
- 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다.
  삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 12:39:20 +09:00
co-authored by Claude Fable 5.1
parent 62520a4dce
commit 4d50bb939a
26 changed files with 1947 additions and 1225 deletions
@@ -11,124 +11,130 @@ version: 32
questionStatus: OPEN
studio: "https://hyeonworks.com/studio/documents/18a5cde2-dd1e-4bff-9f1c-997577ae438f/edit"
public: "https://hyeonworks.com/questions/bff-session-authorized-client-store"
sourceRevision: keycloak-patterns-lab@2026-08
source:
- final/document.md#검토한-선택지와-막힌-지점-ap3
- final/document.md#문제를-어렵게-만든-제약-학습-환경
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3
---
# BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
Application Session과 Authorized Client는 저장하고 조회하는 기준이 서로 다르다.
Session은 `session ID`를 기준으로 조회하지만, Authorized Client는 `client registration 이름``principal name`을 기준으로 조회한다.
그래서 두 상태를 반드시 같은 저장소에 보관해야 하는 것은 아니며, 각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 결정해야 한다.
현재 BFF의 세션과 Authorized Client는 프로세스 메모리에 저장된다. 재시작과 레플리카 이동 뒤에도 로그인 상태를 유지하려면 두 상태를 어디에 저장할지 정해야 한다.
현재 Shared Store의 후보로는 Redis를 우선 생각하고 있지만 아직 최종 저장소로 결정한 것은 아니다.
특히 Access Token과 Refresh Token을 Redis에 저장할 경우 Token을 어떤 방식으로 암호화할지, Session과 Token의 만료 시간을 어떻게 맞출지, Logout할 때 Session과 Authorized Client가 모두 정상적으로 제거되는지까지는 확인하지 않았다.
BFF(Backend for Frontend)가 서버에 들고 있는 상태는 둘이다. 하나는 브라우저의 로그인 세션이고,
다른 하나는 Keycloak에서 받은 액세스 토큰과 리프레시 토큰을 담아 두는 Authorized Client다.
세션은 `session ID`로 찾지만 Authorized Client는 `client registration 이름``principal name`으로 찾는다.
따라서 현재 단계에서는 Redis를 저장소 후보로 작성하고, Token 보호와 만료 처리, Logout 시 상태 정리까지 검증한 뒤 실제 저장 구조를 결정한다.
찾는 열쇠가 이렇게 다르니 두 상태를 반드시 한 저장소에 담아야 하는 것은 아니다.
각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 따로 정할 수 있다.
공유 저장소 후보로 Redis를 우선 보고 있지만 아직 고르지 않았다.
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 토큰을 어떤 방식으로 암호화할지, 세션과 토큰의 만료 시간을 어떻게 맞출지,
로그아웃할 때 세션과 Authorized Client가 모두 지워지는지를 확인해야 하는데 아직 확인하지 않았다.
토큰 보호와 만료 처리, 로그아웃 뒤 상태 정리까지 검증한 다음에 저장 구조를 정한다.
## 관계
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
이 질문에서 저장소 부분만 떼어 낸 것이다.
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
sessionauthorized client 열쇠가 다르다는 사실의 출처다.
세션Authorized Client를 찾는 열쇠가 다르다는 사실의 출처다.
- **BFF 인증 구조 설계 기준**
이 기준의 저장소 항목이 이 질문의 답을 기다린다.
이 기준의 저장소 항목은 여기에 답이 나와야 채울 수 있다.
- **Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가**
저장소를 공유한 에야 replica 경쟁 재현다.
저장소를 공유한 다음에야 레플리카 경쟁 재현할 수 있다.
## 사실
- 현재 구성에 Spring Session Redis, JDBC repository, 암호화 token store가 없다.
- 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이다.
- session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다.
두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
- 현재 구성에 Spring Session도, Redis, JDBC 저장소도, 토큰을 암호화해 담는 저장소도 없다.
- HttpSession은 서블릿 컨테이너의 메모리 구현을 쓰기 때문에, 그 프로세스가 종료되면 세션 데이터도 같이 사라진다.
- OAuth2AuthorizedClientService도 Spring Boot 자동구성이 고르는 메모리 구현이다.
- 세션은 session ID로 조회하고 Authorized Client는 registration 이름과 principal name으로 조회한다.
두 저장 구조를 공유 저장소로 옮길 각각 따로 설계해야 한다.
- Authorized Client 매니저에는 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
저장소를 공유하면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있다.
## 가정
- 두 상태를 같은 저장소에 둘 필요는 없다.
- 저장된 refresh token을 평문으로 두면 안 된다.
- session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
예를 들면 token이 만료됐을 때 사용자의 로그인 상태가 여전히 유지되는지.
- 저장한 리프레시 토큰을 평문으로 두면 안 된다.
- 세션 만료와 토큰 만료 중 하나가 먼저 오면 그 순간에 무엇이 일어날지 정해 두어야 한다.
토큰이 만료됐을 때 사용자의 로그인 상태가 유지되는지가 그런 예다.
## 미지수
- Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가.
- Redis와 JDBC 중 어느 쪽이 이 상태의 접근 패턴에 맞는가.
요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- sessionauthorized client를 같은 store에 둘지 나눌지.
- 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장 값은 어떻게 읽는가.
- session TTL과 refresh token 수명 중 어느 을 기준으로 만료를 맞추게 되는가.
- session과 Authorized Client 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- sticky session이 durable store의 대안이 되는가 보완이 되는가.
- 세션Authorized Client를 한 저장소에 둘지 나눌지.
- 암호화 를 어디에 두고 어떻게 교체하는가. 교체하는 동안 이전 로 저장 값은 어떻게 읽는가.
- 세션 TTL과 리프레시 토큰 수명 중 어느 을 기준으로 만료를 맞추는가.
- 로그아웃 한 번으로 세션과 Authorized Client 두 저장소를 어떻게 같이 지우는가.
- 스티키 세션이 영속 저장소의 대안인가 보완인가.
## 제약
- authorized client의 조회 시 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token을 보게 된다.
- 현재 테스트에는 저장소 관련 계약이 없다.
- 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
- Authorized Client를 찾을 때는 session ID를 쓰지 않아서, 세션만 공유해도 같은 사용자의 여러 세션이 같은 토큰을 본다.
- 현재 테스트에는 저장소 계약이 없다.
- 모든 화면 요청이 BFF를 지나기 때문에 저장소가 느려지면 화면도 바로 느려진다.
## 선택지
### 1. sessionauthorized client를 모두 Redis에 둔다
### 1. 세션Authorized Client를 모두 Redis에 둔다
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 사용하면 여러 애플리케이션 인스턴스가 같은 Session과 Authorized Client 정보를 조회할 수 있다.
상태가 특정 인스턴스의 메모리에 묶이지 않으므로 서버가 재시작되거나 요청이 다른 Replica로 전달되는 환경에서도 인증 상태를 공유하기 쉬워진다.
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 면 여러 애플리케이션 인스턴스가 같은 세션과 Authorized Client를 조회할 수 있다.
상태가 특정 인스턴스의 메모리에 묶이지 않으 서버가 재시작되거나 요청이 다른 레플리카로 전달되는 환경에서도 인증 상태를 이어 쓰기 쉬워진다.
Session과 Authorized Client에 설정 유효 시간에 따라 Redis의 TTL을 이용해 저장 상태를 만료시키는 구조도 구성할 수 있다.
다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 Session 정책과 OAuth Token의 수명에 맞춰 별도로 정해야 한다.
세션과 Authorized Client에 설정 유효 시간에 맞춰 Redis의 TTL 저장 상태를 만료시키는 구조도 수 있다.
다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 세션 정책과 OAuth 토큰 수명에 맞춰 로 정한다.
이 구성에서는 인증 경로가 Redis의 가용성에 의존하게 된다.
Redis에 장애가 발생했을 때 기존 Session과 Authorized Client를 조회하지 못하는 상황을 어떻게 처리할지 정해야 하,
장애 복구와 데이터 유지 방식도 함께 고려해야 한다.
대신 인증 경로가 Redis의 가용성에 매인다.
Redis에 장애가 을 때 기존 세션과 Authorized Client를 지 못하는 상황을 어떻게 처리할지 정해야 하, 장애 복구와 데이터 유지 방식도 같이 본다.
또한 Access Token과 Refresh Token을 Redis에 저장한다면 저장된 Token을 어떤 방식으로 보호할지도 정해야 한다.
Token을 암호화해서 저장할지, 암호화한다면 Key를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 담은 토큰을 어떻게 보호할지도 정해야 한다.
암호화해서 저장할지, 암호화한다면 를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
### 2. sessionauthorized client를 모두 JDBC에 둔다
### 2. 세션Authorized Client를 모두 JDBC에 둔다
JDBC 기반 저장소를 사용하면 이미 운영 중인 관계형 DB에 Session과 Authorized Client 정보를 저장할 수 있다.
JDBC 기반 저장소를 면 이미 운영 중인 관계형 DB에 세션과 Authorized Client를 담을 수 있다.
인증 과정에서 Session이나 Authorized Client를 조회할 때마다 DB 접근이 발생하므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 영향을 받게 된다.
요청량이 증가했을 때 Session 조회가 지연되지 않는지 확인하고, 인증 관련 조회 기존 애플리케이션 쿼리 서로 영향을 주지 않는지 확인해야 한다.
인증 과정에서 세션이나 Authorized Client를 조회할 때마다 DB 접근이 일어나므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 매이게 된다.
요청량이 늘었을 때 세션 조회가 지연되지 않는지, 인증 조회 기존 애플리케이션 쿼리 서로 영향을 주지 않는지 확인해야 한다.
또한 만료된 Session과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기해야 한다.
JDBC를 고르면 기존 DB 운영 체계를 그대로 쓰면서 조회 지연과 DB 부하, 만료 데이터 정리까지 같이 관리하게 된다.
만료된 세션과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기 정한다.
### 3. session만 공유하고 sticky session을 쓴다
### 3. 세션만 공유하고 스티키 세션을 쓴다
Session Affinity를 사용하면 같은 사용자의 요청을 가능한 한 동일한 애플리케이션 인스턴스로 전달할 수 있으므로 기존 구조의 변경을 줄일 수 있다.
세션 어피니티를 쓰면 같은 사용자의 요청을 되도록 같은 애플리케이션 인스턴스로 보낼 수 있어서 기존 구조를 덜 건드린다.
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되는 것은 아니다.
Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 저장되어 있다면, 요청이 다른 인스턴스로 전달되거나 해당 인스턴스가 종료되었을 때 기존 Token 정보를 조회할 수 없다.
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되않는다.
Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 있다면, 요청이 다른 인스턴스로 전달되거나 인스턴스가 종료을 때 기존 토큰 정보를 찾지 못한다.
Session Affinity는 요청을 특정 인스턴스로 보내는 방법이다. Session과 Authorized Client를 공유하는 문제 장애 이후에 인증 상태를 유지하는 문제는 그대로 남는다.
인스턴스 장애와 Replica 간 이동까지 고려한다면 Session과 Authorized Client의 저장 방식을 별도로 설계해야 한다.
세션과 Authorized Client를 공유하는 문제, 장애 에 인증 상태를 는 문제는 따로 풀어야 한다.
인스턴스 장애와 레플리카 간 이동까지 보려면 세션과 Authorized Client의 저장 방식을 로 설계한다.
### 4. session은 Redis, token은 암호화 JDBC에 둔다
### 4. 세션은 Redis, 토큰은 암호화 JDBC에 둔다
Session과 Authorized Client를 서로 다른 저장소에 보관하는 방법도 있다.
요청마다 자주 조회되는 Session은 Redis와 같이 빠르게 접근할 수 있는 저장소에 두고, Access Token과 Refresh Token은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 저장할 수 있다.
각 데이터의 접근 패턴과 보호 요구사항에 맞춰 저장소를 선택할 수 있다는 장점이 있다.
세션과 Authorized Client를 서로 다른 저장소에 는 방법도 있다.
요청마다 자주 조회되는 세션은 Redis처럼 빠르게 접근할 수 있는 저장소에 두고, 액세스 토큰과 리프레시 토큰은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 담는 식이다.
두 저장소의 상태 함께 관리해야 한다.
Session이 만료되었는데 Authorized Client가 남거나, 반대로 Authorized Client가 먼저 제거되어 유효한 Session에서 Token을 찾지 못하는 상황이 발생할 수 있다.
따라서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
대신 두 저장소의 상태 함께 관리해야 한다.
세션이 만료는데 Authorized Client는 아직 지워지지 않거나, 반대로 Authorized Client가 먼저 지워져서 유효한 세션인데도 토큰을 찾지 못하는 상황이 생길 수 있다.
그래서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
Logout에서도 Session과 Authorized Client가 서로 다른 저장소에 있으므로 두 상태를 모두 정리해야 한다.
한쪽을 제거하는 과정에서 실패했을 때 어떻게 처리할지도 함께 결정해야 한다.
로그아웃에서도 세션과 Authorized Client가 서로 다른 저장소에 있으 두 상태를 모두 지워야 하고, 한쪽을 지우다 실패했을 때 어떻게 할지도 같이 정한다.
또한 Redis와 관계형 DB를 모두 인증 경로에서 사용하게 되므로 모니터링, 장애 대응, 백업 운영해야 하는 저장소 늘어난다.
저장소를 나눠서 얻는 것과 늘어나는 운영 대상을 같이 놓고 정한다.
Redis와 관계형 DB를 둘 다 인증 경로에서 게 되므로 모니터링, 장애 대응, 백업까지 운영 저장소 늘어난다.
## 다음 검증
후보마다 같은 입력으로 비교한다.
1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
2. 저장소를 직접 열어 refresh token이 평문으로 는지 확인한다.
3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다.
2. 저장소를 직접 열어 리프레시 토큰이 평문으로 보이는지 확인한다.
3. 세션 TTL과 토큰 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
4. 로그아웃 뒤 두 저장소에 잔여 항목이 없는지 확인한다.
5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
암호화 교체 절차는 후보를 고른 뒤에 따로 설계한다.