Errors

[Spring Security + JWT]: JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically.

neal89 2025. 5. 28. 09:22

[Spring Security + JWT] JWT 클레임 사용 시 타입 변환 오류 (JJWT) 해결하기: Enum 다루기

📌 요약

JWT(JSON Web Token)를 Spring Security 환경에서 사용할 때, 토큰에 저장된 Claim(페이로드) 값을 가져오는 과정에서 JJWT 라이브러리의 타입 변환 관련 오류(Cannot convert existing claim value of type 'class java.lang.String' to desired type 'class your.package.YourEnum')가 발생할 수 있습니다. 특히 Enum 타입을 클레임으로 사용하는 경우 이러한 문제가 빈번하게 나타납니다.

이 포스팅에서는 이 오류의 원인을 분석하고, JJWT의 Claim 직렬화/역직렬화 메커니즘을 이해하며, Enum 타입을 안전하고 올바르게 다루는 방법을 소개합니다.

❓ 무엇이 문제였을까? (JJWT 타입 변환 오류)

JWT는 Header, Payload(Claims), Signature 세 부분으로 구성됩니다. 우리가 주로 다루는 정보는 Payload 내의 Claims입니다. JWT를 생성할 때 애플리케이션의 사용자 역할(Role)이나 권한(Authorities) 같은 정보를 클레임으로 포함시키는 경우가 많습니다.

예를 들어, 사용자 역할을 UserRole이라는 Enum으로 정의하고 이 값을 JWT 클레임에 추가하는 코드는 다음과 같을 것입니다.

Java
 
// JWT 생성 시 (JWTUtil.java)
public String createJwt(String username, UserRole role, Long expirationMs) {
    // ...
    // 'role'은 UserRole Enum 객체이지만, .name()을 통해 String으로 저장됩니다.
    // (JJWT는 내부적으로 Jackson 같은 직렬화 라이브러리를 사용)
    return Jwts.builder()
               .claim("username", username)
               .claim("role", role.name()) // Enum의 이름을 String으로 저장
               .issuedAt(issuedAt)
               .expiration(expiration)
               .signWith(secretKey)
               .compact();
}

문제는 이렇게 저장된 JWT를 다시 파싱하여 클레임 값을 가져올 때 발생합니다.

Java
 
// JWT 파싱 시 (예상되는 오류 발생 코드)
public UserRole getRole(String token) {
    // JJWT는 기본적으로 String 값을 UserRole Enum으로 자동 변환해주지 않습니다.
    // "role" 클레임은 토큰에 String ("ROLE_ADMIN")으로 저장되어 있습니다.
    // 따라서 이 코드는 JJWT 변환 오류를 발생시킵니다.
    return Jwts.parserBuilder()
               .setSigningKey(secretKey)
               .build()
               .parseClaimsJws(token)
               .getBody()
               .get("role", UserRole.class); // ❌ 오류 발생 지점
}

발생하는 오류 메시지:

Error extracting role from token: Cannot convert existing claim value of type 'class java.lang.String' to desired type 'class teo.springjwt.user.UserRole'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more complex is expected to be already converted to your desired type by the JSON Deserializer implementation. ...

이 메시지는 명확합니다. "클레임 값은 String인데, UserRole 타입으로 변환하려고 하니 안 된다"는 의미입니다. JJWT가 String, Date, Long 등 기본적인 타입만 자동으로 변환해주며, UserRole과 같은 **복잡한 사용자 정의 타입(Enum, POJO)**은 자동 변환 대상이 아니라는 설명도 덧붙여줍니다.

💡 해결 방법: String으로 가져온 후 수동 변환

해결책은 간단합니다. JWT 클레임에서 role 값을 String 타입으로 먼저 가져온 다음, Java 코드 내에서 Enum.valueOf() 메서드를 사용하여 원하는 Enum 타입으로 수동으로 변환하는 것입니다.

JWTUtil.java (수정된 getRole 메서드):

Java
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException; // 예외 처리를 위해 필요
import teo.springjwt.user.UserRole; // UserRole Enum import

// ... (다른 코드 생략)

public UserRole getRole(String token) {
    try {
        // 1. JWT 클레임에서 'role' 값을 String 타입으로 가져옵니다.
        String roleString = extractClaim(token).get("role", String.class);

        // 2. String 값이 유효한지 검증합니다. (null이거나 비어있는지 등)
        if (roleString == null || roleString.isEmpty()) {
            throw new JwtException("JWT 'role' claim is missing or empty.");
        }

        // 3. String 값을 UserRole Enum으로 변환합니다.
        //    이때 Enum의 이름(예: "ROLE_ADMIN")과 정확히 일치해야 합니다.
        return UserRole.valueOf(roleString);
    } catch (IllegalArgumentException e) {
        // `roleString`이 `UserRole` Enum에 정의되지 않은 값일 경우 발생
        System.err.println("Invalid role string '" + roleString + "' in JWT: " + e.getMessage());
        throw new JwtException("Invalid role value found in JWT: " + roleString);
    } catch (JwtException e) {
        // JWT 파싱 중 발생하는 예외 (예: 토큰 만료, 서명 오류 등)
        throw e; // 이미 JwtException이므로 다시 던짐
    } catch (Exception e) {
        // 그 외 예상치 못한 예외
        System.err.println("Unexpected error extracting role from token: " + e.getMessage());
        throw new JwtException("Failed to extract role from token due to unexpected error: " + e.getMessage());
    }
}

// JWT 파싱 및 클레임 추출을 위한 내부 헬퍼 메서드
private Claims extractClaim(String token) {
    // 이 메서드도 JwtException을 던질 수 있습니다.
    return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload();
}

📌 핵심 정리

  1. JWT 생성 시 (Serialization):
    • Jwts.builder().claim("key", value) 형태로 클레임을 추가할 때, value 객체는 내부적으로 Jackson과 같은 JSON 직렬화 라이브러리에 의해 JSON String 형태로 변환되어 JWT 페이로드에 저장됩니다.
    • Collection 객체나 Enum 객체(role.name())도 JSON으로 직렬화 가능하면 문제없이 저장됩니다. 예를 들어 List<String>은 JSON 배열로, Enum은 해당 name() 메서드의 결과인 문자열로 저장됩니다.
  2. JWT 파싱 시 (Deserialization):
    • get("key", TargetType.class)를 사용하여 클레임을 가져올 때, JJWT는 TargetType.class에 해당하는 Java 객체로 JSON 값을 역직렬화하려고 시도합니다.
    • 하지만 JJWT는 String, Date, Long, Integer 등 기본 타입에 대해서만 자동 변환을 지원합니다.
    • **사용자 정의 타입(Enum, 커스텀 POJO)**이나 더 복잡한 구조의 Collection (예: List<YourCustomObject>)의 경우, 자동으로 역직렬화되지 않습니다. 이 경우 JJWT는 String 또는 더 일반적인 Object 타입으로 값을 가져온 다음, 개발자가 직접 원하는 타입으로 변환하는 로직을 구현해야 합니다.
    • Enum의 경우, String으로 클레임 값을 받아온 후 Enum.valueOf(String) 메서드를 사용하여 변환합니다. 이때, Enum.valueOf()는 해당 문자열과 정확히 일치하는 Enum 상수가 없을 경우 IllegalArgumentException을 발생시키므로, 적절한 예외 처리가 필수입니다.

이러한 이해를 바탕으로 JWT 클레임의 타입 변환 오류를 효과적으로 해결하고, 더 견고한 JWT 인증 시스템을 구축할 수 있습니다.