TECH/웹개발

[Java/Spring] 현대 인증의 표준, JWT(JSON Web Token) 완벽 가이드

런코리치 2026. 3. 17. 08:00

지난 포스팅에서 다룬 세션(Session) 방식은 서버가 사용자의 상태를 기억해야 하는 'Stateful'한 특성이 있었습니다. 하지만 서버가 늘어나고 마이크로서비스(MSA)가 대세가 되면서, 서버가 상태를 저장하지 않고도 인증을 수행할 수 있는 JWT(JSON Web Token)가 현대 인증의 표준으로 자리 잡았습니다.

오늘은 JWT가 무엇인지, 왜 쓰는지, 그리고 어떻게 구현하는지 아주 상세하게 파헤쳐 보겠습니다.


1. JWT란 무엇인가?

JWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다.

  • Stateless(무상태): 서버는 토큰을 발행만 할 뿐, 서버 메모리에 저장하지 않습니다.
  • Self-Contained(자가 수용적): 토큰 자체가 사용자의 정보와 유효성 검증을 위한 서명을 모두 포함하고 있습니다.

2. JWT의 3단계 구조

JWT는 점(.)을 구분자로 하여 세 부분으로 나뉩니다.

  1. Header (헤더): 토큰의 타입(JWT)과 알고리즘(HS256 등)을 담고 있습니다.
  2. Payload (페이로드): 실제 전달할 데이터(Claim)가 들어있습니다. 유저의 ID, 이름, 권한, 토큰 만료 시간 등을 넣습니다. (⚠️ 비밀번호 같은 민감 정보는 금지!)
  3. Signature (서명): 헤더와 페이로드를 서버만 알고 있는 Secret Key로 암호화한 값입니다. 토큰이 중간에 변조되지 않았음을 증명합니다.

3. 왜 세션 대신 JWT를 쓸까?

  • 서버 확장성: 서버가 100대로 늘어나도 세션을 공유할 필요가 없습니다. 각 서버가 Secret Key만 가지고 있다면 어디서든 검증이 가능합니다.
  • 모바일 환경 최적화: 쿠키를 관리하기 어려운 모바일 앱에서도 HTTP 헤더에 담아 간편하게 인증할 수 있습니다.
  • CORS 문제 해결: 세션/쿠키 방식은 여러 도메인 간의 인증이 복잡하지만, JWT는 헤더에 담기만 하면 되므로 자유롭습니다.

4. Spring Boot 실전 구현 (Step-by-Step)

실무에서 바로 활용할 수 있는 JWT 유틸리티 클래스 예시입니다. (jjwt 라이브러리 기준)

✅ 의존성 추가 (build.gradle)

Gradle
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

✅ JWT 생성 및 검증 유틸 (JwtTokenProvider.java)

Java
@Component
public class JwtTokenProvider {
    private final String secretKey = "your-very-long-and-secure-secret-key-at-least-32-chars";
    private final long tokenValidityInMilliseconds = 3600000; // 1시간

    private Key key;

    @PostConstruct
    protected void init() {
        this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    // 1. 토큰 생성
    public String createToken(String userPk, String role) {
        Claims claims = Jwts.claims().setSubject(userPk);
        claims.put("role", role);
        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenValidityInMilliseconds))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    // 2. 토큰 유효성 검증
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    // 3. 토큰에서 유저 정보 추출
    public String getUserPk(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject();
    }
}

5. 실무의 핵심: Access Token & Refresh Token

JWT의 최대 단점은 "한번 발행된 토큰은 만료될 때까지 제어할 수 없다"는 점입니다. 이를 보완하기 위해 2-Token 전략을 사용합니다.

  1. Access Token: 유효 기간을 매우 짧게(예: 30분) 설정하여 보안성을 높입니다.
  2. Refresh Token: 유효 기간을 길게(예: 2주) 설정하고 서버(DB 또는 Redis)에 저장합니다. Access Token이 만료되었을 때 새로운 Access Token을 발급받는 용도로만 사용합니다.

6. 보안을 위한 체크리스트 (⚠️ 필독)

  1. Payload에 민감 정보 넣지 않기: 페이로드는 누구나 Base64 디코딩을 통해 내용을 볼 수 있습니다. (주민번호, 비밀번호 절대 금지)
  2. Secret Key 보안: 소스코드에 직접 노출하지 말고 환경 변수나 설정 파일(.yml)로 관리하세요.
  3. HTTPS 사용: 토큰이 탈취되는 것을 막기 위해 반드시 HTTPS 환경에서 전송해야 합니다.
  4. 쿠키 사용 시 주의: 브라우저에 저장한다면 HttpOnly와 SameSite 설정을 통해 XSS 및 CSRF 공격을 방어하세요.

🏁 마무리하며: "언제 무엇을 써야 할까?"

  • 세션을 써야 할 때: 서버가 사용자의 상태를 완전히 통제해야 하는 경우 (예: 중복 로그인 차단, 즉각적인 로그아웃 제어).
  • JWT를 써야 할 때: 서비스가 거대해져 서버 확장이 빈번한 경우, 또는 다양한 플랫폼(웹, 모바일, 외부 연동)에 대응해야 하는 경우.

단순히 트렌드를 따르기보다 프로젝트의 규모와 요구사항에 맞는 인증 방식을 선택하는 혜안이 필요합니다. 이번 시리즈를 통해 여러분의 서비스에 가장 적합한 보안 전략을 세우시길 바랍니다!

항상 정해진 것만 하는 것이 아닌, 상황에 맞는 방식을 선택할 수 있도록 공부하면 좋겠습니다.