로그인 기능을 구현할 때 JWT를 쓸지 Session을 쓸지, 어떤 기준으로 결정하면 좋을까요?

공부하다 보니 "JWT vs Session" 논쟁은 양자택일이 아니라는 걸 알게 되었습니다. 2025-2026년 기준 업계 컨센서스는 둘 다 단독으로는 충분하지 않다 는 것이고, 각각의 약점을 보완하는 하이브리드 전략이 주류가 되고 있습니다.

인증 vs 인가

먼저 용어를 명확히 하겠습니다.

  • 인증(Authentication) — "누구인가?" 신원 확인. 로그인 과정
  • ** 인가(Authorization)** — "무엇을 할 수 있는가?" 권한 확인. 접근 제어
PLAINTEXT
사용자 → 로그인(인증) → 관리자 페이지 접근 시도 → 권한 확인(인가)

이 두 개념을 혼용하면 면접에서 감점 요소가 됩니다. 영어 약어로 AuthN(인증), AuthZ(인가)라고 구분하기도 합니다.

Session 기반 인증

동작 흐름

PLAINTEXT
1. 클라이언트 → 서버: 아이디/비밀번호 전송
2. 서버: 검증 후 세션 생성 (서버 메모리에 저장)
3. 서버 → 클라이언트: 세션 ID를 쿠키로 전달
4. 이후 요청마다 쿠키에 세션 ID 포함 → 서버가 세션 조회
JAVA
// Spring Security에서의 세션 기반 인증 설정
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .formLogin(form -> form
            .loginPage("/login")
            .defaultSuccessUrl("/dashboard"))
        .sessionManagement(session -> session
            .maximumSessions(1) // 동시 세션 1개 제한
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
        .build();
}

** 장점:**

  • 서버에서 세션을 즉시 무효화 가능 (강제 로그아웃)
  • 세션 ID 자체에는 사용자 정보가 없어 탈취 시 피해 범위 제한
  • 구현이 직관적

** 약점:**

  • Stateful — 서버가 상태를 저장해야 함
  • 서버 확장 시 세션 공유 문제 (Sticky Session 또는 외부 저장소 필요)
  • 서버 재시작 시 세션 유실 (메모리 저장 시)

JWT 기반 인증

JWT 구조

JWT는 세 부분으로 구성됩니다: Header.Payload.Signature

PLAINTEXT
eyJhbGciOiJIUzI1NiJ9.          ← Header (알고리즘, 타입)
eyJzdWIiOiJ1c2VyMSIsInJvbGUi.  ← Payload (클레임: 사용자 정보)
SflKxwRJSMeKKF2QT4fwpMeJf36P   ← Signature (서명: 위변조 방지)
  • Header — 서명 알고리즘 (HS256, RS256 등)
  • Payload — 사용자 정보(클레임). Base64 인코딩 (암호화 아님!)
  • Signature — Header + Payload를 비밀키로 서명

Payload는 Base64 디코딩하면 누구나 읽을 수 있습니다. 민감한 정보(비밀번호 등)를 절대 넣으면 안 됩니다.

동작 흐름

PLAINTEXT
1. 클라이언트 → 서버: 아이디/비밀번호 전송
2. 서버: 검증 후 JWT 생성 (비밀키로 서명)
3. 서버 → 클라이언트: JWT 반환
4. 이후 요청마다 Authorization 헤더에 JWT 포함
5. 서버: JWT 서명 검증 + 만료 시간 확인 → 인증 완료
JAVA
// JWT 생성 예시
public String createToken(String username, String role) {
    return Jwts.builder()
        .subject(username)
        .claim("role", role)
        .issuedAt(new Date())
        .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 15)) // 15분
        .signWith(secretKey)
        .compact();
}

장점:

  • Stateless — 서버가 상태를 저장하지 않음
  • 서버 확장이 쉬움 — 어떤 서버가 받아도 토큰 검증 가능
  • 마이크로서비스 간 인증 전달이 편리

** 약점:**

  • ** 발급 후 강제 무효화 불가** — 탈취 시 만료까지 악용 가능
  • 토큰 크기가 큼 — 매 요청마다 전송
  • Payload 노출 — Base64이므로 디코딩 가능

JWT의 치명적 약점

JWT의 가장 큰 문제는 ** 토큰 탈취 시 만료 전까지 무력화할 방법이 없다 **는 것입니다.

PLAINTEXT
공격자가 JWT 탈취 → 만료 시간까지 자유롭게 사용 → 서버에서 차단 불가

이를 보완하려면 결국 서버에 상태를 두어야 합니다:

  • 블랙리스트 (토큰 무효화 목록) → 그러면 Stateless 장점이 사라짐
  • 매우 짧은 유효 시간 → 사용자가 자주 재로그인해야 함

여기서 하이브리드 전략이 등장합니다.

하이브리드 전략 — 짧은 JWT + Redis Session

2025-2026년 업계에서 가장 권장되는 접근입니다.

