웹 개발을 하면서 사용자 로그인을 처리해야 할 때 쿠키, 세션, 토큰이라는 세 가지 방식을 마주치게 된다. 다만 이 세 가지가 정확히 뭐가 다른지, 어떤 상황에서 뭘 써야 하는지 설명할 수 있는 개발자는 생각보다 적다. 많은 사람들이 그냥 "세션을 쓰면 된다더라" 또는 "요즘은 토큰을 쓴다더라" 정도만 알고 있을 뿐이다.
다만 각 방식의 원리를 이해하면, 프로젝트 특성에 맞는 최적의 인증 방식을 선택할 수 있다. 특히 모바일 앱, API 서버, 마이크로서비스 같은 최신 아키텍처에서는 올바른 선택이 매우 중요하다. 이번에는 쿠키, 세션, 토큰이 정확히 뭔지, 어떻게 작동하는지, 그리고 어떤 상황에서 뭘 써야 하는지 완벽하게 정리해서 소개하겠다.
먼저 쿠키, 세션, 토큰이 왜 필요한지 이해해야 한다.
HTTP는 기본적으로 "stateless"(상태를 유지하지 않음) 프로토콜이다. 즉, 요청(request)을 보낼 때마다 서버는 "이 사람이 누구인지" 알 수 없다.
사용자: "안녕, 나 홍길동이야"
서버: "반가워, 뭐 도와줄까?"
사용자: "내 정보 좀 봐"
서버: "어? 누구신데요?" ← 이전 대화를 기억하지 못함
이 문제를 해결하기 위해 세 가지 방식이 나왔다: 쿠키, 세션, 토큰.
정의: 클라이언트(브라우저)에 저장되는 작은 데이터
쿠키는 서버가 사용자 정보를 담아 브라우저로 보내고, 브라우저는 그것을 저장했다가 매 요청마다 자동으로 함께 보낸다.
1. 사용자가 로그인
사용자 → 서버: "홍길동, 비번 1234"
서버: 검증 완료 → 쿠키 생성
서버 → 사용자: "로그인 성공! 쿠키: user_id=123"
(브라우저가 자동 저장)
2. 사용자가 다른 페이지 접속
사용자 → 서버: 자동으로 쿠키 포함해서 요청 (user_id=123)
서버: "아, 123번 사용자네!" 인증 완료
쿠키의 특징:
- 저장 위치: 클라이언트 (브라우저)
- 크기: 작음 (보통 4KB 이하)
- 유효기간: 설정 가능 (일정 시간 후 자동 삭제)
- 보안: 브라우저에 평문으로 저장되어 비교적 취약
- 전송 방식: 모든 HTTP 요청에 자동 포함
쿠키 설정 예제 (PHP):
<?php
// 로그인 성공 후 쿠키 설정
setcookie('user_id', '123', time() + (86400 * 30)); // 30일 유효
setcookie('username', '홍길동', time() + (86400 * 30));
// 쿠키 읽기
echo $_COOKIE['user_id']; // 123
echo $_COOKIE['username']; // 홍길동
// 쿠키 삭제 (유효기간을 과거로 설정)
setcookie('user_id', '', time() - 3600);
?>
쿠키 설정 예제 (JavaScript):
// 쿠키 설정
document.cookie = 'user_id=123; max-age=2592000; path=/'; // 30일
document.cookie = 'username=홍길동; max-age=2592000; path=/';
// 쿠키 읽기
var cookies = document.cookie;
console.log(cookies); // "user_id=123; username=홍길동"
// 쿠키 삭제
document.cookie = 'user_id=; max-age=0; path=/';
쿠키의 문제점:
- XSS(Cross-Site Scripting) 공격: JavaScript로 쿠키를 탈취할 수 있음
- 모든 요청에 포함: 불필요한 데이터 전송으로 성능 저하
- 크기 제한: 저장할 데이터가 크면 문제
- 크로스 도메인: 다른 도메인간 공유 불가능
정의: 서버에 저장되는 사용자 정보
세션은 쿠키와 반대 개념이다. 사용자 데이터는 서버에 저장하고, 브라우저는 "세션 ID"라는 식별자만 쿠키로 받는다.
1. 사용자가 로그인
사용자 → 서버: "홍길동, 비번 1234"
서버: 검증 완료 → 세션 생성
서버의 메모리/파일: session_abc123 = {user_id: 123, name: '홍길동'}
서버 → 사용자: 쿠키 설정 (PHPSESSID=abc123)
2. 사용자가 다른 페이지 접속
사용자 → 서버: 자동으로 쿠키 포함 (PHPSESSID=abc123)
서버: 세션 테이블에서 abc123 조회 → 사용자 정보 확인
세션의 특징:
- 저장 위치: 서버 (메모리, 파일, 데이터베이스)
- 크기: 크기 제한 없음
- 보안: 서버에 저장되어 비교적 안전 (세션 ID만 노출)
- 유효기간: 서버에서 관리
- 서버 자원: 세션을 저장하기 위해 서버 메모리 필요
세션 사용 예제 (PHP):
<?php
// 세션 시작 (페이지 가장 위에 작성)
session_start();
// 로그인 성공 후 세션에 저장
$_SESSION['user_id'] = 123;
$_SESSION['username'] = '홍길동';
$_SESSION['login_time'] = time();
// 세션 읽기
echo $_SESSION['user_id']; // 123
echo $_SESSION['username']; // 홍길동
// 특정 세션 삭제
unset($_SESSION['user_id']);
// 세션 전체 삭제
session_destroy();
?>
세션 저장 방식:
// php.ini에서 설정
session.save_handler = files // 파일로 저장 (기본값)
// 또는
session.save_handler = redis // Redis에 저장 (성능 좋음)
session.save_handler = memcached // Memcached에 저장 (확장성 좋음)
세션의 문제점:
- 서버 메모리 사용: 사용자가 많아질수록 서버 부담 증가
- 확장성 문제: 여러 서버가 있을 때 세션 공유 복잡
- 모바일/API 부적합: 쿠키 기반이라 모바일 앱에서 사용 어려움
- CSRF 공격 가능: 쿠키 기반이라 악의적 요청이 자동으로 쿠키 포함
정의: 서명된 정보를 담은 문자열
토큰은 JWT(JSON Web Token) 같은 형태로, 사용자 정보를 암호화해서 클라이언트에 줄 뿐 서버는 보관하지 않는다.
1. 사용자가 로그인
사용자 → 서버: "홍길동, 비번 1234"
서버: 검증 완료 → 토큰 생성 및 서명
token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiLhhYDhhKDhhZXhhYEifQ...
서버 → 사용자: 토큰 반환 (저장하라고 알려줌)
2. 사용자가 API 요청
사용자 → 서버: HTTP Header에 토큰 포함
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
서버: 토큰 검증 (서명 확인) → 유효하면 요청 처리
토큰의 특징:
- 저장 위치: 클라이언트 (localStorage, 메모리, 쿠키 등)
- 서버 저장: 서버는 토큰을 저장하지 않음 (stateless)
- 보안: 토큰에 서명이 포함되어 위조 불가능
- 확장성: 여러 서버에서 같은 토큰 검증 가능
- 적용 범위: 웹, 모바일 앱, API 등 어디든 사용 가능
JWT 토큰 구조:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiLhhYDhhKDhhZXhhYEifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
↑ ↑ ↑
Header (알고리즘) Payload (데이터) Signature (서명)
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user_id":123,"name":"홍길동","exp":1642424400}
Signature: HMACSHA256(base64(header)+"."+base64(payload), secret_key)
토큰 생성 예제 (PHP):
<?php
// JWT 라이브러리 필요 (composer require firebase/jwt)
require 'vendor/autoload.php';
use FirebaseJWTJWT;
// 로그인 성공 후 토큰 생성
$key = 'your_secret_key';
$payload = array(
'user_id' => 123,
'username' => '홍길동',
'email' => 'hong@example.com',
'exp' => time() + 3600 // 1시간 유효
);
$token = JWT::encode($payload, $key, 'HS256');
echo json_encode(['token' => $token]);
?>
토큰 검증 예제 (PHP):
<?php
require 'vendor/autoload.php';
use FirebaseJWTJWT;
use FirebaseJWTKey;
// HTTP Header에서 토큰 추출
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
$token = str_replace('Bearer ', '', $authHeader);
try {
$key = 'your_secret_key';
$decoded = JWT::decode($token, new Key($key, 'HS256'));
echo '토큰 유효함';
echo $decoded->user_id; // 123
echo $decoded->username; // 홍길동
} catch(Exception $e) {
echo '토큰 유효하지 않음: ' . $e->getMessage();
}
?>
토큰 사용 예제 (JavaScript/AJAX):
// 1. 로그인 후 토큰 받기
$.post('/login', {username: 'hong', password: '1234'}, function(response) {
var token = response.token;
localStorage.setItem('auth_token', token); // 로컬 스토리지에 저장
});
// 2. API 요청 시 토큰 포함
var token = localStorage.getItem('auth_token');
$.ajax({
url: '/api/user/profile',
type: 'GET',
headers: {
'Authorization': 'Bearer ' + token
},
success: function(data) {
console.log(data);
}
});
// 3. 로그아웃 (토큰 삭제)
localStorage.removeItem('auth_token');
토큰의 문제점:
- 토큰 취소 어려움: 서버에 저장되지 않아 로그아웃 후에도 유효
- 토큰 탈취: localStorage에 저장되면 XSS 공격에 취약
- Refresh Token 필요: 보안상 만료 시간이 짧아 Refresh Token 필요
- 크기: 토큰에 정보가 많으면 크기가 커짐
| 특징 | 쿠키 | 세션 | 토큰 |
|---|---|---|---|
| 저장 위치 | 클라이언트 | 서버 | 클라이언트 |
| 서버 부담 | 적음 | 많음 | 적음 |
| 보안 | 낮음 | 높음 | 높음 |
| 확장성 | 나쁨 | 나쁨 | 우수 |
| 모바일 앱 | 불편 | 불편 | 적합 |
| API 서버 | 불적합 | 불적합 | 매우 적합 |
| 로그아웃 | 쉬움 | 쉬움 | 어려움 |
| CSRF 공격 | 취약 | 취약 | 안전 |
쿠키 사용:
- 간단한 웹사이트 (블로그, 정보 사이트)
- 사용자 선호도 저장 (언어, 테마 등)
- 실시간 성능이 중요하지 않은 경우
- 요즘은 거의 사용하지 않음
세션 사용:
- 전통적인 웹 애플리케이션 (ASP.NET, PHP, JSP)
- 단일 서버 환경
- 보안이 중요한 금융/관리 사이트
- 사용자 수가 많지 않은 서비스
토큰(JWT) 사용:
- REST API 서버
- 모바일 앱
- 마이크로서비스 아키텍처
- 다중 서버/클라우드 환경
- 요즘 대부분의 신규 프로젝트
쿠키 보안:
// HttpOnly: JavaScript에서 접근 불가 (XSS 공격 방지)
setcookie('user_id', '123', time() + 3600, '/', '', false, true);
// ↑
// HttpOnly = true
// Secure: HTTPS에서만 전송
setcookie('user_id', '123', time() + 3600, '/', '', true, true);
// ↑
// Secure = true
// SameSite: CSRF 공격 방지
header('Set-Cookie: user_id=123; SameSite=Strict');
토큰 보안:
- Refresh Token 사용 (Access Token은 1시간, Refresh Token은 7일)
- 토큰을 쿠키의 HttpOnly에 저장 (localStorage 대신)
- 토큰에 민감한 정보 포함 금지
- HTTPS 필수
- 서버에서 토큰 만료 시간 검증
세션 보안:
- 세션 ID를 안전한 난수로 생성
- 세션 타임아웃 설정
- https 필수
- 세션 파일 권한 제한 (644)
가장 안전한 방식은 토큰 + Refresh Token 조합이다:
// 1. 로그인 시 두 가지 토큰 발급
{
"access_token": "eyJhbGc...", // 1시간 유효
"refresh_token": "eyJhbGc...", // 7일 유효
"expires_in": 3600
}
// 2. API 요청: Access Token 사용
$.ajax({
headers: {
'Authorization': 'Bearer ' + access_token
}
});
// 3. Access Token 만료 시: Refresh Token으로 새로운 Access Token 발급
$.post('/token/refresh', {
refresh_token: refresh_token
}, function(response) {
access_token = response.access_token;
});
// 4. 로그아웃: Refresh Token 삭제 (서버에 블랙리스트 기록)
웹 인증은 보안, 성능, 확장성을 모두 고려해야 한다. 쿠키는 오래된 방식이지만 여전히 사용되고, 세션은 전통적인 웹에서 안전한 선택지이며, 토큰은 현대적인 API/모바일 환경에 가장 적합하다. 당신의 프로젝트 특성에 따라 세 가지 중 가장 적절한 방식을 선택하되, 보안을 절대 간과해서는 안 된다는 점을 잊지 말자.