프로그래밍/디자인패턴

인증 상태 관리 패턴: 세션(Session) vs 토큰(JWT) 완벽 비교

Jinwookoh 2025. 12. 7. 21:09

"사용자가 아이디/비번을 입력하고 로그인에 성공했습니다. 그런데 페이지를 이동할 때마다 아이디/비번을 계속 물어볼 순 없습니다. 이 사용자가 아까 그 사용자라는 것을 어떻게 기억해야 할까요?"

HTTP는 상태가 없는(Stateless) 프로토콜입니다. 따라서 우리는 상태(State)를 유지하기 위한 기술이 필요합니다. 이때 "서버가 기억하느냐(Stateful)", **"클라이언트가 들고 다니느냐(Stateless)"**에 따라 시스템 구조가 완전히 달라집니다.


1. 방식 1: 세션 기반 인증 (Session-based Auth) - "서버의 장부"

전통적인 방식입니다. 서버가 메모리(또는 DB)에 **"누가 로그인했는지"**를 적어두고, 클라이언트에게는 그 장부의 **식별 번호(Session ID)**만 줍니다.

동작 순서

  1. 로그인: 사용자가 정보를 보냅니다.
  2. 생성: 서버는 JSESSIONID 같은 고유 ID를 생성하고, 서버 메모리(장부)에 {ID: "UserA", Role: "Admin"} 정보를 저장합니다.
  3. 발급: 클라이언트에게 Set-Cookie: JSESSIONID=123 헤더를 통해 ID를 줍니다.
  4. 인증: 클라이언트는 쿠키를 통해 ID를 보냅니다. 서버는 장부를 뒤져서 "아, 너 123번, UserA구나"라고 확인합니다.

코드 예시 (Spring Security)

Java
// 세션 방식은 별도 설정이 없으면 스프링 시큐리티의 기본값입니다.
// SecurityContextHolder가 세션(HttpSession)에 저장됩니다.

@GetMapping("/dashboard")
public String dashboard(HttpSession session) {
    // 서버 메모리에 있는 세션 저장소에서 값을 꺼내옴
    String user = (String) session.getAttribute("USER_ID");
    if (user == null) {
        return "redirect:/login";
    }
    return "Welcome " + user;
}

장단점

  • 장점:
    • 제어권(Control): 서버가 모든 정보를 쥐고 있습니다. 의심스러운 사용자가 있으면 서버에서 해당 세션을 지워버리면 즉시 강제 로그아웃(Kick out) 시킬 수 있습니다.
    • 보안: 클라이언트는 의미 없는 ID 문자열만 가지고 있으므로, 정보가 탈취되어도(세션 하이재킹 제외) 개인정보 자체는 안전합니다.
  • 단점:
    • 확장성(Scalability): 서버가 1대면 괜찮지만, 10대로 늘어나면 문제입니다. A 서버에 로그인했는데 B 서버로 요청이 가면, B 서버는 장부에 없는 사람이라며 튕겨냅니다. (이를 해결하려면 Sticky Session이나 Redis Session Cluster가 필요함)
    • 메모리 부하: 동시 접속자가 100만 명이면 서버 메모리가 터집니다.

2. 방식 2: 토큰 기반 인증 (Token-based Auth / JWT) - "디지털 서명된 출입증"

모던 웹/앱, MSA 환경의 표준입니다. 서버는 아무것도 기억하지 않습니다. 대신 사용자에게 위변조가 불가능한 **출입증(Access Token)**을 발급해 주고, "네가 알아서 들고 다녀"라고 합니다.

동작 순서

  1. 로그인: 사용자가 정보를 보냅니다.
  2. 발급: 서버는 {id: "UserA", role: "Admin"} 정보를 JSON으로 만들고, 서버만 아는 **비밀키로 서명(Signature)**하여 **JWT(JSON Web Token)**를 만들어 줍니다. (서버에는 저장 안 함!)
  3. 저장: 클라이언트는 이 토큰을 로컬 스토리지나 쿠키에 저장합니다.
  4. 인증: 요청할 때마다 헤더(Authorization: Bearer <Token>)에 토큰을 실어 보냅니다. 서버는 서명이 맞는지 수학적으로 검증만 합니다.

코드 예시 (JWT 파싱)

Java
public boolean validateToken(String token) {
    try {
        // DB 조회 없이, 서명(Signature)만 수학적으로 검증하면 끝
        // 서버는 Stateless 상태를 유지할 수 있음
        Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token);
        return true;
    } catch (JwtException e) {
        return false; // 위변조되었거나 만료됨
    }
}

장단점

  • 장점:
    • 확장성(Scalability): 서버가 상태를 저장하지 않으므로(Stateless), 서버를 무한대로 늘려도 상관없습니다. 트래픽 분산에 최적입니다.
    • 멀티 디바이스: 모바일 앱, 웹 등 다양한 클라이언트에서 공통으로 쓰기 편합니다.
  • 단점:
    • 제어권 상실: 한 번 발급된 토큰은 유효기간이 끝날 때까지 서버가 강제로 회수할 수 없습니다. (도난당하면 끝)
    • 트래픽 증가: 토큰 자체에 정보가 담겨 있어 세션 ID보다 길이가 깁니다. 매 요청마다 데이터 전송량이 늘어납니다.

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Session (Stateful) JWT / Token (Stateless)
저장 위치 서버 (메모리/DB) 클라이언트 (헤더/스토리지)
확장성 낮음 (Redis 등 별도 저장소 필요) 높음 (서버 증설 용이)
보안 제어 높음 (즉시 차단 가능) 낮음 (만료될 때까지 차단 어려움)
데이터 크기 작음 (ID만 전송) 큼 (정보+서명 전송)
추천 상황 관리자 페이지, 금융권, 동시 접속자 적은 PC 웹 대규모 B2C 서비스, 모바일 앱, 마이크로서비스(MSA)

결론 및 하이브리드 전략

"보안이 최우선이고 접속자가 제한적이다"라면 세션 방식이 가장 안전하고 구현도 쉽습니다.

하지만 **"대규모 트래픽과 모바일 앱을 지원해야 한다"**면 JWT가 필수입니다. 단, JWT의 "제어권 상실" 문제를 보완하기 위해 실무에서는 Refresh Token을 도입합니다.

  • Access Token (JWT): 유효기간 30분. (Stateless하게 검증)
  • Refresh Token (UUID): 유효기간 2주. DB에 저장. (토큰 갱신 요청이 오면 DB를 확인)

이렇게 하면 평소에는 JWT의 빠른 성능을 누리고, 토큰 갱신 시점에는 DB를 체크하여 강제 로그아웃 기능을 구현할 수 있습니다.