핵심 아이디어

  • Access Token (JWT) — 유효 시간 5~15분. 빠른 Stateless 인증
  • Refresh Token — 서버(Redis)에 저장. 유효 시간 7~30일. Access Token 재발급용
  • ** 세션 정보** — Redis에 저장. 즉시 무효화 가능

동작 흐름

PLAINTEXT
1. 로그인 → Access Token(JWT, 15분) + Refresh Token(Redis, 7일) 발급
2. API 요청 → Access Token으로 빠른 인증 (DB 조회 없음)
3. Access Token 만료 → Refresh Token으로 새 Access Token 발급
4. 강제 로그아웃 → Redis에서 Refresh Token 삭제 → 재발급 불가
JAVA
// 하이브리드 인증 흐름 예시
@PostMapping("/auth/login")
public TokenResponse login(@RequestBody LoginRequest request) {
    // 사용자 검증
    User user = authService.authenticate(request);

    // 짧은 수명의 Access Token (JWT)
    String accessToken = jwtProvider.createAccessToken(user, Duration.ofMinutes(15));

    // 긴 수명의 Refresh Token (Redis에 저장)
    String refreshToken = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(
        "refresh:" + refreshToken,
        user.getId().toString(),
        Duration.ofDays(7)
    );

    return new TokenResponse(accessToken, refreshToken);
}

@PostMapping("/auth/refresh")
public TokenResponse refresh(@RequestBody RefreshRequest request) {
    // Redis에서 Refresh Token 검증
    String userId = redisTemplate.opsForValue()
        .get("refresh:" + request.getRefreshToken());

    if (userId == null) {
        throw new UnauthorizedException("유효하지 않은 Refresh Token");
    }

    // 새 Access Token 발급
    User user = userRepository.findById(Long.parseLong(userId)).orElseThrow();
    String newAccessToken = jwtProvider.createAccessToken(user, Duration.ofMinutes(15));

    return new TokenResponse(newAccessToken, request.getRefreshToken());
}

왜 이 조합이 효과적인가

문제해결 방식
JWT 탈취유효 시간 15분 → 피해 범위 최소화
JWT 강제 무효화Refresh Token을 Redis에서 삭제
Session 확장성대부분의 요청은 JWT로 Stateless 처리
사용자 경험Refresh Token으로 자동 재발급 → 재로그인 불필요

Refresh Token 보안

Refresh Token은 Access Token보다 더 중요합니다. 탈취되면 지속적으로 새 Access Token을 발급받을 수 있기 때문입니다.

보안 권장사항:

  • HttpOnly 쿠키 로 전달 — JavaScript에서 접근 불가
  • Secure 플래그 — HTTPS에서만 전송
  • Refresh Token Rotation — 사용할 때마다 새 토큰으로 교체
  • ** 사용 탐지** — 이미 사용된 Refresh Token이 다시 사용되면 모든 토큰 무효화
JAVA
// Refresh Token Rotation 예시
@PostMapping("/auth/refresh")
public TokenResponse refreshWithRotation(@RequestBody RefreshRequest request) {
    String key = "refresh:" + request.getRefreshToken();
    String userId = redisTemplate.opsForValue().get(key);

    if (userId == null) {
        throw new UnauthorizedException("유효하지 않은 Refresh Token");
    }

    // 기존 Refresh Token 삭제
    redisTemplate.delete(key);

    // 새 Refresh Token 발급
    String newRefreshToken = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(
        "refresh:" + newRefreshToken, userId, Duration.ofDays(7));

    User user = userRepository.findById(Long.parseLong(userId)).orElseThrow();
    String newAccessToken = jwtProvider.createAccessToken(user, Duration.ofMinutes(15));

    return new TokenResponse(newAccessToken, newRefreshToken);
}

OAuth 2.0과의 관계

OAuth 2.0은 ** 인가(Authorization) 프레임워크 **입니다. "이 앱이 내 구글 캘린더에 접근해도 되는가?"를 처리합니다.

  • OAuth 2.0 자체는 인증 프로토콜이 아님
  • OpenID Connect(OIDC)가 OAuth 2.0 위에 인증 계층을 추가한 것
  • "구글로 로그인"은 OAuth 2.0 + OIDC 조합

JWT는 OAuth 2.0에서 Access Token의 형식으로 자주 사용됩니다. 즉, JWT는 토큰 형식이고, OAuth 2.0은 토큰을 발급/사용하는 프로토콜입니다.

정리

  • ** 인증(누구인가)과 인가(뭘 할 수 있는가)는 다른 개념** — 면접에서 혼용하면 안 됨
  • Session은 즉시 무효화 가능하지만 Stateful, JWT는 Stateless지만 강제 무효화 어려움
  • ** 둘 다 단독으로는 충분하지 않음** — 하이브리드 전략이 2026년 기준 권장
  • 짧은 JWT(15분) + Redis Refresh Token(7일) 조합이 보안과 확장성을 모두 확보
  • Refresh Token은 HttpOnly 쿠키 + Rotation으로 보호
  • OAuth 2.0은 인가 프레임워크, JWT는 토큰 형식 — 레이어가 다름
댓글 로딩 중...