세션 토큰 vs JWT: 잘못된 이분법
Source: Dev.to
“JWT‑only” 접근 방식
// User logs in
const jwt = sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '7d' }
);
setCookie('token', jwt);
// Every request
const payload = verify(req.cookies.token, SECRET);
// Done. No database hit.
장점
- 데이터베이스 쿼리 없음 – 모든 서버가 토큰을 검증할 수 있음.
- “무상태” 인증 (그게 무슨 의미인지 알 수 있든 말든).
단점
- 폐기 문제 – 사용자가 삭제되거나 해고되더라도, 해당 JWT는 최대 7 일 동안 유효합니다.
- 구식 역할 데이터 – 사용자의 역할을 변경해도 이미 발급된 토큰에는 영향을 주지 않습니다.
- ‘전역 로그아웃’ 없음 – 도난당한 노트북, 분실된 기기 등은 토큰이 만료될 때까지 계속 유효합니다.
결과: 요청당 약 0.97 ms의 빠른 속도와 초당 약 5,527 요청의 높은 처리량을 제공하지만, 토큰을 폐기할 수 없으며 데이터가 즉시 구식이 됩니다.
“세션‑전용” 접근 방식
// User logs in
const sessionId = randomBytes(32).toString('hex');
await db.session.create({
id: sessionId,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
setCookie('sessionId', sessionId);
// Every request – database lookup
const session = await db.session.findUnique({
where: { id: req.cookies.sessionId },
include: { user: true }
});
if (!session || session.expiresAt /* … */) {
// handle missing or expired session
}
Result: 안전하지만 느림; 데이터베이스가 제한 요소가 됩니다.
하이브리드 접근 방식이 효과적인 이유
하이브리드 방법은 액세스 토큰이 만료될 때만 스토리지를 접근합니다 (예: 전체 요청의 약 1 %). 나머지 99 %의 요청에 대해서는 네트워크 호출 없이 JWT‑수준의 속도를 얻을 수 있습니다.
Redis에 대한 간단한 참고
세션 스토리지를 PostgreSQL 대신 Redis로 교체하면 조회 시간이 ~2–3 ms로 감소하지만 모든 요청에 대해 여전히 외부 호출을 수행하게 됩니다. 하이브리드 접근 방식은 대부분의 트래픽에 대해 그 호출조차도 없애줍니다.
“마이크로서비스” 반론
“마이크로서비스는 데이터베이스를 공유하지 않으므로 JWT만이 토큰을 독립적으로 검증하는 유일한 방법이다.”
실제로 마이크로서비스는 이미 다음을 공유한다:
- 영속성을 위한 데이터베이스(또는 클러스터)
- 공유 상태를 위한 Redis(또는 다른 캐시)
- 메시지 큐, 로깅, 모니터링 등
이미 캐시용 Redis가 있다면 세션 검증은 비용이 적다. 하이브리드 접근 방식은 99 %의 요청에 대해 네트워크 왕복을 피함으로써 여전히 승리한다.
해결책: 단기간 접근 토큰 + 장기간 리프레시 토큰
JWT와 세션 중 하나를 고르는 문제가 아니라, JWT + 세션을 결합해 각각의 장점을 살리는 방식입니다.
작동 방식
// User logs in
async function login(email, password) {
const user = await authenticateUser(email, password);
// Refresh token – stored in DB (valid for 30 days)
const refreshToken = randomBytes(32).toString('hex');
await db.session.create({
userId: user.id,
refreshToken,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
});
// Access token – NOT stored, just a JWT (valid for 15 minutes)
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '15m' }
);
// Send tokens to the client
res.cookie('accessToken', accessToken, { httpOnly: true });
res.cookie('refreshToken', refreshToken, { httpOnly: true });
}
- 접근 토큰 (JWT) – 짧은 수명, 로컬에서 검증, DB/Redis 조회 없음.
- 리프레시 토큰 (세션) – 긴 수명, DB/Redis에 저장, 접근 토큰이 만료되었을 때 또는 사용자가 명시적으로 로그아웃할 때만 사용.
리프레시 흐름 (단순화)
// Refresh endpoint
async function refresh(req, res) {
const { refreshToken } = req.cookies;
const session = await db.session.findUnique({
where: { refreshToken },
include: { user: true }
});
if (!session || session.expiresAt /* … */) {
// handle missing or expired refresh token
return;
}
// Issue a new short‑lived access token
const newAccessToken = jwt.sign(
{ userId: session.user.id, role: session.user.role },
SECRET,
{ expiresIn: '15m' }
);
res.cookie('accessToken', newAccessToken, { httpOnly: true });
}
핵심 요점: 세션만 사용하면 인증 시스템이 DB 병목이 됩니다. 하이브리드 방식은 JWT의 빠른 검증 속도를 유지하면서 세션 방식의 제어 기능을 추가합니다.
전체 벤치마크 결과는 stat-tests/RESULTS.md에 있으며, 테스트 코드는 stat-tests/test-three-auth-strategies에서 확인할 수 있습니다.
Recommendations
-
Use the hybrid approach for virtually everything. It gives you JWT‑level speed with session‑level security.
→ 하이브리드 접근법을 거의 모든 경우에 사용하세요. JWT 수준의 속도와 세션 수준의 보안을 제공합니다. -
JWT‑only only when tokens are extremely short‑lived (< 5 min) and you truly don’t care about revocation (rare).
→ 토큰이 매우 짧은 기간(< 5 분)만 유지되고, 실제로 폐기(리보케이션)를 신경 쓰지 않을 때만 JWT‑only를 사용하세요(드물게). -
Session‑only only for tiny apps (< 10 req/sec) where simplicity outweighs performance concerns.
→ 단순함이 성능 우려보다 중요한 작은 앱(< 10 req/sec)일 때만 Session‑only를 사용하세요.
You don’t have to pick sides. Get the speed of JWTs and the control of sessions—just think about the trade‑offs instead of cargo‑culting a single approach.
→ 어느 쪽을 고집할 필요는 없습니다. JWT의 속도와 세션의 제어를 모두 얻으세요—단일 접근법을 맹목적으로 따르기보다 트레이드오프를 고민하세요.
Happy hacking!
행복한 해킹 되세요!