--- kind: REFERENCE slug: look-at-the-lookup-key-before-moving-the-store title: 저장소를 옮기기 전에 조회 키를 본다 topic: where-application-state-lives topicName: 세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기 project: keycloak-session-store status: 게시 전 sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af source: - final/document.md#선택이-코드와-흐름에-반영되는-방식-b0 - final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답 evidence: - ../../../final/evidence/raw/b0-bff-redis-deploy__03-beans-analysis.txt - ../../../final/evidence/raw/b1-redis-session-store__02-autoconfig-after.txt - ../../../final/evidence/raw/b1-redis-session-store__03-redis-contents.txt - ../../../final/evidence/raw/b2-multi-instance-session__02-schema.txt - ../../../final/evidence/raw/b2-multi-instance-session__04-overwrite-test.txt - ../../../final/evidence/raw/b2-multi-instance-session__05-logout-cleanup.txt --- # 저장소를 옮기기 전에 조회 키를 본다 서버가 들고 있던 상태를 외부 저장소로 옮기기 전에 무엇으로 조회하는지부터 읽는다. BFF 에서 세션은 세션 id 로, 인가된 클라이언트는 principal 이름으로 찾았고, 세션만 Redis 로 옮기자 토큰은 따라오지 않았다. ## 관계 - **세션과 인가된 클라이언트는 조회 키가 다르다** 이 기준이 선 근거. 두 저장소가 무엇을 담고 무엇으로 찾는지 설명한다. - **기본키에 세션 id 가 없어서 두 번째 로그인이 첫 토큰을 덮어썼다** 조회 키를 그대로 둔 채 저장소만 옮겼을 때 무엇이 남았는지 잰 기록이다. ## 목적 BFF(Backend For Frontend, 브라우저 앞에 두는 서버)는 로그인한 사용자가 누구인지와 그 사용자의 access token, refresh token 을 둘 다 서버에 들고 있다. 두 값은 한 요청에서 같이 쓰이지만 조회 키가 다르다. 세션은 세션 id 로 찾고, 인가된 클라이언트(OAuth2AuthorizedClient)는 principal 이름으로 찾는다. 이 기준은 저장소 한쪽만 밖으로 옮겨 놓고 나머지도 따라왔다고 여기는 실수를 막는다. 세션 저장소를 Redis 로 바꾸는 설정은 세션 id 로 찾는 것만 옮기고, principal 이름으로 찾는 인가된 클라이언트는 그 설정에 걸리지 않는다. 옮긴 뒤에 남은 문제도 저장소 종류가 아니라 조회 키에서 나왔다. 토큰을 PostgreSQL 로 옮긴 뒤에도 테이블의 기본키에 세션 id 가 없어서, 같은 사용자의 두 세션이 같은 행을 쓰고 나중 로그인이 앞의 토큰을 덮어썼다. ## 규칙 ### 1. 저장소를 붙이기 전에 자동구성이 무엇을 골랐는지 읽는다 설정하지 않은 값에도 구현체가 하나씩 들어가 있다. 저장소를 붙이기 전 BFF 를 들여다보니 인가된 클라이언트 서비스는 InMemoryOAuth2AuthorizedClientService 였고 저장소는 AuthenticatedPrincipalOAuth2AuthorizedClientRepository 였다. 세션 저장소는 고른 것이 없어 서블릿 컨테이너 메모리에서 돌고 있었고, Redis 도 Spring Session 도 구성되지 않은 상태였다. 두 번째 줄이 조회 키를 정한다. AuthenticatedPrincipalOAuth2AuthorizedClientRepository 는 principal 이름으로 찾기 때문에 조회 키에 세션 id 가 없다. ### 2. 상태마다 무엇으로 찾는지 적고, 키가 다르면 저장소도 따로 정한다 BFF 가 서버에 들고 있는 것은 둘이다. Application Session 은 누가 로그인했는지를 담고 세션 id 로 찾는다. OAuth2AuthorizedClient 는 access token 과 refresh token 을 담고 principal 이름으로 찾는다. 이름은 비슷해도 서로 다른 것을 저장하는 두 개가 따로 굴러간다. 키가 다르면 한쪽을 옮기는 설정이 다른 쪽에 닿지 않는다. 저장소를 하나 골라 둘을 함께 옮기는 대신 상태마다 따로 정한다. ### 3. 한쪽을 옮긴 뒤 나머지 빈이 그대로인지 다시 읽는다 SPRING_SESSION_STORE_TYPE=redis 로 세션을 Redis 로 옮긴 뒤 1번과 같은 방법으로 빈을 다시 읽었더니, 인가된 클라이언트를 다루는 서비스와 저장소와 매니저 셋 다 옮기기 전과 같았다. Redis 에 생긴 키는 하나였고 타입은 hash 였으며, 담긴 필드 일곱 개 어느 이름도 access token 이나 refresh token 을 가리키지 않았다. 설정을 넣은 것과 그 설정이 무엇을 옮겼는지는 따로 확인한다. ### 4. 조회 키가 그대로면 저장소를 옮겨도 키 충돌은 남는다 토큰을 JdbcOAuth2AuthorizedClientService 로 PostgreSQL 에 옮긴 뒤, 같은 사용자가 다른 브라우저로 로그인하자 두 번째 로그인이 첫 토큰을 덮어썼다. 만들어진 테이블의 기본키는 client_registration_id 와 principal_name 두 컬럼이고 세션 id 가 없어서, 같은 사용자의 두 세션이 같은 행을 쓴다. 기본키에 세션 id 를 더하면 덮어쓰기가 사라지는지는 이 실험대에서 재지 않았다. ### 5. 저장소가 둘이면 정리 경로도 둘인지 확인한다 로그아웃한 뒤 두 저장소를 열어 보니 Redis 세션은 0 키로 정리됐고 PostgreSQL 토큰은 1 행이 남았다. 남은 행에는 평문 refresh token 이 그대로 있었다. 세어 보니 지울 것이 둘이 아니라 셋이었다. Keycloak 쪽 SSO 세션이 2 로 남아 있었다. HttpSession 은 Spring Security 가 지우고, 인가된 클라이언트는 아무도 안 지우고, IdP 세션은 RP-initiated logout 을 보내야 끊긴다. 그래서 로그아웃한 뒤 같은 주소를 다시 열면 로그인 화면 없이 그냥 들어가진다. ### 6. 이름을 본 것과 값을 연 것을 나눠 적는다 Redis 해시에서 값까지 꺼내 본 필드는 sessionAttr 로 시작하는 SPRING_SECURITY_CONTEXT 하나이고, 그 값은 \xac\xed 두 바이트로 시작한다 — Java 기본 직렬화의 매직 넘버다. 이름에 OAuth2 가 들어간 …AUTHORIZATION_REQUEST 는 값 안을 열어 보지 않았다. 그래서 확인한 범위는 일곱 필드의 이름까지다. 토큰이 저장되지 않았다고 적으려면 값을 열지 않은 필드를 한 번 더 봐야 한다. ## 적용 조건 - 인스턴스가 둘 이상일 때 - 한 요청이 서버에 든 상태를 둘 이상 쓸 때. BFF 에서는 세션과 인가된 클라이언트가 그렇다 - 메모리에서만 돌던 상태를 Redis 나 PostgreSQL 같은 외부 저장소로 옮기려 할 때 - 자동구성이 고른 구현체를 그대로 쓰고 있을 때 ## 예외 - 확인한 짝은 세션(세션 id)과 인가된 클라이언트(principal 이름) 하나뿐이다. 다른 상태 짝에서도 조회 키가 이렇게 갈리는지는 재지 않았다. 이 기준은 다른 짝에서 무엇이 나올지 미리 말하지 않고, 옮기기 전에 같은 확인을 한 번 하라고만 한다 - 상태가 하나뿐이고 조회 키도 하나면 이 확인이 필요 없다. 다만 자동구성이 무엇을 골랐는지는 그때도 읽는다 - 단일 인스턴스로만 운영하면 이 기준이 막으려는 문제가 생기지 않는다 - 기본키를 고쳐서 덮어쓰기를 막는 방법은 이 기준에 없다. 원인을 확인한 데까지이고 고친 뒤를 재지 않았다 ## 예시 - 저장소를 붙이기 전 BFF 에는 세션 저장소로 고른 것이 없었고 서블릿 컨테이너 메모리에서 돌고 있었다 - 세션을 Redis 로 옮긴 뒤에도 인가된 클라이언트를 다루는 빈 셋은 옮기기 전과 같았다 - Redis 해시의 필드 일곱 개 어느 이름도 access token 이나 refresh token 을 가리키지 않았다 - 토큰 테이블의 기본키가 client_registration_id 와 principal_name 이라 같은 사용자의 두 번째 로그인이 첫 토큰을 덮어썼다 - 로그아웃한 뒤 Redis 세션은 0 키였고 PostgreSQL 토큰은 1 행이 남았다