Builds up from HTTP statelessness to why session storage location determines the operational response, so the measurements have context to land in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
401 lines
18 KiB
Markdown
401 lines
18 KiB
Markdown
# 이 실험을 이해하기 위한 선수 지식
|
|
|
|
실험 결과를 먼저 들이밀었더니 맥락이 사라졌다. 이 문서는 **왜 이런 걸
|
|
측정하고 있는지**를 바닥부터 세운다.
|
|
|
|
읽는 순서가 곧 의존 관계다. 아는 절은 건너뛰어도 되지만, 3장까지는
|
|
"세션"이라는 말의 뜻이 계속 바뀌므로 훑고 가는 편이 낫다.
|
|
|
|
---
|
|
|
|
## 0. 출발점 — 당신이 원래 물은 것
|
|
|
|
> Keycloak이 여러 개일 경우, 세션 저장소를 Redis나 별도 저장소로 쓸 경우,
|
|
> Redis와 DB에 분리해서 세션과 토큰을 관리할 때 어떻게 달라지는지.
|
|
> SSO를 추가하면 어떻게 달라지는지. Redis 또는 DB가 죽으면 어떻게 복구하는지.
|
|
|
|
이 질문에 답하려면 **"세션이 어디에 있는가"** 를 정확히 알아야 한다.
|
|
지금 하고 있는 실험은 전부 그 한 문장을 쪼갠 것이다.
|
|
|
|
---
|
|
|
|
## 1. HTTP는 기억이 없다
|
|
|
|
모든 것의 출발점.
|
|
|
|
```
|
|
요청 1: GET /login → 서버
|
|
요청 2: GET /mypage → 서버 ← 서버는 요청 1을 기억하지 못한다
|
|
```
|
|
|
|
HTTP 요청은 **하나하나가 완전히 독립적**이다. 서버 입장에서 두 번째 요청은
|
|
생판 처음 보는 사람이 보낸 것과 구별되지 않는다.
|
|
|
|
그래서 "로그인했다"는 사실을 **어딘가 저장**해야 한다.
|
|
|
|
```
|
|
브라우저 서버
|
|
┌──────────────┐ ┌────────────────────────┐
|
|
│ 쿠키 │ │ 세션 저장소 │
|
|
│ SESSIONID= │ ──── 매 요청 ────▶ │ abc123 → { │
|
|
│ abc123 │ 이 값만 보냄 │ user: "홍길동", │
|
|
└──────────────┘ │ 로그인시각: ... │
|
|
│ } │
|
|
작은 표만 들고 다닌다 └────────────────────────┘
|
|
실제 내용은 여기 있다
|
|
```
|
|
|
|
| 용어 | 뜻 |
|
|
|---|---|
|
|
| **쿠키** | 브라우저가 들고 다니는 **작은 표(번호표)**. 보통 세션 ID만 들어 있다 |
|
|
| **세션** | 서버가 그 번호에 대해 기억하는 **실제 내용** |
|
|
|
|
**서버가 1대면 여기서 이야기가 끝난다.** 문제는 2대부터다.
|
|
|
|
---
|
|
|
|
## 2. 서버가 2대가 되는 순간 — 이 실험의 진짜 출발점
|
|
|
|
```
|
|
로그인 요청 ──▶ 서버 A A의 메모리에 "abc123 = 홍길동" 기록
|
|
다음 요청 ──▶ 서버 B B: "abc123? 그런 거 모르는데" → 로그아웃 화면
|
|
```
|
|
|
|
**이게 전부다.** 분산 세션이라는 주제 전체가 이 한 장면에서 나온다.
|
|
|
|
푸는 방법은 셋뿐이다.
|
|
|
|
| 방법 | 어떻게 | 대가 |
|
|
|---|---|---|
|
|
| **1. 고정 배정** (sticky session) | 같은 사람은 항상 같은 서버로 보낸다 | **그 서버가 죽으면 그 사람 세션은 사라진다.** 부하도 안 고르게 퍼진다 |
|
|
| **2. 복제** | 서버끼리 메모리 내용을 서로 보낸다 | 서버가 N대면 트래픽이 N² 로 는다. 어긋남(불일치)이 생긴다 |
|
|
| **3. 공유 저장소** | 세션을 바깥(DB·Redis)에 두고 모두가 본다 | **그게 죽으면 전체가 멈춘다.** 매 요청마다 네트워크 왕복 |
|
|
|
|
> **당신이 원래 물은 "Redis나 별도 저장소를 쓰면"이 바로 3번**이다.
|
|
> 그리고 "Redis나 DB가 죽으면 어떻게 복구하나"는 3번의 대가를 묻는 것이다.
|
|
|
|
**Keycloak도 예외가 아니다.** Keycloak을 2대 띄우면 정확히 이 문제가 생긴다.
|
|
Keycloak이 이걸 어떻게 풀었는지가 실험 0의 주제다.
|
|
|
|
---
|
|
|
|
## 3. Keycloak은 무엇이고, 왜 세션을 갖는가
|
|
|
|
### 3-1. 하는 일
|
|
|
|
Keycloak은 **로그인을 대신 해주는 서버**다.
|
|
|
|
```
|
|
[사용자] [내 앱] [Keycloak]
|
|
│ │ │
|
|
│─ 접속 ──────▶│ │
|
|
│◀─ "Keycloak 가서 로그인하고 와" ────│
|
|
│──────────────────── 로그인 ───────▶│
|
|
│◀─────────────── 토큰 발급 ─────────│
|
|
│─ 토큰 들고 ──▶│ │
|
|
│ │─ 이 토큰 유효해? ──▶│
|
|
```
|
|
|
|
내 앱은 비밀번호를 저장하지도, 검증하지도 않는다. 그 일을 Keycloak이 한다.
|
|
|
|
### 3-2. 그래서 **세션이 두 겹**이 된다
|
|
|
|
여기가 헷갈리는 지점이다. "세션"이라는 말이 두 가지를 가리킨다.
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Keycloak 의 SSO 세션 │
|
|
│ "이 브라우저는 홍길동으로 로그인되어 있다" │
|
|
│ 쿠키 이름: KEYCLOAK_IDENTITY │
|
|
└─────────────────────────────────────────────────────┘
|
|
│ │
|
|
▼ ▼
|
|
┌──────────────────┐ ┌──────────────────┐
|
|
│ 앱1 의 세션 │ │ 앱2 의 세션 │
|
|
│ (또는 토큰) │ │ (또는 토큰) │
|
|
└──────────────────┘ └──────────────────┘
|
|
```
|
|
|
|
| | 누가 갖는가 | 사라지면 |
|
|
|---|---|---|
|
|
| **SSO 세션** | **Keycloak** | 모든 앱에서 다시 로그인해야 한다 |
|
|
| 앱 세션 | 각 애플리케이션 | 그 앱만 다시 들어가면 된다 |
|
|
|
|
**SSO가 되는 원리가 이것이다.** 앱1에서 로그인하면 Keycloak에 SSO 세션이
|
|
생긴다. 앱2로 가면 Keycloak이 "이 브라우저 이미 로그인했네" 하고 **로그인
|
|
화면 없이** 바로 토큰을 준다.
|
|
|
|
> **그래서 Keycloak의 세션이 사라지면 SSO 전체가 깨진다.**
|
|
> 당신 질문의 "SSO를 추가하면 어떻게 달라지는지"가 여기 걸린다.
|
|
> 앱이 하나일 때는 그 앱만 재로그인이지만, SSO에서는 **전 앱이 동시에** 터진다.
|
|
|
|
---
|
|
|
|
## 4. 토큰이 있는데 왜 세션이 필요한가
|
|
|
|
가장 흔한 오해다. "JWT는 stateless라서 서버가 기억할 게 없다"는 말은
|
|
**반만 맞다.**
|
|
|
|
### 4-1. 토큰이 두 종류다
|
|
|
|
```
|
|
로그인 성공
|
|
│
|
|
├──▶ access token 수명 짧음 (이 실험대: 60초)
|
|
│ JWT. 서명이 붙어 있어 서버가 아무것도 기억 안 해도 검증된다
|
|
│ → 진짜 stateless
|
|
│
|
|
└──▶ refresh token 수명 김 (이 실험대: 1800초 = 30분)
|
|
access token 이 만료되면 이걸로 새로 받는다
|
|
→ 서버가 세션을 기억하고 있어야 한다
|
|
```
|
|
|
|
| | access token | refresh token |
|
|
|---|---|---|
|
|
| 검증 방식 | **서명만 보면 됨** | **서버 세션 조회 필요** |
|
|
| 취소 | **불가능** (만료를 기다려야) | 가능 |
|
|
| 수명 | 짧게 (분 단위) | 길게 (시간~일) |
|
|
|
|
### 4-2. 그래서 이렇게 된다
|
|
|
|
```
|
|
0초 로그인 세션 생성
|
|
0초 access token 발급 이후 60초간은 서버에 안 물어봐도 됨
|
|
60초 access token 만료
|
|
60초 refresh 요청 ─────▶ 서버: "이 세션 살아 있나?" ◀── 여기서 세션 필요
|
|
60초 새 access token
|
|
120초 또 만료 → 또 refresh → 또 세션 조회
|
|
```
|
|
|
|
**60초마다 세션 저장소를 친다.** access token 수명이 짧을수록 세션 저장소
|
|
부하가 커진다 — 보안과 성능의 맞바꿈이 여기서 일어난다.
|
|
|
|
### 4-3. 로그아웃도 세션이 있어야 한다
|
|
|
|
**로그아웃 = 세션 삭제**다. 세션이 없으면 로그아웃이라는 개념 자체가 없다.
|
|
이미 발급된 access token은 서명이 유효하므로 만료 전까지 계속 통과한다.
|
|
|
|
> 그래서 access token 수명을 60초로 짧게 잡는다. 로그아웃해도 최대 60초는
|
|
> 살아 있다는 뜻이고, 그 이상은 refresh 가 막히므로 끝난다.
|
|
|
|
---
|
|
|
|
## 5. Keycloak은 세션을 어디에 두는가 — **버전에 따라 답이 다르다**
|
|
|
|
이 실험 전체가 여기에 걸려 있다.
|
|
|
|
### 5-1. 두 개의 후보
|
|
|
|
| | 무엇 | 성질 |
|
|
|---|---|---|
|
|
| **Infinispan** | Keycloak **안에 내장된** 분산 캐시. Java 라이브러리 | 메모리. 빠름. 프로세스가 죽으면 사라짐 |
|
|
| **데이터베이스** | PostgreSQL 등 바깥의 DB | 디스크. 느림. 재시작해도 남음 |
|
|
|
|
**Infinispan은 별도로 설치하는 물건이 아니다.** Keycloak 프로세스 안에서 도는
|
|
라이브러리다. Redis처럼 따로 띄우는 게 아니다 — 이걸 헷갈리면 전체가 안 맞는다.
|
|
|
|
### 5-2. 버전별로 이렇게 바뀌었다
|
|
|
|
| 버전 | 진실의 원천 | 전체 재시작하면 |
|
|
|---|---|---|
|
|
| ~24 | **Infinispan (메모리)** | **세션 전부 소멸** |
|
|
| 25 | 선택 (`persistent-user-sessions` 옵션) | 설정에 따라 |
|
|
| **26 (지금 이 실험대)** | **데이터베이스** | **세션 살아남음** |
|
|
|
|
**이게 결정적이다.** 인터넷에 있는 Keycloak 클러스터링 자료 대부분은
|
|
24 이전 기준이라 **"세션은 Infinispan이 노드끼리 복제한다"** 고 쓰여 있다.
|
|
26에서는 더 이상 사실이 아니다.
|
|
|
|
> 제가 처음에 개념 문서에 "Infinispan = 캐시 + 노드 간 실시간 전파"라고
|
|
> 써둔 것도 이 옛 모델을 그대로 옮긴 것이었다. 실험 0에서 틀렸음이 드러났다.
|
|
|
|
---
|
|
|
|
## 6. Infinispan / JGroups / 7800 — 이름들의 정체
|
|
|
|
실험 로그에 계속 나오는 이름들이다.
|
|
|
|
```
|
|
Keycloak 프로세스
|
|
┌────────────────────────────────────────┐
|
|
│ Infinispan "세션을 어디 두고 어떻게 │ ← 캐시 계층
|
|
│ 나눌까" │
|
|
│ │ │
|
|
│ JGroups "누가 우리 멤버이고 │ ← 그룹 통신 계층
|
|
│ 어떻게 메시지를 주고받나" │
|
|
│ │ │
|
|
│ TCP 7800 실제 소켓 │ ← 네트워크
|
|
└────────────────────────────────────────┘
|
|
```
|
|
|
|
| 이름 | 정체 |
|
|
|---|---|
|
|
| **Infinispan** | Keycloak 내장 캐시. 세션·realm 설정·로그인 실패 횟수 등을 담는다 |
|
|
| **JGroups** | Infinispan이 노드끼리 대화할 때 쓰는 하부 라이브러리 |
|
|
| **TCP 7800** | JGroups가 쓰는 포트. **노드 간 통신 경로** |
|
|
| **jdbc-ping** | 서로를 **찾는** 방법. DB의 `JGROUPS_PING` 테이블을 게시판처럼 쓴다 |
|
|
| `ISPN000094` | "새 멤버 명단을 받았다"는 로그 코드 |
|
|
|
|
**찾는 것과 대화하는 것이 다른 경로다.**
|
|
|
|
```
|
|
디스커버리 (서로를 찾는다) → PostgreSQL JGROUPS_PING 테이블
|
|
트랜스포트 (실제 대화) → TCP 7800
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 왜 쿠버네티스와 노드 2대가 나오는가
|
|
|
|
당신 질문은 "Keycloak이 여러 개일 경우"였다. 그걸 **진짜로** 재현하려면
|
|
Keycloak 프로세스 2개가 **서로 다른 기계**에 있어야 한다.
|
|
|
|
| 방식 | 노드 상실을 실험할 수 있나 |
|
|
|---|---|
|
|
| Docker 컨테이너 2개 (한 기계) | **못 한다.** 커널이 하나라 "기계가 죽는" 상황을 못 만든다 |
|
|
| **VM 2대 + k3s** | **된다.** 하나를 전원 차단할 수 있다 |
|
|
|
|
그래서 이 실험대는 VM 2대(`kc-lab-1`, `kc-lab-2`) 위에 k3s를 올렸다.
|
|
|
|
```
|
|
kc-lab-1 (k3s 서버) kc-lab-2 (k3s 에이전트)
|
|
├─ keycloak-1 ├─ keycloak-0
|
|
├─ traefik, coredns └─ postgres
|
|
└─ prometheus, grafana
|
|
```
|
|
|
|
**`keycloak-0` / `keycloak-1` 은 Keycloak 프로세스**이고,
|
|
**`kc-lab-1` / `kc-lab-2` 는 그것들이 올라간 기계**다. 이름이 비슷해서
|
|
헷갈리기 쉬운데 계층이 다르다.
|
|
|
|
---
|
|
|
|
## 8. 그래서 실험 0은 무엇을 알아내려 한 것인가
|
|
|
|
### 8-1. 답해야 할 실무 질문
|
|
|
|
> Keycloak을 2대로 늘렸다. **한 대가 죽으면 로그인한 사람들은 어떻게 되나?**
|
|
> **DB가 죽으면?** **노드 사이 네트워크가 끊기면?**
|
|
|
|
이 질문들에 답하려면 **정상일 때 무엇이 어디에 있는지**를 먼저 알아야 한다.
|
|
그게 없으면 장애를 일으켜도 무엇이 왜 깨졌는지 해석할 수 없다.
|
|
|
|
### 8-2. 그래서 실험 0의 질문은 두 개다
|
|
|
|
```
|
|
질문 A. 한 노드에서 만든 세션을 다른 노드가 쓸 수 있는가?
|
|
↓ 답: 그렇다
|
|
|
|
질문 B. 그 공유는 무엇 덕분인가?
|
|
(a) Infinispan 이 메모리를 복제해서
|
|
(b) 둘 다 같은 DB 를 봐서
|
|
```
|
|
|
|
### 8-3. **B를 구분해야 하는 이유** — 운영 대응이 정반대다
|
|
|
|
| 상황 | (a) 복제라면 | (b) DB라면 |
|
|
|---|---|---|
|
|
| 노드 간 7800 끊김 | **세션 공유 깨짐** | **멀쩡** |
|
|
| DB 죽음 | 한동안 버팀 | **즉시 전면 장애** |
|
|
| 노드 1대 죽음 | 세션 살아남음 | 세션 살아남음 |
|
|
| 성능 병목 | 노드 간 네트워크 | **DB, 커넥션 풀** |
|
|
| 튜닝할 곳 | JGroups 설정 | **DB 인덱스, 커넥션 수** |
|
|
| 노드를 10대로 늘리면 | **복제 트래픽 폭증** | DB 부하 증가 |
|
|
|
|
**같은 증상에 정반대 처방이 나온다.** 그래서 추측이 아니라 측정으로
|
|
확정해야 했다.
|
|
|
|
---
|
|
|
|
## 9. 왜 하필 "캐시 엔트리 개수"를 셌는가
|
|
|
|
질문 B를 가르는 가장 직접적인 방법이기 때문이다.
|
|
|
|
```
|
|
keycloak-0 에만 로그인을 보낸다
|
|
│
|
|
└──▶ 그리고 keycloak-1 의 메모리를 들여다본다
|
|
|
|
그 세션이 들어와 있으면 → (a) 복제한 것
|
|
비어 있으면 → (b) DB 로 공유한 것
|
|
```
|
|
|
|
Keycloak은 자기 캐시에 몇 개가 들었는지를 `/metrics` 로 알려준다.
|
|
|
|
```
|
|
vendor_statistics_approximate_entries_unique{cache="sessions"} 7.0
|
|
───────────── ───
|
|
세션 캐시 7개 들어 있다
|
|
```
|
|
|
|
**측정 결과: keycloak-1은 계속 0이었다.** keycloak-0이 14개를 들고 있는
|
|
동안에도 0. 그리고 keycloak-1에 직접 로그인을 보낸 순간에만 늘었다.
|
|
|
|
```
|
|
단계 k0 k1
|
|
시작 2 0
|
|
keycloak-1 에 로그인 5회 2 5 ← k0 안 늘어남
|
|
keycloak-0 에 로그인 5회 7 5 ← k1 안 늘어남
|
|
|
|
PostgreSQL 세션 수: 12 = 7 + 5 ← 캐시 합과 정확히 일치
|
|
```
|
|
|
|
**각 노드는 자기가 처리한 것만 캐시한다. 메모리는 건너가지 않는다.**
|
|
→ 답은 **(b)**.
|
|
|
|
그리고 마지막으로 PostgreSQL 로그를 켜서, keycloak-1이 **실제로 날리는
|
|
SELECT 문**을 잡았다. 추측이 아니라는 것을 못 박기 위해서다.
|
|
|
|
---
|
|
|
|
## 10. 이 사실이 당신 운영에 뜻하는 것
|
|
|
|
| 알게 된 것 | 실무적 의미 |
|
|
|---|---|
|
|
| 세션은 DB에 있다 | **DB가 단일 장애점이다.** HA·백업 계획이 Keycloak 대수보다 중요하다 |
|
|
| 메모리는 로컬 캐시일 뿐 | Keycloak을 몇 대로 늘려도 **노드 간 트래픽은 안 는다.** 대신 DB 부하가 는다 |
|
|
| 남의 세션은 캐시 안 함 | **sticky session 은 정확성이 아니라 성능 문제다.** 없어도 동작하지만 DB를 더 친다 |
|
|
| refresh 마다 DB 읽기+쓰기 | access token 수명을 줄이면 **DB 부하가 그만큼 는다** |
|
|
| `synchronous_commit OFF` | **DB가 강제 종료되면 직전 수백 ms 갱신이 사라진다** (의도된 설계) |
|
|
| 낙관적 락 (`VERSION`) | **동시에 refresh 하면 한쪽이 진다.** 클라이언트에 재시도가 필요하다 |
|
|
|
|
---
|
|
|
|
## 11. 앞으로 할 실험과 각각이 답하는 질문
|
|
|
|
| # | 실험 | 답하는 실무 질문 | 예측 |
|
|
|---|---|---|---|
|
|
| **A-0** | ✅ 세션 복제 확인 | 정상일 때 세션은 어디 있나 | — (완료) |
|
|
| **A-1** | TCP 7800 차단 | 노드 간 네트워크가 끊기면? | 세션 공유는 **안 깨짐**. 무효화 전파가 깨질 것 |
|
|
| **A-2** | DB 정지 | **DB가 죽으면?** | **즉시 전면 장애** |
|
|
| A-2' | DB 강제 종료 | 복구하면 뭘 잃나 | 직전 수백 ms 세션 갱신 소멸 |
|
|
| **A-3** | 노드 1대 전원 차단 | **Keycloak 한 대가 죽으면?** | 세션 살아남음 |
|
|
| **A-4** | volatile 모드 비교 | 옛 방식(24 이전)은 뭐가 다른가 | A-1이 **정반대로** 치명적이 됨 |
|
|
| B-5 | 동시 refresh 경쟁 | 토큰 갱신이 겹치면? | 한쪽이 낙관적 락에서 짐 |
|
|
| — | SSO 다중 앱 | **SSO를 붙이면 뭐가 달라지나** | Keycloak 세션 하나가 전 앱을 좌우 |
|
|
|
|
**A-1이 특히 중요하다.** 통념("클러스터 포트 막으면 세션 깨짐")과
|
|
이번 측정("세션은 7800으로 안 다님")이 정면으로 어긋나므로, **둘 중 하나는
|
|
틀렸다.** 실험이 판정한다.
|
|
|
|
---
|
|
|
|
## 12. 용어 빠른 참조
|
|
|
|
| 용어 | 한 줄 |
|
|
|---|---|
|
|
| **세션** | 서버가 "이 사람 로그인했음"을 기억하는 것 |
|
|
| **SSO 세션** | Keycloak이 가진 세션. 이게 죽으면 전 앱 재로그인 |
|
|
| **access token** | 짧게 사는 JWT. 서명만으로 검증. 취소 불가 |
|
|
| **refresh token** | access token을 새로 받는 표. **세션 조회가 필요** |
|
|
| **sid** | 세션 식별자. JWT·DB·관리 API에서 **같은 문자열** |
|
|
| **Infinispan** | Keycloak **내장** 캐시 (별도 설치 아님) |
|
|
| **JGroups** | Infinispan의 노드 간 통신 라이브러리 |
|
|
| **jdbc-ping** | DB 테이블로 서로를 찾는 방식 |
|
|
| **7800** | 노드 간 통신 포트 |
|
|
| **persistent-user-sessions** | 세션을 DB에 저장하는 기능. **KC 26 기본값** |
|
|
| **`OFFLINE_USER_SESSION`** | 이름과 달리 **온라인 세션도** 여기 있다 (`offline_flag='0'`) |
|
|
| **낙관적 락** | 읽을 때 버전과 같을 때만 쓰기. 충돌은 사후 검출 |
|
|
| **kc-lab-1/2** | VM(기계) 이름 |
|
|
| **keycloak-0/1** | Keycloak 프로세스(파드) 이름 |
|