sggnology
하늘속에서IT
sggnology
전체 방문자
오늘
어제
  • 분류 전체보기 (83)
    • Algorithm (31)
      • Programmers (27)
      • Baekjoon (4)
    • WIKI (4)
      • VirtualBox (1)
      • Power Toys (1)
    • NodeJS (4)
      • nvm (1)
      • React (1)
      • Vue (1)
    • Dev Language (3)
      • Java (2)
      • Kotlin (1)
    • Spring Boot (17)
      • Gradle (1)
      • JPA (3)
    • DB (4)
      • MariaDB (3)
      • Redis (0)
    • Android (6)
      • Debug (3)
    • Nginx (3)
      • Debug (1)
    • Intellij (0)
    • Network (1)
    • Git (2)
      • GitHub (2)
    • Chrome Extension (0)
    • ETC (5)
      • Monitoring (2)
    • Linux (1)
      • WSL (1)
    • Visual Studio (1)
    • Side Project (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • docker
  • 프로그래머스
  • 레벨2
  • Android Studio
  • kotlin
  • java
  • spring boot
  • DB
  • 티스토리챌린지
  • 백준
  • JPA
  • 오블완
  • 고득점KIT
  • 안드로이드 스튜디오
  • mariadb
  • 연습문제
  • 고득점 Kit
  • 알고리즘
  • 레벨3
  • nginx

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
sggnology

하늘속에서IT

[Spring Boot] JWT 라이브러리를 의존성 추가했음에도 빌드 오류가 나는건 왜일까?(feat. `io.jsonwebtoken:jjwt-api`)
Spring Boot

[Spring Boot] JWT 라이브러리를 의존성 추가했음에도 빌드 오류가 나는건 왜일까?(feat. `io.jsonwebtoken:jjwt-api`)

2023. 5. 25. 22:00
728x90

발단

마지막 업데이트 시점이 2018년이다.

흔히 사용하는 jwt 라이브러리인 jjwt 를 사용하여 개발하려 하니 2018년도 이후로 업데이트 되지 않고 있었다. 예제로도 jjwt 를 사용하는 경우가 많지만 최신 패키지인 jjwt-api 가 있기에 이를 사용하여 공부하던 도중 문제가 발생한 의존성에 대해 트러블 슈팅 하는 과정을 작성하였다.

 

환경

  • Spring Boot: 3.1.0 (kotlin, gradle)
  • jjwt-api 0.11.5

 

jjwt 와 jjwt-api 의 상관관계

artifact 가 이동되었다는 maven 의 설명

 

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

  • maven 에서 제공하는 정보처럼 jjwt 와 jjwt-api 는 같은 내용이다.(버전에 따라 달라짐)
  • artifcat 만 새로 생성하여 따로 관리되어지고 있는 듯 하다.

 


 

문제

github 에서 제공하는 jjwt 문서를 제대로 읽지 않았기에 아래 두가지 문제가 발생하였다.

 

문제1. jjwt-impl 의존성 추가

// 에러 로그
io.jsonwebtoken.lang.UnknownClassException: Unable to load class named [io.jsonwebtoken.impl.DefaultJwtBuilder] from the thread context, current, or system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.  Have you remembered to include the jjwt-impl.jar in your runtime classpath?
at app//io.jsonwebtoken.lang.Classes.forName(Classes.java:93)
at app//io.jsonwebtoken.lang.Classes.newInstance(Classes.java:137)
at app//io.jsonwebtoken.Jwts.builder(Jwts.java:144)
...
  • `jjwt-impl` 의존성을 추가하지 않은 채 `Jwts.builder()` 를 호출하게 되면 오류가 발생한다.
  • 에러 로그에서, `io.jsonwebtoken.impl.DefaultJwtBuilder` 라는 이름을 가진 class 를 load 하지 못한다고 말하고 있다.

 

코드

class Jwts
public final class Jwts {
	...
	public static JwtBuilder builder() {
	    return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtBuilder");
	}
}
  • Jwts class 의 builder 메서드를 호출하게 되면 `DefaultjwtBuilder` 를 인스턴스화 하려는 것을 확인할 수 있다.

 

해결

// build.gradle.kts 에 추가
...
runtimeOnly 'io.jsonwebtoken:jjwt-impl:JJWT_RELEASE_VERSION'
...
  • 문제를 해결하기 위해 github 문서에 명시되어 있는 `jjwt-impl` 의존성을 추가하여 해결할 수 있었다.

 


 

문제2. jjwt-gson or jjwt-jackson 의존성 추가

// 에러 로그
io.jsonwebtoken.lang.UnknownClassException: Unable to find an implementation for interface io.jsonwebtoken.io.Serializer using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations.
at app//io.jsonwebtoken.impl.lang.LegacyServices.loadFirst(LegacyServices.java:26)
at app//io.jsonwebtoken.impl.DefaultJwtBuilder.compact(DefaultJwtBuilder.java:291)
...
  • 로그를 살펴보면, compact 메서드를 처리하던 도중 오류가 발생하였다.
  • 위에서 겪은 의존성 문제와 같은 문제가 아닐까 하는 느낌이 들었다..

 

코드

compact()
@Override
public String compact() {

    if (this.serializer == null) {
        // try to find one based on the services available
        // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0
        // use the previous commented out line instead
        this.serializer = LegacyServices.loadFirst(Serializer.class);
    }
		...
}
  • compact 메서드의 시작 부분에서는 serializer 변수를 초기화 하기 위해 loadFirst 를 호출하고 있다.

 

loadFirst(Class<T> spi)
// Loads the first available implementation the given SPI class from the classpath.
public static <T> T loadFirst(Class<T> spi) {
    Assert.notNull(spi, "Parameter 'spi' must not be null.");

    for (ClassLoaderAccessor classLoaderAccessor : CLASS_LOADER_ACCESSORS) {
        T result = loadFirst(spi, classLoaderAccessor.getClassLoader());
        if (result != null) {
            return result;
        }
    }
    throw new UnavailableImplementationException(spi);
}
  • 위의 compact 메서드 처리 과정에서 인수로 받은 Serializer.class 를 인스턴스화를 시도한다.
  • 선언해둔 CLASS_LOADER_ACCESSORS 를 사용하여 Serializer.class 구현체를 찾아본다.

 

해결

...
runtimeOnly 'io.jsonwebtoken:jjwt-gson:JJWT_RELEASE_VERSION'
  • CLASS_LOADER_ACCESSORS  내부에 사용할 수 있는 구현체가 없기에 오류가 발생한 것이다.
  • jjwt-impl 의존성 문제와 마찬가지로, github 문서를 읽어보니 의존성을 추가해야 한다고 명시되어 있다.
  • jjwt-gson 의존성을 추가하여 문제를 해결하였다.

 


 

jjwt-api 관련 정보

 

패키지 구조

 

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

  • jjwt-api 는 패키지 관리에 있어서 implemenation 과 runtimeonly 로 구분하여 의존성 추가를 권장하고 있다.
  • 링크의 설명을 해석해보면 경고 없이 언제든 변화할 수 있는 패키지는 runtimeonly 로 관리하고 그렇지 않은 것은 implemenation 으로 관리하여 안정적인 jjwt-api 라이브러리 사용을 하겠다는 의도이다.
  • 즉, jjwt-impl, jjwt-gson 은 경고없이 언제든 변화할 수 있고 jjwt-api 는 하위호환성을 맞춰가며 개발한다는 의미일 듯 싶다.
  • 실제로 코드를 보면서 하위호환성에 대한 언급과 @Deprecated 를 통해 코드를 유지관리하는 노력을 살펴 볼 수 있다.

 

jjwt-api 에서 사용하는 Serializer 구현체 설명

 

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

  • jjwt-api 는 내부적으로 serialize, deseiralize 를 사용하기 위해 라이브러리를 추가하거나 임의로 Serializer.class 를 구현하여 사용해야 한다고 말하고 있다.
  • 제공되는 구현체로는 jackson 과 gson 이 있는데, 임의로 설정하여 사용하지 않는다면 둘중 편한 방식으로 사용하면 된다.

 

jjwt-gson 의 serialize 방식

public class GsonSerializer<T> implements Serializer<T> {
    ...

    @Override
    public byte[] serialize(T t) throws SerializationException {
        Assert.notNull(t, "Object to serialize cannot be null.");
        try {
            return writeValueAsBytes(t);
        } catch (Exception e) {
            String msg = "Unable to serialize object: " + e.getMessage();
            throw new SerializationException(msg, e);
        }
    }

    @SuppressWarnings("WeakerAccess") //for testing
    protected byte[] writeValueAsBytes(T t) {
        Object o;
        if (t instanceof byte[]) {
            o = Encoders.BASE64.encode((byte[]) t);
        } else if (t instanceof char[]) {
            o = new String((char[]) t);
        } else {
            o = t;
        }
        return this.gson.toJson(o).getBytes(Strings.UTF_8);
    }
}
  • serialize 호출시 writeValueAsBytes 를 호출한다.
  • writeValueAsBytes 메서드 파라미터 `t`는 toJson 메서드를 통해 json string 형식으로 파싱되고 getBytes 메서드를 통해 UTF_8 형식으로 인코딩 되는 것을 확인할 수 있다.
728x90

'Spring Boot' 카테고리의 다른 글

[Spring Boot] Spring 에서 Redis 를 사용할 때 ConnectionFactory 를 왜 Bean 으로 등록해서 사용해야 할까?(feat. Kotlin, Lettuce, LettuceConnectionFactory was not initialized through afterPropertiesSet())  (0) 2023.06.10
[Spring Boot] custom error page 적용(feat. 예제, ErrorController)  (0) 2023.05.26
[Spring Boot] s3 aws-java-sdk(v 1.1) 을 사용하는 개발환경에서 SSL 인증서와 무관하게 통신하는 방법(feat. ValidatorException: PKIX path building failed)  (0) 2023.05.23
[Spring Boot] Spring Boot 에서 JPA 로 값을 수정시 알 수 없는 이유로 sql 에러가 발생할 때(feat. SQL Error: 1064, SQLState: 42000)  (0) 2023.05.20
[Spring Boot] Spring Boot(Kotlin) Gradle 환경에서 docker 이미지를 추출하는 방법(feat. bootBuildImage)  (0) 2023.05.18
    'Spring Boot' 카테고리의 다른 글
    • [Spring Boot] Spring 에서 Redis 를 사용할 때 ConnectionFactory 를 왜 Bean 으로 등록해서 사용해야 할까?(feat. Kotlin, Lettuce, LettuceConnectionFactory was not initialized through afterPropertiesSet())
    • [Spring Boot] custom error page 적용(feat. 예제, ErrorController)
    • [Spring Boot] s3 aws-java-sdk(v 1.1) 을 사용하는 개발환경에서 SSL 인증서와 무관하게 통신하는 방법(feat. ValidatorException: PKIX path building failed)
    • [Spring Boot] Spring Boot 에서 JPA 로 값을 수정시 알 수 없는 이유로 sql 에러가 발생할 때(feat. SQL Error: 1064, SQLState: 42000)
    sggnology
    sggnology
    하늘은 파란색이니까 내 삶도 파란색이길 ㅎㅎ

    티스토리툴바