이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다. 사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다. 대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 — final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인 final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다. 삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다. 그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개, writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물, scripts/check-ssot-facts.py 와 그 시험이 들어 있다. 이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.0 KiB
id, kind, slug, title, topic, topicName, project, status, version, questionStatus, studio, public, sourceRevision, source
| id | kind | slug | title | topic | topicName | project | status | version | questionStatus | studio | public | sourceRevision | source | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 18a5cde2-dd1e-4bff-9f1c-997577ae438f | QUESTION | bff-session-authorized-client-store | BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가 | oauth-oidc-auth-boundary | OAuth/OIDC 인증 경계 | KeyCloak Patterns | 게시 중 | 32 | OPEN | https://hyeonworks.com/studio/documents/18a5cde2-dd1e-4bff-9f1c-997577ae438f/edit | https://hyeonworks.com/questions/bff-session-authorized-client-store | keycloak-patterns-lab@2026-08 |
|
BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
현재 BFF(Backend for Frontend)의 세션과 Authorized Client는 프로세스 메모리에 저장된다. 재시작과 레플리카 이동 뒤에도 로그인 상태를 유지하려면 두 상태를 어디에 저장할지 정해야 한다.
관계
- 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 이 질문에서 저장소 부분만 떼어 낸 것이다.
- Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 세션과 Authorized Client를 찾는 열쇠가 다르다는 사실의 출처다.
- BFF 인증 구조 설계 기준 이 기준의 저장소 항목은 여기에 답이 나와야 채울 수 있다.
- Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가 저장소를 공유한 다음에야 레플리카 경쟁을 재현할 수 있다.
사실
- BFF가 서버에 들고 있는 상태는 둘이다. 하나는 브라우저의 로그인 세션이고, 다른 하나는 Keycloak에서 받은 액세스 토큰과 리프레시 토큰을 담아 두는 Authorized Client다. JavaScript 응답에 OAuth 토큰이 보이지 않았을 때는 토큰을 다루는 일도 같이 사라진 것처럼 보였지만, BFF 코드를 따라가 보니 BFF가 세션에서 Authorized Client를 찾아 액세스 토큰을 붙여 내부 API를 대신 불렀다.
- 현재 구성에는 Spring Session도, Redis도, JDBC 저장소도, 토큰을 암호화해 담는 저장소도 없다.
- HttpSession은 서블릿 컨테이너의 메모리 구현을 쓰기 때문에, 그 프로세스가 종료되면 세션 데이터도 같이 사라진다.
- OAuth2AuthorizedClientService도 Spring Boot 자동구성이 고르는 메모리 구현이다.
- 세션은 session ID로 조회하지만 Authorized Client는 registration 이름과 principal name으로 조회한다. 두 저장 구조를 공유 저장소로 옮길 때는 각각 따로 설계해야 한다.
- Authorized Client 매니저에는 authorization-code와 refresh-token provider가 함께 구성돼 있어서, 저장소를 공유하면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있다.
- 여기까지는 한 대에서 실행한 학습 환경에서 코드와 테스트로 확인한 것이다. 여러 인스턴스가 세션과 Authorized Client를 공유하는지는 실행해 확인하지 못했다.
가정
- 두 상태를 같은 저장소에 둘 필요는 없다.
- 저장한 리프레시 토큰을 평문으로 두면 안 된다.
- 세션 만료와 토큰 만료 중 하나가 먼저 오면 그 순간에 무엇이 일어날지 정해 두어야 한다. 토큰이 만료됐을 때 사용자의 로그인 상태가 유지되는지가 그런 예다.
미지수
- Redis와 JDBC 중 어느 쪽이 이 상태의 접근 패턴에 맞는가. 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- 세션과 Authorized Client를 한 저장소에 둘지 나눌지.
- 암호화 키를 어디에 두고 어떻게 교체하는가. 교체하는 동안 이전 키로 저장한 값은 어떻게 읽는가.
- 세션 TTL과 리프레시 토큰 수명 중 어느 쪽을 기준으로 만료를 맞추는가.
- 로그아웃 한 번으로 세션과 Authorized Client 두 저장소를 어떻게 같이 지우는가.
- 스티키 세션이 영속 저장소의 대안인가 보완인가.
제약
- Authorized Client를 찾을 때는 session ID를 쓰지 않아서, 세션만 공유해도 같은 사용자의 여러 세션이 같은 토큰을 본다.
- 현재 테스트에는 저장소 계약이 없다.
- 모든 화면 요청이 BFF를 지나기 때문에 저장소가 느려지면 화면도 바로 느려진다. 다만 이 학습 환경에서는 처리량을 재지 못해서, 어느 지점부터 느려지는지는 숫자로 말할 수 없다.
선택지
1. 세션과 Authorized Client를 모두 Redis에 둔다
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 쓰면 여러 애플리케이션 인스턴스가 같은 세션과 Authorized Client를 조회할 수 있다. 상태가 특정 인스턴스의 메모리에 묶이지 않으니 서버가 재시작되거나 요청이 다른 레플리카로 전달되는 환경에서도 인증 상태를 이어 쓰기 쉬워진다.
세션과 Authorized Client에 설정한 유효 시간에 맞춰 Redis의 TTL로 저장한 상태를 만료시키는 구조도 짤 수 있다. 다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 세션 정책과 OAuth 토큰 수명에 맞춰 따로 정한다.
대신 인증 경로가 Redis의 가용성에 매인다. Redis에 장애가 났을 때 기존 세션과 Authorized Client를 읽지 못하는 상황을 어떻게 처리할지 정해야 하고, 장애 복구와 데이터 유지 방식도 같이 본다.
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 담은 토큰을 어떻게 보호할지도 정해야 한다. 암호화해서 저장할지, 암호화한다면 키를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
2. 세션과 Authorized Client를 모두 JDBC에 둔다
JDBC 기반 저장소를 쓰면 이미 운영 중인 관계형 DB에 세션과 Authorized Client를 담을 수 있다.
인증 과정에서 세션이나 Authorized Client를 조회할 때마다 DB 접근이 일어나므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 매이게 된다. 요청량이 늘었을 때 세션 조회가 지연되지 않는지, 인증 조회와 기존 애플리케이션 쿼리가 서로 영향을 주지 않는지 확인해야 한다.
만료된 세션과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기도 정한다.
3. 세션만 공유하고 스티키 세션을 쓴다
세션 어피니티를 쓰면 같은 사용자의 요청을 되도록 같은 애플리케이션 인스턴스로 보낼 수 있어서 기존 구조를 덜 건드린다.
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되지는 않는다. Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 있다면, 요청이 다른 인스턴스로 전달되거나 그 인스턴스가 종료됐을 때 기존 토큰 정보를 찾지 못한다.
세션과 Authorized Client를 공유하는 문제, 장애 뒤에 인증 상태를 잇는 문제는 따로 풀어야 한다. 인스턴스 장애와 레플리카 간 이동까지 보려면 세션과 Authorized Client의 저장 방식을 따로 설계한다.
4. 세션은 Redis, 토큰은 암호화해 JDBC에 둔다
세션과 Authorized Client를 서로 다른 저장소에 담는 방법도 있다. 요청마다 자주 조회되는 세션은 Redis처럼 빠르게 접근할 수 있는 저장소에 두고, 액세스 토큰과 리프레시 토큰은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 담는 식이다.
대신 두 저장소의 상태를 함께 관리해야 한다. 세션이 만료됐는데 Authorized Client는 아직 지워지지 않거나, 반대로 Authorized Client가 먼저 지워져서 유효한 세션인데도 토큰을 찾지 못하는 상황이 생길 수 있다. 그래서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
로그아웃에서도 세션과 Authorized Client가 서로 다른 저장소에 있으니 두 상태를 모두 지워야 하고, 한쪽을 지우다 실패했을 때 어떻게 할지도 같이 정한다.
Redis와 관계형 DB를 둘 다 인증 경로에서 쓰게 되므로 모니터링, 장애 대응, 백업까지 운영할 저장소가 늘어난다.
다음 검증
후보마다 같은 입력으로 비교한다.
- 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
- 저장소를 직접 열어 리프레시 토큰이 평문으로 보이는지 확인한다.
- 세션 TTL과 토큰 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
- 로그아웃 뒤 두 저장소에 잔여 항목이 없는지 확인한다.
- 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
토큰 보호와 만료 처리, 로그아웃 뒤 상태 정리까지 검증한 다음에 저장 구조를 정한다.
암호화 키 교체 절차는 후보를 고른 뒤에 따로 설계한다.