TechFeedTechFeed
Security

JWT vs 세션 인증 — 무엇을 선택할 것인가

JWT와 세션 방식의 구조, 장단점, 보안 고려사항을 비교하고 실무 선택 기준을 정리.

한 줄 요약: JWT는 무상태 확장에 유리하고, 세션은 즉시 무효화와 서버 제어에 유리하다. 어떤 서비스를 만드느냐에 따라 정답이 갈린다.

로그인 기능을 구현할 때 가장 먼저 마주치는 결정이 있다. JWT를 쓸 것인가, 세션을 쓸 것인가. 두 방식은 각자 장단점이 뚜렷하고, 잘못 선택하면 나중에 바꾸기 어렵다. 이 글에서는 구조적 차이, 보안 특성, 실무 선택 기준을 정리한다.

JWT 구조 — 토큰 안에 무엇이 들어있나

JWT(JSON Web Token)는 세 부분으로 구성된다. 각 부분은 Base64URL로 인코딩되어 점(.)으로 구분된다.

  • Header: 토큰 타입(JWT)과 서명 알고리즘 지정. 예: {"alg": "HS256", "typ": "JWT"}
  • Payload(Claims): 사용자 정보와 메타데이터. sub(사용자 ID), iat(발급 시각), exp(만료 시각) 등.
  • Signature: Header + Payload를 비밀 키로 서명한 값. 변조 탐지에 사용.

중요한 것은 JWT Payload는 암호화가 아니라 인코딩이라는 점이다. Base64URL 디코딩하면 내용이 그대로 보인다. 민감 정보(비밀번호, 카드번호 등)를 Payload에 넣어서는 안 된다.

JWT 생성 및 검증 예시 (Node.js / jsonwebtoken)
const jwt = require('jsonwebtoken'); const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET; const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // Access Token 생성 (15분) function generateAccessToken(userId, role) { return jwt.sign( { sub: userId, role }, ACCESS_SECRET, { algorithm: 'HS256', expiresIn: '15m' } ); } // Refresh Token 생성 (7일) function generateRefreshToken(userId) { return jwt.sign( { sub: userId }, REFRESH_SECRET, { algorithm: 'HS256', expiresIn: '7d' } ); } // 검증 미들웨어 function verifyAccessToken(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.slice(7); try { const payload = jwt.verify(token, ACCESS_SECRET, { algorithms: ['HS256'] }); req.user = payload; next(); } catch (err) { return res.status(401).json({ error: 'Invalid or expired token' }); } }

세션 인증 방식 — 서버가 상태를 기억한다

세션 방식은 서버가 로그인 상태를 직접 저장한다. 클라이언트에는 세션 ID만 전달되고, 실제 데이터는 서버(또는 Redis 같은 스토어)에 있다.

세션 동작 흐름

  1. 클라이언트가 로그인 요청 전송
  2. 서버가 자격 증명 확인 후 세션 생성 → Session Store(Redis/DB)에 저장
  3. 서버가 쿠키로 Session ID 전달 (httpOnly + Secure)
  4. 클라이언트는 이후 요청마다 쿠키를 자동 전송
  5. 서버가 Session ID로 Session Store 조회하여 유효성 확인

세션 저장소 선택

메모리 스토어는 서버 재시작 시 세션이 사라지고 멀티 인스턴스 환경에서 공유가 안 된다. 프로덕션에서는 Redis를 사용하는 것이 표준이다.

