쿠키, 세션, JWT 완벽 비교: 인증 방식의 차이와 실무 선택 기준
“쿠키와 세션의 차이가 뭔가요?”, “JWT가 뭔가요?”, “세션 대신 JWT를 쓰는 이유는?” — 인증 관련 질문은 신입 면접의 단골이다. HTTP의 무상태(Stateless) 특성에서 시작해서, 쿠키·세션·JWT 각각이 이 한계를 어떻게 극복하는지, 그리고 실무에서 어떤 기준으로 선택하는지를 다룬다.
1. HTTP는 왜 상태를 기억하지 못하는가
1.1 Stateless 프로토콜
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ┌─────────────────────────────────────────────────────────────────────┐
│ HTTP의 Stateless 특성 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 요청 1: "로그인해주세요" (ID: hong, PW: 1234) │
│ 서버: "로그인 성공!" │
│ │
│ 요청 2: "내 주문 내역 보여주세요" │
│ 서버: "너 누구야?" ← 이전 요청 기억 못함! │
│ │
│ HTTP는 각 요청을 독립적으로 처리한다. │
│ 이전 요청의 정보를 기억하지 않는다 (Stateless). │
│ → 로그인 상태를 유지하려면 별도 메커니즘이 필요 │
│ │
│ 해결 방법 3가지: │
│ ① 쿠키: 클라이언트가 정보를 저장하고 매 요청에 보냄 │
│ ② 세션: 서버가 상태를 저장하고, 클라이언트에 ID만 줌 │
│ ③ JWT: 토큰 자체에 정보를 담아 클라이언트가 보관 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
2. 쿠키 (Cookie)
2.1 동작 원리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ┌─────────────────────────────────────────────────────────────────────┐
│ 쿠키 동작 흐름 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │── POST /login ──────────────────→ │ │
│ │ (ID, PW) │ 인증 확인 │
│ │ │ │
│ │←── Set-Cookie: userId=1 ───────── │ 쿠키 설정 │
│ │ │ │
│ │── GET /orders ──────────────────→ │ │
│ │ Cookie: userId=1 │ 쿠키 자동 전송 │
│ │ │ "userId=1이구나" │
│ │←── 주문 내역 ──────────────────── │ │
│ │ │ │
│ 브라우저가 쿠키를 저장하고, │
│ 같은 도메인 요청 시 자동으로 헤더에 포함하여 전송 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
2.2 쿠키의 주요 속성
1
2
3
4
5
6
7
| Set-Cookie: sessionId=abc123;
Path=/;
Domain=example.com;
Max-Age=3600;
HttpOnly;
Secure;
SameSite=Lax
|
| 속성 |
설명 |
HttpOnly |
JavaScript에서 접근 불가 (XSS 방어) |
Secure |
HTTPS에서만 전송 |
SameSite=Lax |
크로스 사이트 요청 시 쿠키 제한 (CSRF 방어) |
Max-Age |
쿠키 유효 시간 (초). 없으면 브라우저 종료 시 삭제 |
Domain |
쿠키가 전송될 도메인 |
Path |
쿠키가 전송될 경로 |
2.3 쿠키의 한계
1
2
3
4
5
| ● 용량 제한: 약 4KB
● 보안 취약: 클라이언트에 저장되므로 변조 가능
→ Cookie: userId=1 → userId=2로 변조하면 다른 사용자 행세 가능!
● 매 요청마다 전송: 불필요한 데이터도 전송 → 네트워크 부담
● 브라우저 전용: 모바일 앱, API 서버 간 통신에는 부적합
|
3. 세션 기반 인증
3.1 동작 원리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| ┌─────────────────────────────────────────────────────────────────────┐
│ 세션 기반 인증 흐름 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │── POST /login ──────────────────→ │ │
│ │ (ID, PW) │ ① 인증 확인 │
│ │ │ ② 세션 생성 │
│ │ │ Session Store: │
│ │ │ {abc123: {userId: 1, │
│ │ │ name: 홍길동}}│
│ │ │ │
│ │←── Set-Cookie: JSESSIONID=abc123── │ ③ 세션 ID를 쿠키로 전달 │
│ │ │ │
│ │── GET /orders ──────────────────→ │ │
│ │ Cookie: JSESSIONID=abc123 │ ④ 세션 ID로 사용자 조회 │
│ │ │ → userId=1, name=홍길동│
│ │←── 주문 내역 ──────────────────── │ │
│ │
│ 핵심: 실제 데이터는 서버에 저장, 클라이언트는 세션 ID만 보관 │
│ → 쿠키에는 세션 ID(의미 없는 랜덤 문자열)만 담김 │
│ → ID를 변조해도 서버에 해당 세션이 없으면 인증 실패 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
3.2 세션의 장단점
1
2
3
4
5
6
7
8
9
10
| 장점:
● 서버에서 상태 관리 → 보안성 높음
● 세션 ID는 의미 없는 값 → 변조해도 소용없음
● 서버에서 즉시 무효화 가능 (로그아웃, 강제 만료)
● 세션에 어떤 데이터든 저장 가능
단점:
● 서버 메모리 사용 → 사용자가 많으면 부담
● 서버 확장(Scale-out) 시 세션 공유 문제
● Stateful → 특정 서버에 종속될 수 있음
|
3.3 서버 확장 시 세션 문제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| ┌─────────────────────────────────────────────────────────────────────┐
│ Scale-out 시 세션 문제와 해결 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 문제: │
│ 요청1 → Server A (세션 생성: abc123) │
│ 요청2 → Server B (abc123 세션 없음! → 인증 실패) │
│ │
│ 해결 방법: │
│ │
│ ① Sticky Session (세션 고정) │
│ 로드밸런서가 같은 사용자를 항상 같은 서버로 보냄 │
│ 단점: 특정 서버에 트래픽 편중 │
│ │
│ ② Session Clustering │
│ 서버 간 세션 데이터 복제 │
│ 단점: 서버가 많아지면 복제 비용 증가 │
│ │
│ ③ 외부 세션 저장소 (Redis) ★ 실무에서 가장 많이 사용 │
│ ┌──────────┐ │
│ │ Redis │ ←── 모든 서버가 같은 세션 저장소 사용 │
│ └──────────┘ │
│ ▲ ▲ ▲ │
│ Server A B C │
│ │
│ ④ JWT (토큰 기반) → 서버에 세션을 저장하지 않음 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
4. JWT 기반 인증
4.1 JWT(JSON Web Token) 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| ┌─────────────────────────────────────────────────────────────────────┐
│ JWT 구조 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6Iu2Zi... │
│ ├── Header ──┤├── Payload ──────────────────┤├── Signature ──┤ │
│ │
│ Header (헤더): │
│ { │
│ "alg": "HS256", // 서명 알고리즘 │
│ "typ": "JWT" │
│ } │
│ │
│ Payload (페이로드 = Claims): │
│ { │
│ "sub": "1", // 사용자 ID │
│ "name": "홍길동", // 사용자 이름 │
│ "role": "USER", // 권한 │
│ "iat": 1711036800, // 발급 시간 (issued at) │
│ "exp": 1711040400 // 만료 시간 (expiration) │
│ } │
│ │
│ Signature (서명): │
│ HMACSHA256( │
│ base64UrlEncode(header) + "." + base64UrlEncode(payload), │
│ secretKey // 서버만 아는 비밀 키 │
│ ) │
│ │
│ ★ Payload는 Base64 인코딩일 뿐, 암호화가 아님! │
│ → 누구나 디코딩해서 내용을 볼 수 있음 │
│ → 민감한 정보(비밀번호 등)를 넣으면 안 됨! │
│ → Signature로 "위변조 여부"만 검증 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
4.2 동작 원리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| ┌─────────────────────────────────────────────────────────────────────┐
│ JWT 인증 흐름 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │── POST /login ──────────────────→ │ │
│ │ (ID, PW) │ ① 인증 확인 │
│ │ │ ② JWT 생성 (서명 포함) │
│ │ │ 서버에 저장 X! │
│ │←── { accessToken: "eyJ..." } ──── │ ③ 토큰 응답 │
│ │ │ │
│ │ 클라이언트가 토큰 저장 │
│ │ (localStorage, 메모리, 쿠키 등) │
│ │ │ │
│ │── GET /orders ──────────────────→ │ │
│ │ Authorization: Bearer eyJ... │ ④ 토큰 검증 │
│ │ │ - 서명 유효한지 │
│ │ │ - 만료되지 않았는지 │
│ │ │ - Payload에서 사용자 추출│
│ │←── 주문 내역 ──────────────────── │ │
│ │
│ 핵심: 서버가 상태를 저장하지 않음 (Stateless) │
│ 토큰 자체에 사용자 정보가 들어있고, 서명으로 위변조를 검증 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
4.3 JWT의 장단점
1
2
3
4
5
6
7
8
9
10
11
| 장점:
● Stateless → 서버 확장(Scale-out) 용이
● 서버 메모리/DB 조회 불필요 (토큰 자체로 인증)
● 모바일, SPA, MSA 등 다양한 환경에 적합
● 서버 간 인증 정보 공유 쉬움 (같은 secretKey면 어떤 서버든 검증 가능)
단점:
● 토큰 탈취 시 만료 전까지 막을 수 없음 (서버에서 무효화 불가)
● Payload가 커지면 네트워크 부담 (매 요청마다 전송)
● 토큰 자체에 정보가 있어 변경 불가 (사용자 권한 변경 시 기존 토큰 무효)
● 강제 로그아웃이 어려움
|
5. 쿠키 vs 세션 vs JWT 비교
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ┌─────────────────────────────────────────────────────────────────────┐
│ 쿠키 vs 세션 vs JWT │
├──────────────┬────────────┬────────────┬────────────────────────────┤
│ │ 쿠키 │ 세션 │ JWT │
├──────────────┼────────────┼────────────┼────────────────────────────┤
│ 저장 위치 │ 클라이언트 │ 서버 │ 클라이언트 │
│ 상태 관리 │ Stateless │ Stateful │ Stateless │
│ 보안 │ 낮음 │ 높음 │ 중간 │
│ │ (변조 가능)│ (서버 관리)│ (탈취 시 위험) │
│ 서버 부담 │ 없음 │ 있음 │ 없음 │
│ │ │ (메모리) │ │
│ 확장성 │ 좋음 │ 나쁨 │ 좋음 │
│ │ │ (세션 공유)│ │
│ 강제 만료 │ 불가 │ 가능 │ 기본적으로 불가 │
│ 용도 │ 설정 저장 │ 전통 웹 │ REST API, SPA, 모바일 │
│ │ 장바구니 │ 서버 렌더링│ MSA │
└──────────────┴────────────┴────────────┴────────────────────────────┘
|
6. Access Token과 Refresh Token
6.1 왜 토큰을 두 개 쓰는가?
1
2
3
4
5
| 문제: Access Token의 딜레마
● 유효 시간을 길게 → 탈취 시 오래 악용됨
● 유효 시간을 짧게 → 자주 재로그인해야 함 (UX 나쁨)
해결: Access Token(짧은 수명) + Refresh Token(긴 수명)
|
6.2 동작 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| ┌─────────────────────────────────────────────────────────────────────┐
│ Access Token + Refresh Token 흐름 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ① 로그인 │
│ Client → POST /login → Server │
│ Server → { accessToken (30분), refreshToken (14일) } │
│ │
│ ② API 요청 (accessToken 유효) │
│ Client → GET /api/orders (Authorization: Bearer {accessToken}) │
│ Server → 200 OK + 데이터 │
│ │
│ ③ accessToken 만료 │
│ Client → GET /api/orders (만료된 accessToken) │
│ Server → 401 Unauthorized │
│ │
│ ④ 토큰 재발급 │
│ Client → POST /auth/refresh (refreshToken 전송) │
│ Server → { 새 accessToken (30분) } │
│ refreshToken이 유효하면 재발급 │
│ │
│ ⑤ refreshToken도 만료되면 │
│ → 다시 로그인 필요 │
│ │
│ Access Token: 짧은 수명 (15분~1시간) │
│ Refresh Token: 긴 수명 (7일~30일), 서버 DB/Redis에 저장 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
6.3 Refresh Token은 어디에 저장하는가?
1
2
3
4
5
6
7
8
9
10
11
12
| Access Token:
● 메모리 (변수) — 가장 안전, 새로고침 시 사라짐
● localStorage — XSS에 취약
● httpOnly 쿠키 — XSS 방어, CSRF 주의
Refresh Token:
● httpOnly + Secure 쿠키 (권장)
● 서버 DB 또는 Redis에도 저장 (검증 + 강제 만료 위해)
실무 추천:
Access Token → httpOnly 쿠키 또는 메모리
Refresh Token → httpOnly + Secure 쿠키 + 서버 Redis에 저장
|
7. JWT의 장점과 한계
7.1 Stateless의 장점이 명확한 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ┌─────────────────────────────────────────────────────────────────────┐
│ JWT가 적합한 아키텍처 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ MSA (Microservices Architecture): │
│ API Gateway → User Service │
│ → Order Service │
│ → Payment Service │
│ → 각 서비스가 독립적으로 JWT 검증 가능 │
│ → 서비스 간 세션 공유 불필요 │
│ │
│ 모바일 + 웹 동시 지원: │
│ 모바일 앱: 쿠키 사용 어려움 → JWT를 헤더로 전송 │
│ 웹 SPA: API 서버와 분리 → JWT로 인증 │
│ │
│ 서버리스 / CDN: │
│ 서버가 상태를 가질 수 없는 환경 → JWT 필수 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
7.2 JWT의 한계
1
2
3
4
5
6
7
8
9
10
11
12
13
| 1. 강제 로그아웃이 안 됨
세션: 서버에서 세션 삭제하면 끝
JWT: 발급된 토큰은 만료 전까지 유효
→ 해결: 블랙리스트 (Redis에 무효화된 토큰 저장)
2. 토큰 탈취 시 위험
세션: 세션 ID 탈취해도 서버에서 무효화 가능
JWT: 탈취된 토큰으로 만료 전까지 API 호출 가능
→ 해결: Access Token 수명을 짧게 (15분~30분)
3. Payload 변경 불가
사용자 권한이 변경되어도 기존 토큰의 Payload는 그대로
→ 해결: 토큰 재발급 또는 짧은 만료 시간
|
8. 로그아웃/강제 만료 처리
8.1 세션 방식
1
2
3
4
5
6
7
8
9
| // 간단! 서버에서 세션 삭제하면 끝
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate(); // 세션 무효화
}
return ResponseEntity.ok().build();
}
|
8.2 JWT 방식 — 블랙리스트 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| @Service
@RequiredArgsConstructor
public class AuthService {
private final RedisTemplate<String, String> redisTemplate;
// 로그아웃: 토큰을 블랙리스트에 추가
public void logout(String accessToken) {
long expiration = tokenProvider.getExpiration(accessToken);
long ttl = expiration - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue()
.set("blacklist:" + accessToken, "logout", Duration.ofMillis(ttl));
}
}
// 토큰 검증 시 블랙리스트 확인
public boolean isValid(String accessToken) {
if (redisTemplate.hasKey("blacklist:" + accessToken)) {
return false; // 블랙리스트에 있으면 무효
}
return tokenProvider.validate(accessToken);
}
}
|
8.3 Refresh Token Rotation
1
2
3
4
5
6
7
8
| 보안 강화: Refresh Token을 사용할 때마다 새로 발급
① Client: refreshToken으로 재발급 요청
② Server: 새 accessToken + 새 refreshToken 발급
기존 refreshToken 무효화
③ 만약 이미 무효화된 refreshToken으로 요청이 오면?
→ 탈취된 것으로 간주
→ 해당 사용자의 모든 Refresh Token 무효화 (강제 재로그인)
|
9. 신입 면접에서 자주 나오는 비교 포인트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ┌─────────────────────────────────────────────────────────────────────┐
│ 면접 핵심 비교 포인트 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ "쿠키와 세션의 차이" │
│ → 쿠키: 클라이언트 저장, 세션: 서버 저장 │
│ → 세션도 세션 ID를 쿠키에 담아 전송 (쿠키가 운반 수단) │
│ │
│ "세션과 JWT의 차이" │
│ → 세션: Stateful (서버에 상태), JWT: Stateless (토큰에 정보) │
│ → 세션: 강제 만료 쉬움, JWT: 강제 만료 어려움 │
│ → 세션: Scale-out 어려움, JWT: Scale-out 쉬움 │
│ │
│ "JWT를 쓰는 이유" │
│ → 서버 확장 시 세션 공유 문제 없음 │
│ → 모바일·SPA·MSA 환경에 적합 │
│ → 서버 메모리 부담 없음 │
│ │
│ "JWT의 단점" │
│ → 토큰 탈취 시 만료 전까지 차단 불가 │
│ → Payload 크기만큼 네트워크 부담 │
│ → 강제 로그아웃을 위해 결국 블랙리스트(Redis) 필요 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
면접 예상 질문 & 답변
Q1. 쿠키와 세션의 차이를 설명해주세요.
쿠키는 클라이언트(브라우저)에 데이터를 저장하고, 같은 도메인으로 요청할 때 자동으로 전송됩니다. 약 4KB 제한이 있고, 클라이언트에서 변조할 수 있어 민감한 정보를 직접 저장하기에 부적합합니다.
세션은 서버에 데이터를 저장하고, 클라이언트에는 세션 ID만 쿠키로 전달합니다. 실제 사용자 정보는 서버에 있으므로 보안성이 높지만, 서버 메모리를 사용하고 Scale-out 시 세션 공유 문제가 생깁니다.
핵심 차이는 데이터 저장 위치(클라이언트 vs 서버)와 보안성(낮음 vs 높음)입니다.
Q2. JWT란 무엇이고, 어떻게 동작하나요?
JWT는 JSON Web Token으로, Header·Payload·Signature 세 부분을 .으로 이어붙인 문자열입니다. Payload에 사용자 정보(ID, 권한 등)가 담기고, Signature는 서버의 비밀 키로 서명하여 위변조를 방지합니다.
로그인 시 서버가 JWT를 발급하고, 클라이언트가 이후 요청마다 Authorization: Bearer {토큰} 헤더에 담아 보냅니다. 서버는 Signature를 검증하고 Payload에서 사용자 정보를 추출합니다. 서버에 상태를 저장하지 않으므로 Stateless합니다.
Q3. 세션 대신 JWT를 쓰는 이유는?
가장 큰 이유는 서버 확장성입니다. 세션은 서버에 상태를 저장하므로 Scale-out 시 세션 공유 문제(Sticky Session, Redis 등)를 해결해야 합니다. JWT는 토큰 자체에 정보가 담겨 있어 어떤 서버에서든 검증할 수 있습니다.
또한 모바일 앱, SPA, MSA 환경에서 다양한 클라이언트와 서비스 간 인증을 유연하게 처리할 수 있습니다.
Q4. JWT의 단점은 무엇인가요?
첫째, 토큰이 탈취되면 만료 전까지 차단할 수 없습니다. 서버에 상태가 없으므로 “이 토큰을 무효화해라”가 기본적으로 불가능합니다. 블랙리스트(Redis)를 도입하면 해결되지만, Stateless의 장점이 희석됩니다.
둘째, Payload는 암호화가 아닌 인코딩이므로 누구나 디코딩할 수 있습니다. 민감한 정보를 넣으면 안 됩니다.
셋째, 토큰 크기만큼 매 요청마다 네트워크 부담이 생깁니다.
Q5. Access Token과 Refresh Token은 왜 나누나요?
Access Token의 수명을 짧게 하면 탈취 피해를 줄일 수 있지만, 자주 재로그인해야 합니다. Refresh Token은 Access Token이 만료되었을 때 재로그인 없이 새 Access Token을 발급받기 위한 용도입니다.
Access Token은 15분~1시간으로 짧게, Refresh Token은 7~30일로 길게 설정합니다. Refresh Token은 서버 DB나 Redis에도 저장하여 강제 무효화가 가능하도록 합니다.
Q6. JWT 로그아웃은 어떻게 구현하나요?
JWT는 서버에서 발급한 토큰을 회수할 수 없으므로, 블랙리스트 방식을 사용합니다. 로그아웃 시 해당 Access Token을 Redis에 남은 만료 시간만큼 TTL로 저장합니다. 이후 요청이 오면 블랙리스트에 있는지 먼저 확인하고, 있으면 인증을 거부합니다.
Refresh Token은 서버 DB/Redis에서 삭제하여 재발급을 차단합니다.
Q7. JWT의 Payload는 암호화되나요?
아닙니다. JWT의 Payload는 Base64 인코딩일 뿐 암호화가 아닙니다. 누구나 디코딩해서 내용을 확인할 수 있습니다. 따라서 비밀번호, 개인정보 같은 민감한 데이터를 Payload에 넣으면 안 됩니다.
Signature는 위변조 방지 용도입니다. Payload를 수정하면 Signature가 일치하지 않아 검증에 실패합니다. 내용을 볼 수 있지만 수정은 불가능한 구조입니다.
마무리
HTTP의 Stateless 한계를 극복하는 세 가지 방법을 정리하면:
- 쿠키: 클라이언트에 직접 데이터 저장, 보안 약함
- 세션: 서버에 상태 저장 + 세션 ID를 쿠키로 전달, 보안 강함, 확장 어려움
- JWT: 토큰 자체에 정보, Stateless, 확장 쉬움, 강제 만료 어려움
선택 기준은 명확하다: 전통적인 서버 렌더링 웹이면 세션, REST API + SPA/모바일/MSA면 JWT. 그리고 JWT를 쓴다면 Access Token + Refresh Token 구조는 거의 필수다.