--- id: 1e5d05fa-88f4-4ef5-a402-4a520ae4a52d kind: SETUP slug: reproduce-b1-redis-session-store title: Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다 topic: where-application-state-lives topicName: 세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기 project: keycloak-session-store status: 게시 전 studio: "https://hyeonworks.com/studio/documents/1e5d05fa-88f4-4ef5-a402-4a520ae4a52d/edit" pinnedVersions: - name: keycloak-pattern-bff version: lab - name: Redis version: 7.4.x source: - final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-1 sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af --- # Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다 Redis 를 세션 저장소로 붙이고 B-0 에서 찍어 둔 빈 목록과 견주는 절차다. 의존성 둘을 함께 넣어 다시 빌드하고 두 노드에 밀어 넣은 뒤, 무엇이 옮겨졌는지와 무엇이 안 옮겨졌는지를 같은 명령으로 확인한다. 전 구간 약 40분이고 빌드 시간이 들어 있다. ## 관계 - **세션만 Redis 로 옮기자 토큰이 따라오지 않았다** 이 절차가 낸 결과를 담은 기록이다. 무엇을 발견했는지는 그쪽에 있고 여기에는 치는 순서만 있다. - **주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다** 의존성을 하나만 넣으면 오류 없이 메모리에 남는다. 그 아홉 건 가운데 하나가 이 편에서 나왔다. - **세션과 인가된 클라이언트는 조회 키가 다르다** 빈 81개가 늘었는데 인가된 클라이언트 빈 셋이 그대로인 까닭을 그 기록이 설명한다. - **아무 저장소도 주지 않고 Spring 이 무엇을 고르는지 찍어서 확인한다** 먼저 해 둬야 하는 편이다. 그 편이 남긴 빈 세 개의 이름과 숫자 `321` 이 여기서 대조군이 된다. - **토큰을 PostgreSQL 로 옮기고 기본키와 로그아웃 정리를 확인한다** 다음 편이다. 여기서 Redis 로 안 옮겨진 인가된 클라이언트를 그 편이 공유 저장소로 옮긴다. ## 본문 ## 읽기 전에 — 어디서 치는가 B-0 과 같다. 소스를 고치고 이미지를 만드는 일은 워크스테이션에서 하고, 클러스터를 보고 배포하는 일은 `[lab host]` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 브라우저 창도 하나 열어 둔다. **원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다. **끊기는 곳도 B-0 과 같다. 시작 전에 셋을 스스로 정해 둔다 — 그 명령이 원 가이드에 없다(unknown).** 첫째, 워크스테이션과 `kc-lab-1` 을 여섯 번 오가는데 건너가는 `ssh` 도 `exit` 도 이 절차에 없다. 경로는 이미지를 밀어 넣는 줄 하나에만 드러난다 — `ssh test-server "ssh kc-lab-1 '...'"`. 둘째, `bff/pom.xml` 과 `deploy/lab/k8s/bff-redis.yaml` 이 전부 저장소 상대경로다. 두 기계 각각에서 저장소 루트로 옮겨 두고 시작한다. 셋째, 워크스테이션에서 고친 `deploy/lab/k8s/bff-redis.yaml` 을 `kc-lab-1` 로 넘기는 단계가 없다. 이 편에서는 그 누락이 B-0 보다 더 헷갈리게 나온다 — 주입 3번이 넣는 `enableServiceLinks: false` 가 `kc-lab-1` 쪽 파일에 없으면 `apply` 는 성공하는데 파드가 똑같이 `CrashLoopBackOff` 로 남고, 그때 이 편은 「파드가 아직 안 바뀌었다」를 먼저 의심하라고 적는다. 실제로는 고친 파일이 그 기계에 안 간 것이다. **아래 코드블록의 라벨은 그 줄을 치는 기계를 가리킨다.** `[워크스테이션 → kc-lab-1]` 이 붙은 블록은 한 블록 안에서 기계가 바뀌므로 어느 줄이 어디인지 블록 뒤에 적어 두었다. | 무엇 | 값 | |---|---| | 네임스페이스 | `keycloak-lab` | | 고치는 파일 | 넷 — `bff/pom.xml` · `application.yml` · `BffControllerTest.java` · `bff-redis.yaml` | | 주입 수단 | 의존성 둘을 넣고 다시 빌드해 두 노드에 import | | 무엇을 찍나 | `/actuator/beans` 의 빈 수와 이름, Redis 의 키·필드·TTL | | 브라우저 | 필요하다. 인가 코드 흐름은 왕복이 두 번이라 `curl` 로 대신할 수 없다 | | 도구 | `jq` 가 이 실험대에 없다. `grep` 과 `redis-cli` 로 읽는다 | | 걸리는 시간 | 약 40분. 빌드 시간이 들어 있다 | ## 이 실험이 가르는 것 B-0 이 답을 냈다. 세션도 토큰도 인스턴스 메모리에 있고, 그래서 replica 2 에서는 로그인조차 안 된다. 처방은 뻔해 보인다 — 공유 저장소를 붙인다. ```text Redis 를 붙인다 → 상태가 공유된다 → 다중 인스턴스가 된다 ↑ 정말 그런가? ``` 이 실험이 재는 것은 붙였다와 공유된다 사이의 거리다. 묻는 것이 넷이고 그중 둘째를 이 실험이 판정한다. | | 물어볼 것 | |---|---| | 무엇이 옮겨졌나 | `/actuator/beans` 를 다시 찍는다 | | 무엇이 안 옮겨졌나 | 같은 곳. 안 바뀐 것을 확인하는 쪽이 더 중요하다 | | 옮겨진 것 안에 무엇이 들었나 | Redis 를 직접 연다 | | 사용자에게는 어떻게 보이나 | 브라우저 | 절차를 끝까지 밟으면 일곱을 자기 화면에서 보게 된다 — 쿠버네티스가 넣지도 않은 환경변수로 파드를 죽이는 것, 그것이 `enableServiceLinks: false` 로 고쳐지는 것, 빈이 321 에서 402 로 81개 늘어나는 것, 그런데 인가된 클라이언트는 하나도 안 바뀐 것, Redis 안의 키와 필드와 TTL 에 토큰이 없는 것, 세션이 Java 네이티브 직렬화인 것, 로그인은 되어 있는데 아무것도 못 하는 상태. ## 전제와 되돌리기 - B-0 이 끝나 있고, B-0 의 답인 빈 세 개의 이름을 손에 들고 시작한다. 이 실험은 그 값들이 어떻게 바뀌는지를 잰다. - B-0 의 복구 절 §3 「전부 지운다」를 치지 않은 상태여야 한다. 그 절은 `bff` 와 `redis` Deployment 와 realm `keycloak-patterns` 를 함께 없애므로, 쳤으면 아래 첫 확인부터 빈 목록이 나온다. 그 상태라면 B-0 의 주입 절과 realm·사용자 단계를 다시 밟아 배포를 세운 뒤 여기로 온다. - 브라우저가 필요하다. - 명령은 `kc-lab-1` 에서 친다. - `jq` 는 이 실험대에 깔려 있지 않다. 이 절차는 `grep` 과 `redis-cli` 로 읽는다. 애플리케이션 구성을 바꾸는 실험이라 의존성과 설정을 고쳐 다시 빌드하고 다시 배포한다. 되돌리려면 소스를 되돌린 뒤 또 한 번 빌드해 두 노드에 다시 밀어 넣어야 하므로 `git status` 가 깨끗한 상태에서 시작한다. 되돌리기는 먼저 읽어 둔다. ```bash label="[워크스테이션] 소스를 되돌린다" git checkout -- bff/pom.xml bff/src/main/resources/application.yml \ deploy/lab/k8s/bff-redis.yaml ``` 주입 절의 첫 배포는 일부러 고장 난 상태로 한다. 함정을 직접 보기 위해서이고, 건너뛰고 `enableServiceLinks: false` 부터 시작해도 결과는 같다고 원 가이드가 적는다. ## 주입 전에 같은 명령으로 먼저 본다 넓은 것부터 좁혀 간다. ```text BFF 가 돌고 있나 → B-0 의 답 세 개 → Redis 가 비어 있나 → ★ 파드 안 환경변수 ``` ### 1. BFF 두 개와 Redis 가 떠 있는가 **무엇을 보는가** — 파드 배치. ```bash label="[lab host] BFF 와 Redis 의 배치를 본다" kubectl -n keycloak-lab get pods -o wide -l app=bff kubectl -n keycloak-lab get pods -o wide -l app=redis ``` **어디를 보나** — 모양은 이렇고 주소와 해시는 환경마다 다르다(observed). ```text bff-574c6d658b-8cz4x 1/1 Running 0 20m 10.42.0.51 kc-lab-1 bff-574c6d658b-zpkbp 1/1 Running 0 20m 10.42.1.52 kc-lab-2 redis-568bd7c4-5c5vc 1/1 Running 0 20m 10.42.1.53 kc-lab-2 ``` **이 값이 뜻하는 것** — BFF 두 개가 서로 다른 노드에 있고 Redis 가 떠 있다. Redis 는 배포만 되어 있고 아직 연결되지 않았다. B-0 이 그렇게 만들어 뒀다. ### 2. B-0 의 답을 before 값으로 다시 잡는다 **무엇을 보는가** — 주입 뒤에 견줄 빈 수와 빈 이름 셋. ```bash label="[lab host] ① 빈 목록을 파드 안에서 받는다" BFF=$(kubectl -n keycloak-lab get pod -l app=bff \ --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') kubectl -n keycloak-lab exec "$BFF" -- \ wget -qO- http://localhost:8083/actuator/beans > /tmp/beans-before.json wc -c /tmp/beans-before.json ``` 이 실험대는 `jq` 가 없어 `grep` 으로 덩어리를 뽑았다. 원 가이드가 아래 두 줄을 미검증으로 표시했다(unknown). ```bash label="[lab host] ② 빈을 세고 저장소 계열 이름을 뽑는다" grep -o '"aliases":\[' /tmp/beans-before.json | wc -l grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-before.json \ | sed 's/{"aliases".*"type":"/ -> /' \ | grep -iE 'authorizedclient|sessionRepository' ``` `jq` 가 깔려 있으면 그것을 쓴다고 원 가이드가 적는데 어떤 표현을 쓰라고는 여기서도 적지 않았다(unknown). **어디를 보나** — 실측은 이렇다(observed, `02-autoconfig-after.txt`). ```text 빈 수: 321 → 402 (+81) ``` ```text authorizedClientService before: InMemoryOAuth2AuthorizedClientService authorizedClientRepository before: AuthenticatedPrincipalOAuth2AuthorizedClientRepository authorizedClientManager before: AuthorizedClientServiceOAuth2AuthorizedClientManager ``` **이 값이 뜻하는 것** — 지금 빈 수가 `321` 이고 `sessionRepository` 는 아예 없다. 이 세 줄과 숫자를 적어 둔다. 관찰 절이 이 값과 견주고, 견줄 것이 없으면 안 바뀌었다고 말할 수 없다. ### 3. Redis 가 살아 있고 비어 있는가 **무엇을 보는가** — 연결 여부와 키 개수. ```bash label="[lab host] ① 응답과 판 번호를 읽는 형태로 본다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping kubectl -n keycloak-lab exec deploy/redis -- redis-cli info server | head ``` ```bash label="[lab host] ② 키 개수와 키 이름을 본다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan ``` **어디를 보나** — 모양은 이렇다(observed). ```text PONG # Server redis_version:7.4.x ... ``` ```text (integer) 0 ``` **이 값이 뜻하는 것** — `0` 이어야 뒤에서 찾은 키를 내가 만든 것이라고 말할 수 있다. `KEYS *` 대신 `--scan` 을 쓰는 까닭은 `KEYS` 가 서버를 블로킹하기 때문이다. 지금은 키가 0개라 차이가 없지만, 습관이 되면 운영에서 사고가 난다. ### 4. 파드 안에 이미 Redis 관련 환경변수가 있는가 **무엇을 보는가** — 아직 아무것도 안 바꿨는데 들어와 있는 값. 이 실험의 함정이 여기서 시작된다. 한 번은 통째로 보고 그다음 걸러 본다. ```bash label="[lab host] ① 환경변수를 통째로 본다" kubectl -n keycloak-lab exec "$BFF" -- printenv | sort ``` ```bash label="[lab host] ② Redis 쪽만 거른다" kubectl -n keycloak-lab exec "$BFF" -- printenv | grep -i redis ``` **어디를 보나** — 모양은 이렇다(observed). ```text REDIS_SERVICE_HOST=10.43.57.116 REDIS_SERVICE_PORT=6379 REDIS_PORT=tcp://10.43.57.116:6379 REDIS_PORT_6379_TCP=tcp://10.43.57.116:6379 REDIS_PORT_6379_TCP_ADDR=10.43.57.116 REDIS_PORT_6379_TCP_PORT=6379 REDIS_PORT_6379_TCP_PROTO=tcp ``` **이 값이 뜻하는 것** — `REDIS_PORT` 의 값이 포트 번호가 아니라 URL 이다. 쿠버네티스는 같은 네임스페이스의 모든 Service 마다 옛 Docker 링크 호환용 환경변수를 파드에 자동으로 넣고, 그 기능이 기본으로 켜져 있다. Service 이름이 `redis` 이므로 `REDIS_*` 가 들어오는데 애플리케이션 설정도 `${REDIS_PORT:6379}` 를 읽는다. 이름이 겹친다. ```text Service 이름이 redis 이면 REDIS_SERVICE_HOST=10.43.57.116 REDIS_SERVICE_PORT=6379 REDIS_PORT=tcp://10.43.57.116:6379 ← 이게 문제 ``` 매니페스트에 `REDIS_PORT: "6379"` 를 명시하면 그쪽이 이긴다. 명시를 안 하면 자동 주입이 이기고, 오류 메시지는 쓰지도 않은 값을 지목한다. `REDIS` 와 `POSTGRES` 와 `MYSQL` 처럼 흔한 Service 이름일수록 위험하다고 원 가이드가 적는다. 지금은 애플리케이션이 그 변수를 안 읽으므로 아무 일도 안 일어나고, 읽기 시작하는 순간 파드가 죽는다. ## 주입 ### 1. 의존성 둘을 함께 넣는다 **목적** — `SessionRepository` 를 Redis 로 갈아끼우고 연결을 제공한다. 하나만 넣으면 오류 없이 메모리에 남으므로 둘을 같이 넣는다. 무엇을 왜 넣는지 읽으면서 고쳐야 하는 파일이라 편집기로 연다. ```bash label="[워크스테이션] ① 빌드 파일을 연다" vim bff/pom.xml ``` ```xml org.springframework.session spring-session-data-redis org.springframework.boot spring-boot-starter-data-redis ``` 설정에 Redis 연결과 세션 저장소를 적는다. ```bash label="[워크스테이션] ② 애플리케이션 설정을 연다" vim bff/src/main/resources/application.yml ``` ```yaml spring: data: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} session: store-type: ${SPRING_SESSION_STORE_TYPE:redis} timeout: ${SPRING_SESSION_TIMEOUT:30m} redis: namespace: bff:session ``` 테스트에는 Redis 가 없으므로 테스트에서만 저장소를 끈다. ```bash label="[워크스테이션] ③ 테스트를 연다" vim bff/src/test/java/com/example/keycloakpattern/bff/BffControllerTest.java ``` ```java @SpringBootTest(properties = { "KEYCLOAK_CLIENT_SECRET=test-only-secret", // 테스트는 Redis 를 띄우지 않는다 "spring.session.store-type=none", }) ``` **예상 결과** — 세 파일이 `git diff --stat` 에 나온다. **왜 필요한가** — `spring-session-data-redis` 를 넣으면 컨텍스트가 뜰 때 Redis 에 붙으려 하고, 테스트에는 Redis 가 없다. `spring.session.store-type=none` 한 줄이 없으면 빌드가 테스트 단계에서 죽는데 그 실패 메시지가 Redis 연결 오류라 배포 환경 문제로 읽히기 쉽다. 실패한 곳은 빌드다. **문제가 생기면** — 뒤에서 `sessionRepository` 가 안 생기면 의존성을 하나만 넣은 것이므로 `pom.xml` 에 둘 다 있는지부터 본다. ### 2. 매니페스트를 일부러 고장 난 채로 올린다 **목적** — 자동 주입된 `REDIS_PORT` 가 애플리케이션 설정을 어떻게 이기는지 한 번 본다. ```bash label="[워크스테이션] ① 배포 매니페스트를 연다" vim deploy/lab/k8s/bff-redis.yaml ``` ```yaml spec: # enableServiceLinks: false ← 아직 넣지 않는다 containers: - name: bff env: - name: SPRING_SESSION_STORE_TYPE value: redis - name: REDIS_HOST value: redis.keycloak-lab.svc # REDIS_PORT 를 일부러 안 준다 — 자동 주입이 어떻게 이기는지 본다 ``` 빌드해서 두 노드에 밀어 넣고 배포한다. 아래 네 줄이 이 실험대가 실제로 친 형태다(observed). ```bash label="[워크스테이션 → kc-lab-1] ② 빌드하고 두 노드에 넣고 배포한다" docker build --progress=plain -t keycloak-pattern-bff:lab bff/ > /tmp/build.log 2>&1 echo "exit=$?" docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-1 'sudo k3s ctr images import -'" docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-2 'sudo k3s ctr images import -'" kubectl apply -f deploy/lab/k8s/bff-redis.yaml kubectl -n keycloak-lab rollout restart deployment/bff ``` **이 블록은 한 블록인데 기계가 둘이다.** 앞의 네 줄(`docker build` · `echo` · `docker save` 두 줄)은 워크스테이션에서 치고, `kubectl` 로 시작하는 아래 두 줄은 `kc-lab-1` 에서 친다. 워크스테이션에는 kubeconfig 가 없어 거기서 `kubectl` 을 치면 클러스터에 못 붙고 끝난다. 그리고 `kubectl apply` 가 읽는 `deploy/lab/k8s/bff-redis.yaml` 은 방금 워크스테이션에서 고친 그 파일이므로, `kc-lab-1` 쪽 체크아웃에도 같은 내용이 있어야 한다. 옮기는 명령은 원 가이드에 없다(unknown). 가운데 두 줄은 한 줄에 `ssh` 가 두 겹이고 원격 셸의 인용이 겹쳐 있어 나눠 치고 싶어지는데, 원 가이드가 나눈 형태를 적어 두지 않아 여기에도 없다(unknown). 지금 멈추려면 롤아웃을 되돌린다. ```bash label="[lab host] 중간에 그만둘 때 치는 한 줄" kubectl -n keycloak-lab rollout undo deployment/bff ``` **예상 결과** — 파드가 뜨지 않는다. 넓은 것부터 본다. ```bash label="[lab host] ③ 파드 상태를 본다" kubectl -n keycloak-lab get pods -l app=bff ``` ```text NAME READY STATUS RESTARTS AGE bff-695646ddb-kzs9k 0/1 CrashLoopBackOff 3 (20s ago) 90s ``` 로그보다 먼저 이벤트를 보고, 그다음 로그를 본다. 지금 파드와 죽기 전 파드를 따로 본다. ```bash label="[lab host] ④ 이벤트를 본다" kubectl -n keycloak-lab describe pod -l app=bff | tail -20 ``` ```bash label="[lab host] ⑤ 지금 로그와 죽기 전 로그를 본다" kubectl -n keycloak-lab logs -l app=bff --tail=40 kubectl -n keycloak-lab logs -l app=bff --previous --tail=40 ``` 실측은 이렇다(observed, 해설 문서 1절). ```text Failed to bind properties under 'spring.data.redis.port' to int: Property: spring.data.redis.port Value: "${REDIS_PORT:6379}" Reason: failed to convert java.lang.String to int (caused by NumberFormatException: For input string: "tcp://10.43.57.116:6379") ``` **왜 필요한가** — 마지막 줄의 `"tcp://10.43.57.116:6379"` 는 매니페스트 어디에도 쓰지 않은 값이다. 주입 전에 `printenv` 로 미리 본 그 환경변수이고 쿠버네티스가 넣었다. Redis 는 멀쩡하고, 파드는 Redis 에 붙어 보지도 못한 채 설정 바인딩에서 죽었다. 메시지가 `Failed to bind properties` 라고 말하고 있다. ```bash label="[lab host] ⑥ Redis 가 멀쩡한지 따로 확인한다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping ``` `PONG` 이 온다(observed). **문제가 생기면** — 파드가 정상으로 떴다면 매니페스트에 `REDIS_PORT` 를 이미 줬다. 함정을 건너뛰어도 결과는 같으므로 다음 단계로 간다. ### 3. 자동 주입을 끄고 다시 올린다 **목적** — 이름 충돌의 근본을 없앤다. 처방은 둘인데 하나만 근본 처방이다. | 처방 | 문제 | |---|---| | 환경변수 이름을 바꾼다 (`BFF_REDIS_PORT` 등) | 다음 사람이 같은 함정에 다시 빠진다 | | 주입 자체를 끈다 | 근본 처방 | ```bash label="[워크스테이션] ① 배포 매니페스트를 다시 연다" vim deploy/lab/k8s/bff-redis.yaml ``` ```yaml spec: enableServiceLinks: false # 근본 처방 containers: - name: bff env: - name: SPRING_SESSION_STORE_TYPE value: redis - name: REDIS_HOST value: redis.keycloak-lab.svc - name: REDIS_PORT value: "6379" ``` ```bash label="[lab host] ② 적용하고 롤아웃을 기다린다" kubectl apply -f deploy/lab/k8s/bff-redis.yaml kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s ``` **예상 결과** — 실측은 이렇다(observed, `01-servicelinks-trap.txt`). ```text deployment.apps/bff configured deployment "bff" successfully rolled out bff-576d869c6d-bshvl true kc-lab-2 bff-695646ddb-kzs9k true kc-lab-1 bff-695646ddb-vjqzf true kc-lab-2 ``` 세 줄이다. replica 는 2인데 파드가 3개 보이는 것은 롤아웃 전환 중에 찍어서이고, 옛 ReplicaSet 의 파드가 아직 종료 전이다. 잠시 뒤 두 개가 된다. **왜 필요한가** — `enableServiceLinks: false` 는 그 파드에 대해 Service 이름 기반 환경변수 주입 전체를 끈다. 이름 하나를 피해 가는 것과 달라서 다음에 Service 를 하나 더 만들어도 같은 충돌이 안 난다. **문제가 생기면** — 고쳤는데 오류가 똑같이 나면 파드가 아직 안 바뀌었다. `rollout restart` 뒤에 `printenv` 를 다시 본다. ## 주입 검증 결과를 읽기 전에 주입이 의도한 것을 정확히 했는지 먼저 본다. 자동 주입이 정말 사라졌는지부터다. ### 1. 자동 주입된 환경변수가 사라졌는가 ```bash label="[lab host] 새 파드 이름을 다시 잡고 환경변수를 본다" BFF=$(kubectl -n keycloak-lab get pod -l app=bff \ --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') kubectl -n keycloak-lab exec "$BFF" -- printenv | grep -i redis ``` 모양은 이렇다(observed). ```text REDIS_HOST=redis.keycloak-lab.svc REDIS_PORT=6379 ``` `REDIS_SERVICE_HOST` 계열이 전부 사라졌고 넘겨준 두 개만 남았다. `REDIS_PORT` 가 `6379` 다. ### 2. 자동구성이 실제로 걸렸는가 ```bash label="[lab host] 파드 배치와 헬스를 본다" kubectl -n keycloak-lab get pods -o wide -l app=bff kubectl -n keycloak-lab exec "$BFF" -- \ wget -qO- http://localhost:8083/actuator/health ``` 모양은 이렇다(observed). ```json {"status":"UP","components":{"redis":{"status":"UP","details":{"version":"7.4.x"}},...}} ``` `redis` 컴포넌트가 있고 `UP` 이다. B-0 에서는 이 컴포넌트가 아예 없었다. `spring-boot-starter-data-redis` 가 헬스 인디케이터를 같이 들고 왔고, 건강 체크에 새 항목이 생긴 것이 자동구성이 걸렸다는 신호다. ### 3. replica 2 에서 로그인이 되는가 브라우저에서 쿠키를 먼저 지우고 `https://app1.hyeonworks.com/` 로 로그인한다. 로그인이 된다(observed, `b1-login-works-two-replicas.png`). B-0 에서 replica 2 로는 `/login?error` 였던 그 부분이다. 인가 요청의 state 와 PKCE verifier 가 이제 Redis 에 있으므로 콜백이 다른 인스턴스로 가도 찾을 수 있다. B-0 이 replica 를 1 로 줄여야 했던 문제는 고쳐졌다. 원 가이드는 여기에 곧바로 경고를 붙인다 — 여기서 멈추면 Redis 를 붙였더니 다 해결됐다로 끝나고, 그것이 이 실험이 막으려는 결론이다. ## 관찰 ### 1. 빈 목록을 다시 찍어 before 와 견준다 **무엇을 보는가** — 늘어난 빈과 안 바뀐 빈. B-0 의 방법을 그대로 다시 쓴다. ```bash label="[lab host] ① 빈 목록을 받고 개수를 센다" kubectl -n keycloak-lab exec "$BFF" -- \ wget -qO- http://localhost:8083/actuator/beans > /tmp/beans-after.json grep -o '"aliases":\[' /tmp/beans-after.json | wc -l ``` **어디를 보나** — 실측은 이렇다(observed, `02-autoconfig-after.txt`). ```text 빈 수: 321 → 402 (+81) ``` 새로 생긴 세션 저장소 빈을 뽑는다. 원 가이드가 아래 줄을 미검증으로 표시했다(unknown). ```bash label="[lab host] ② 세션·Redis 계열 빈을 뽑는다" grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-after.json \ | sed 's/{"aliases".*"type":"/ -> /' \ | grep -iE 'session|redis' ``` ```text --- 세션 저장소 관련 (새로 생긴 것) --- ★ cookieSerializer -> DefaultCookieSerializer ★ org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration -> RedisHttpSessionConfiguration ★ sessionRepository -> RedisSessionRepository ★ springSessionRepositoryFilter -> SessionRepositoryFilter ★ redisConnectionFactory -> LettuceConnectionFactory ★ redisTemplate -> RedisTemplate ``` 같은 파일에서 인가된 클라이언트 쪽을 따로 뽑는다. 이 실험이 판정하려는 것이 이쪽이다. ```bash label="[lab host] ③ authorized client 계열을 뽑는다" grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-after.json \ | sed 's/{"aliases".*"type":"/ -> /' \ | grep -i authorizedclient ``` ```text --- OAuth2 authorized client — 바뀌었는가? --- authorizedClientService before: InMemoryOAuth2AuthorizedClientService after : InMemoryOAuth2AuthorizedClientService 그대로 — Redis 로 안 옮겨졌다 authorizedClientRepository before: AuthenticatedPrincipalOAuth2AuthorizedClientRepository after : AuthenticatedPrincipalOAuth2AuthorizedClientRepository 그대로 — Redis 로 안 옮겨졌다 authorizedClientManager before: AuthorizedClientServiceOAuth2AuthorizedClientManager after : AuthorizedClientServiceOAuth2AuthorizedClientManager 그대로 — Redis 로 안 옮겨졌다 ``` **이 값이 뜻하는 것** — 빈 81개가 늘었는데 인가된 클라이언트는 하나도 안 바뀌었다. ```text Application Session ──▶ Redis (인증 상태, principal, 인가 요청) OAuth2AuthorizedClient ──▶ 프로세스 메모리 (access token, refresh token) ``` `spring.session.store-type` 은 `HttpSession` 을 갈아끼우는 설정이고 `OAuth2AuthorizedClient` 는 그 설정과 무관한 다른 저장소에 있다. 찍어서 확인하지 않으면 이 사실을 알 방법이 없다. 로그인은 되고 화면도 뜨고 파드도 건강하다. B-0 을 실험으로 만든 까닭이 여기서 드러난다 — before 가 있어야 after 를 읽는다. ### 2. Redis 를 직접 연다 **무엇을 보는가** — 옮겨진 것 안에 무엇이 들었는지. ```bash label="[lab host] ① 키 개수와 키 이름을 본다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan ``` 실측은 이렇다(observed, `03-redis-contents.txt`). ```text === Redis 에 무엇이 들어 있는가 === bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae 총 키 수: 1 ``` 네임스페이스가 `bff:session` 이다. `application.yml` 의 `spring.session.redis.namespace` 가 그대로 접두어가 됐다. 키 이름을 변수로 잡는다. 이 줄에는 걸러 내는 조각이 둘 붙어 있다. ```bash label="[lab host] ② 세션 키 하나를 변수에 담는다" KEY=$(kubectl -n keycloak-lab exec deploy/redis -- \ redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | head -1 | tr -d '\r') echo "$KEY" ``` `grep -v expires` 가 필요한 까닭은 Spring Session 이 만료 추적용 키인 `bff:session:expirations:*` 와 `bff:session:sessions:expires:*` 도 만들기 때문이다. 그것을 잡으면 다음 명령이 빈 결과를 낸다. `tr -d '\r'` 은 `redis-cli` 출력이 CR 을 달고 오기 때문에 붙인다. 빼면 키가 안 맞는데 오류는 안 난다. 바로 위 `redis-cli --scan` 이 이미 전체 키를 보여 줬으므로 그 출력에서 키 하나를 눈으로 골라 쳐도 되는데, 원 가이드가 그 두 단계 형태를 적어 두지 않았다(unknown). ```bash label="[lab host] ③ 타입과 필드 이름을 본다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli type "$KEY" kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "$KEY" ``` **어디를 보나** — 실측은 이렇다(observed, `03-redis-contents.txt`). ```text 타입: hash 필드: sessionAttr:SPRING_SECURITY_CONTEXT 필드: sessionAttr:SPRING_SECURITY_SAVED_REQUEST 필드: sessionAttr:SPRING_SECURITY_LAST_EXCEPTION 필드: sessionAttr:org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository.AUTHORIZATION_REQUEST 필드: lastAccessedTime 필드: maxInactiveInterval 필드: creationTime ``` **이 값이 뜻하는 것** — 필드 목록에 토큰이 없다. 저장소를 직접 열어 refresh token 이 평문으로 남는지 확인하는 것이 검증 항목이었는데, 답은 더 앞에 있었다 — 애초에 들어가지 않는다. 토큰 암호화를 어떻게 할지 고민하기 전에 토큰이 그 저장소에 가지도 않는다는 것을 먼저 알아야 한다. ### 3. TTL 과 값의 바이트를 본다 ```bash label="[lab host] ① 남은 수명을 본다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "$KEY" ``` ```text === TTL (Q3 검증 3번 — session TTL) === TTL: 1772 초 ``` `spring.session.timeout=30m` 인 1800초에서 방금 지난 만큼 줄어든 값이다. 세션 TTL 1772초와 access token 수명 60초가 처음부터 어긋나 있다. 어느 쪽에 맞출지 고르기 전에 이미 어긋나 있고, 그 간극을 누가 메우는지가 B-3 의 주제다. ```bash label="[lab host] ② 값의 앞 네 줄을 이스케이프해서 본다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli --no-raw hgetall "$KEY" | head -4 ``` ```text 1) "sessionAttr:SPRING_SECURITY_CONTEXT" 2) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x02l\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Sorg.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken... ``` `\xac\xed` 로 시작한다. Java 직렬화 매직 넘버이고 JSON 이 아니다. `--no-raw` 를 쓰는 것은 바이너리를 이스케이프해서 보여 주기 때문이다. 안 쓰면 터미널이 제어문자를 먹고 화면이 깨진다. | 결과 | | |---|---| | 사람이 못 읽는다 | 운영 중 디버깅이 어렵다 | | 클래스 버전에 묶인다 | 애플리케이션을 올리면 기존 세션이 역직렬화에 실패할 수 있다 | | 역직렬화 취약점 | 신뢰할 수 없는 데이터가 들어오면 위험한 형식이다 | D-2 의 버전 업그레이드에서 이 성질이 다시 나온다. Spring Security 버전이 바뀌면 Redis 에 있던 세션이 깨질 수 있다. ### 4. 파드를 전부 교체하고 사용자 화면을 본다 **목적** — 롤링 재시작으로 Redis 덕을 보는지 확인한다. 롤링 재시작은 정상 작업이라 되돌릴 것이 없다. ```bash label="[lab host] ① 파드를 전부 교체한다" kubectl -n keycloak-lab rollout restart deployment/bff kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s ``` 로그인은 그대로 둔 채 브라우저에서 `https://app1.hyeonworks.com/bff/token-boundary` 를 연다. **예상 결과** — 실측은 이렇다(observed, `b1-token-boundary-after-redis.png`). ```json {"pattern":"AP3-backend-for-frontend", "principal":"labuser", ← 세션은 Redis 에서 복원되었다 "accessTokenStoredOnServer":false, ← 토큰은 사라졌다 "refreshTokenStoredOnServer":false, "browserTokenCount":0, "csrfProtectionEnabled":true} ``` **왜 필요한가** — `principal` 은 살아 있는데 두 토큰이 `false` 다. ```text 사용자 관점: 로그인되어 있다고 나온다 실제: BFF 가 사용자를 대신해 아무것도 못 한다 ``` 파드가 전부 교체됐는데 로그인 상태는 살아남았다. Redis 덕분이다. 토큰은 같이 살아남지 못했다. 인스턴스 메모리에 있었으니까. 원 가이드는 이것을 부분적으로만 공유했을 때의 실패 모양이라고 부르고, 완전히 로그아웃되는 편이 차라리 낫다고 적는다. 적어도 사용자가 다시 로그인하는데, 지금은 화면상 로그인 상태라 사용자가 아무것도 안 한다. | | B-0 (Redis 없음, replica 1) | B-1 (Redis 세션, replica 2) | |---|---|---| | `principal` | labuser | labuser | | `accessTokenStoredOnServer` | true | false | | 파드 재시작 후 | 로그아웃 | 로그인 상태만 남고 토큰은 소실 | **문제가 생기면** — 스크린샷으로 시점을 구별하지 않는다. 증거 폴더의 `README.md` 가 적어 둔 대로 `b1-login-works-two-replicas.png` 와 `b1-token-boundary-after-redis.png` 는 동일 파일이다. 세 시점 모두 `accessTokenStoredOnServer: false` 인 같은 화면이었고, 시점 구별은 터미널 출력과 Redis·DB 조회가 한다. 그래서 무엇을 해야 하는가 — `OAuth2AuthorizedClientService` 를 공유 저장소로 옮기는 구현이 따로 필요하다. | 후보 | | |---|---| | `JdbcOAuth2AuthorizedClientService` | Spring Security 기본 제공. PostgreSQL 이 이미 있다 | | 직접 구현 (Redis) | `OAuth2AuthorizedClientService` 인터페이스를 Redis 로 구현 | | 세션 안에 넣기 | `HttpSessionOAuth2AuthorizedClientRepository` 를 쓰면 세션과 함께 Redis 로 간다 | 세 번째는 조회 키 문제까지 같이 푼다. 세션 단위로 저장되므로 같은 사용자의 다른 브라우저가 서로를 덮어쓰지 않고, 대신 세션이 커진다. B-2 가 이 선택지를 비교한다. ## 복구와 원상복구 확인표 B-2 로 이어갈 것이면 이 구성이 B-2 의 출발이므로 아무것도 안 되돌린다. ### 1. 소스를 되돌리고 다시 빌드해 다시 밀어 넣는다 B-0 상태로 되돌릴 때는 소스만 되돌려서는 안 된다. 클러스터에는 여전히 옛 이미지가 돈다. ```bash label="[워크스테이션] ① 네 파일을 되돌리고 확인한다" git checkout -- bff/pom.xml bff/src/main/resources/application.yml \ bff/src/test/java/com/example/keycloakpattern/bff/BffControllerTest.java \ deploy/lab/k8s/bff-redis.yaml git status --short ``` ```bash label="[워크스테이션 → kc-lab-1] ② 다시 빌드해 두 노드에 다시 넣고 다시 배포한다" docker build --progress=plain -t keycloak-pattern-bff:lab bff/ > /tmp/build.log 2>&1 docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-1 'sudo k3s ctr images import -'" docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-2 'sudo k3s ctr images import -'" kubectl apply -f deploy/lab/k8s/bff-redis.yaml kubectl -n keycloak-lab rollout restart deployment/bff kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s ``` ② 도 한 블록에 기계가 둘이다. `docker` 로 시작하는 앞의 세 줄은 워크스테이션, `kubectl` 로 시작하는 뒤의 세 줄은 `kc-lab-1` 이다. ① 이 되돌린 파일이 `kc-lab-1` 쪽 체크아웃에도 반영돼 있어야 `apply` 가 B-0 구성을 올린다. `git status --short` 가 빈 출력이어도 클러스터는 아직 옛 이미지를 쓰고 있다. ② 를 끝내야 소스와 클러스터가 같아진다. ### 2. Redis 를 비운다 Redis 를 비우는 것은 되돌릴 수 없다. 지운 세션은 돌아오지 않고 로그인한 사용자는 전부 로그아웃된다. 실험대라서 하는 일이다. ```bash label="[lab host] ① 개수를 보고 비우고 다시 센다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushdb kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize ``` 세션 하나만 지우려면 이쪽이다. `$KEY` 는 관찰 2번에서 담아 둔 키이고, 그 뒤로 `rollout restart` 와 브라우저 왕복이 들어가 절차가 40분쯤 걸리므로 터미널을 새로 열었으면 값이 비어 있다. 치기 전에 `echo "$KEY"` 로 키가 나오는지 보고, 안 나오면 관찰 2번의 `KEY=$(...)` 두 줄을 다시 쳐서 담는다. 바로 위 ① 의 `flushdb` 를 이미 쳤으면 지울 키가 없으므로 ② 를 건너뛴다. ```bash label="[lab host] ② 잡아 둔 키 하나만 지운다" kubectl -n keycloak-lab exec deploy/redis -- redis-cli del "$KEY" ``` | 항목 | 명령 | 돌아왔을 때 | |---|---|---| | 소스 | `git status --short` | 출력 없음 (B-0 로 되돌릴 때) | | 이미지 | `sudo k3s ctr images ls \| grep keycloak-pattern-bff` | 두 노드 모두에 있다 | | 파드 | `kubectl -n keycloak-lab get pods -o wide -l app=bff` | `2/2`, 두 노드에 하나씩 | | Redis | `kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping` | `PONG` | | Redis 키 | `... redis-cli dbsize` | 의도한 값 | | Keycloak | `kubectl -n keycloak-lab get pods \| grep keycloak` | 둘 다 `1/1 Running` | | 밖 | `curl -I https://app1.hyeonworks.com/` | `200` | | 임시 파일 | `rm -f /tmp/beans-before.json /tmp/beans-after.json /tmp/build.log` | — | 표의 두 줄은 한 기계에서 다 못 본다. **이미지** 줄의 `sudo k3s ctr images ls` 는 그 명령을 친 노드 하나만 본다. `kc-lab-1` 에서 치면 `kc-lab-2` 는 안 보이므로, 「두 노드 모두에 있다」를 확인하려면 B-0 이 짝으로 친 `ssh kc-lab-2 'sudo k3s ctr images ls | grep keycloak-pattern-bff'` 도 함께 봐야 한다. 한쪽에만 있으면 그 노드의 replica 만 뜨고 나머지는 `ErrImageNeverPull` 로 나타난다. **임시 파일** 줄도 앞의 두 파일은 `kc-lab-1`, `/tmp/build.log` 는 워크스테이션에 있으므로 각 기계에서 자기 쪽을 지운다. ## 막히면 원 가이드는 이 표를 두고 전부 이 실험대가 실제로 겪은 증상이고 지어낸 것은 없다고 적는다. | 증상 | 원인 | 확인 | |---|---|---| | 파드가 `CrashLoopBackOff` 이고 오류에 `tcp://...:6379` | 쿠버네티스가 `REDIS_PORT` 를 주입했다 | `printenv \| grep -i redis` | | 위 오류를 Redis 가 죽어서로 읽었다 | 메시지가 `Failed to bind properties` 다 | `redis-cli ping` 으로 Redis 를 따로 확인 | | `enableServiceLinks` 를 넣었는데 그대로다 | 파드가 아직 옛 것이다 | `rollout restart` 후 `printenv` 다시 | | 빌드가 Redis 연결 오류로 죽는다 | 테스트가 Redis 를 찾는다 | `spring.session.store-type=none` | | `sessionRepository` 가 안 생긴다 | 의존성을 하나만 넣었다. 오류 없이 메모리에 남는다 | 두 개 다 있는지 `pom.xml` | | `redis-cli --scan` 이 비어 있다 | 아직 로그인 안 했다 | 브라우저로 로그인 후 다시 | | `hkeys` 가 빈 결과 | 만료 추적 키를 잡았다 | `grep -v expires` | | 키가 맞는데 명령이 안 먹는다 | 출력에 CR 이 붙었다 | `tr -d '\r'` | | 값이 깨져서 터미널이 이상해진다 | 바이너리를 그대로 찍었다 | `--no-raw` | | API 호출이 `500` 인데 토큰은 멀쩡하다 | DNS 다. 다른 네임스페이스의 서비스 | 로그의 `UnresolvedAddressException` | | `/actuator/beans` 가 `Bad Gateway` | 응답이 커서 프록시가 못 넘긴다 | 파드 안에서 받는다 | | `jq: command not found` | 이 실험대에 `jq` 가 없다 | `grep` 으로 읽는다 | | 로그인은 되는데 API 가 전부 실패한다 | 이게 이 실험의 결론이다 | `token-boundary` 의 두 `false` | | 스크린샷으로 시점을 구별하려다 헷갈린다 | 두 파일이 동일하다 | 터미널 출력과 Redis 조회로 구별 | `500` 쪽은 원인을 찾는 데 한 번 헛짚었다. 로그를 보니 토큰이 아니라 DNS 였다. 아래 줄의 `$BFF` 는 주입 검증 1번에서 담아 둔 파드 이름이다. 「막히면」은 아무 때나 펼치는 절이고 그 사이에 관찰 4번의 `rollout restart` 가 파드를 통째로 갈아치우므로, 그대로 치면 `NotFound` 가 온다. 치기 전에 주입 검증 1번의 `BFF=$(...)` 두 줄을 다시 쳐서 이름을 새로 담는다. ```bash label="[lab host] 예외만 골라 본다" kubectl -n keycloak-lab logs "$BFF" --tail=100 | grep -iE 'exception|error' ``` ```text java.nio.channels.UnresolvedAddressException ``` `RESOURCE_API_BASE_URL=http://echo.keycloak-lab.svc:8080` 이었는데 `echo` 는 `header-lab` 네임스페이스의 8081 이었다. 배포조차 되어 있지 않았다. ```yaml # 다른 네임스페이스의 서비스는 ..svc 로 부른다 - name: RESOURCE_API_BASE_URL value: http://echo.header-lab.svc:8081 ``` ```bash label="[lab host] 그 서비스가 어디 있는지 본다" kubectl -n header-lab get svc echo ``` ## 이 실험대에서 아직 못 밟은 단계 2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.** - **남은 것** — 브라우저 로그인 뒤의 Redis 키·필드·TTL 관찰. 지금까지 밟은 것 — 이미지 빌드·반입, 배포, `/actuator/beans` 로 `RedisSessionConfiguration` 확인. - **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다. - **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다. ## 무엇이 관측이고 무엇이 아닌가 이 절차의 숫자는 `2026-09-04 13:59–14:03 KST` 에 돈 한 번의 실행에서 나왔다(observed). - (observed) 자동 주입된 `REDIS_*` 일곱 줄과 `REDIS_PORT=tcp://10.43.57.116:6379`, `Failed to bind properties` 오류 전문, `enableServiceLinks: false` 뒤의 롤아웃 출력 세 줄, 빈 수 `321 → 402 (+81)`, 새로 생긴 세션 저장소 빈 여섯 줄, 안 바뀐 인가된 클라이언트 빈 세 개의 before 와 after, Redis 키 `bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae` 와 필드 일곱 개, `TTL: 1772 초`, `\xac\xed` 로 시작하는 바이트, 재시작 뒤 `token-boundary` 의 `principal` 생존과 두 토큰 `false`, `UnresolvedAddressException`. - (unknown) 빈을 세는 `grep -o '"aliases":\['` 줄과 이름·타입을 뽑는 `grep`·`sed` 줄. 원 가이드가 미검증으로 표시했다. `jq` 판본과, `--scan` 출력에서 키를 눈으로 골라 치는 두 단계 형태와, 두 겹 `ssh` 를 나눠 치는 형태도 가이드에 없다. - (observed) `b1-login-works-two-replicas.png` 와 `b1-token-boundary-after-redis.png` 가 동일 파일이라는 것은 증거 폴더의 `README.md` 가 적어 둔 사실이다. 같은 화면이 세 시점에 나왔기 때문이고, 그래서 시점은 터미널 출력과 저장소 조회로만 갈린다. - 이 실험이 재지 않은 것 셋 — Redis 를 끊었을 때 무엇이 나는지는 B-5 의 주제이고, 로그아웃 뒤 두 저장소에 무엇이 있는지는 B-2 로 넘겼으며, 저장소 지연이 화면 지연으로 얼마나 번역되는지는 B-2 이후에 잰다.