Express 세션 설정 (express-session + Redis)
const session = require('express-session'); const RedisStore = require('connect-redis').default; const { createClient } = require('redis'); const redisClient = createClient({ url: process.env.REDIS_URL }); await redisClient.connect(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, name: 'sessionId', // 기본 'connect.sid' 변경 — 기술 노출 방지 resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 1000 * 60 * 60 * 24 // 24시간 } })); // 로그인 처리 app.post('/auth/login', async (req, res) => { const user = await verifyCredentials(req.body); if (!user) return res.status(401).json({ error: 'Invalid credentials' }); req.session.userId = user.id; req.session.role = user.role; res.json({ message: 'Logged in' }); }); // 로그아웃 — 세션 즉시 무효화 app.post('/auth/logout', (req, res) => { req.session.destroy((err) => { res.clearCookie('sessionId'); res.json({ message: 'Logged out' }); }); });

JWT vs 세션 — 항목별 비교

항목JWT세션
상태 저장클라이언트 (Stateless)서버/Redis (Stateful)
수평 확장서버 간 공유 불필요 — 유리중앙 Redis 필요 — 추가 구성
즉시 무효화어렵다 (만료 전까지 유효)즉시 가능 (Session Store 삭제)
데이터 포함Payload에 클레임 포함 — DB 조회 줄임ID만 저장, 매 요청마다 DB/Redis 조회
토큰 크기200~500 bytes (클레임 양에 따라)20~30 bytes (ID만)
탈취 시 피해만료 전까지 사용 가능 — 위험서버에서 즉시 폐기 가능
브라우저 저장localStorage (XSS 취약) 또는 httpOnly 쿠키httpOnly 쿠키 (표준)
적합한 환경마이크로서비스, 모바일 앱, Public API전통적 웹앱, 관리자 패널
주의: JWT를 localStorage에 저장하면 XSS 공격으로 토큰이 탈취된다. JWT를 브라우저에서 사용한다면 httpOnly + Secure + SameSite=Strict 쿠키에 저장해야 한다. 이 경우 세션 쿠키와 보안 특성이 거의 같아지므로, '어차피 쿠키를 쓴다면 세션이 더 단순하다'는 논리가 성립한다.

보안 고려사항 — JWT의 함정

JWT는 구현하기 쉬워 보이지만 잘못 구현하면 세션보다 더 위험한 결과를 낳는다. 자주 발생하는 보안 실수를 정리한다.

알고리즘 혼동 공격 (Algorithm Confusion)

서버가 alg 헤더를 그대로 신뢰하면 공격자가 alg를 none으로 바꿔 서명 검증을 우회할 수 있다. 또는 RS256 서버에 HS256 토큰을 보내 공개키를 비밀키로 사용하도록 혼동시킬 수도 있다. 항상 algorithms 옵션을 명시적으로 지정한다.

Refresh Token 탈취

Refresh Token은 수명이 길어 탈취되면 피해가 크다. Refresh Token Rotation 패턴을 적용해야 한다. Refresh Token을 사용할 때마다 새 Refresh Token을 발급하고 기존 것은 폐기한다. 이미 사용된 Refresh Token이 다시 들어오면 해당 계정의 모든 세션을 강제 로그아웃시킨다.

JWT 블랙리스트의 한계

JWT 즉시 무효화를 위해 블랙리스트를 구현하는 경우가 있다. 그러나 이는 JWT의 Stateless 장점을 완전히 버리는 것과 같다. 블랙리스트가 필요하다면 Access Token 수명을 짧게(5~15분) 유지하고, 만료를 통한 자연 무효화에 의존하는 것이 더 실용적이다.

팁: Refresh Token은 반드시 httpOnly 쿠키에 저장하고, Access Token만 메모리(변수)에 보관하는 패턴을 사용한다. 이렇게 하면 XSS로는 Refresh Token을 탈취할 수 없고, CSRF로는 Access Token을 쓸 수 없다. 두 공격 벡터를 동시에 차단하는 가장 실용적인 구성이다.

실무 선택 기준 — 무엇을 써야 하나

상황별로 권장 방식이 다르다. 다음 기준으로 판단한다.

JWT를 선택해야 하는 경우

  • 마이크로서비스 환경 — 여러 서비스가 동일한 토큰을 각자 검증
  • 모바일 앱 — 쿠키 관리가 불편하고, Authorization 헤더가 자연스러운 환경
  • 서드파티 API — 클라이언트가 다양하고 서버 세션 공유가 어려운 경우
  • 수평 확장이 잦은 환경 — 서버 인스턴스 간 세션 공유 부담 없음

세션을 선택해야 하는 경우

  • 즉시 로그아웃이 필수 — 의심 활동 감지 시 모든 세션 강제 종료
  • 관리자 패널 — 높은 권한 계정의 세션을 서버에서 직접 제어
  • 단일 서버 또는 소규모 서비스 — Redis를 추가하지 않아도 되는 단순한 구성
  • 규제 준수 — 세션 감사 로그, 동시 세션 수 제한이 요구되는 환경

하이브리드 패턴

실무에서는 두 방식을 조합하는 경우도 많다. Access Token(JWT, 단수명) + Refresh Token(DB/Redis 기반, 장수명) 패턴이 대표적이다. 빠른 요청 처리에는 JWT를 쓰고, 세션 관리와 즉시 무효화는 서버 사이드 Refresh Token 관리로 처리한다.

참고: 어떤 방식을 선택하든 HTTPS 없이는 모두 무의미하다. 쿠키의 Secure 플래그, HSTS 헤더, TLS 1.2 이상 설정은 인증 방식과 무관하게 항상 적용해야 한다.
JWT세션인증보안토큰

관련 포스트

인증 구현 가이드 2026 — JWT, OAuth, Passkey2026-02-20API 보안 체크리스트 20262026-03-06OWASP Top 10 2026 — 웹 보안 필수 체크리스트2026-02-18CORS 완벽 이해 — 왜 차단되고 어떻게 해결하나2026-03-08