728x90
발단
흔히 사용하는 jwt 라이브러리인 jjwt 를 사용하여 개발하려 하니 2018년도 이후로 업데이트 되지 않고 있었다. 예제로도 jjwt 를 사용하는 경우가 많지만 최신 패키지인 jjwt-api 가 있기에 이를 사용하여 공부하던 도중 문제가 발생한 의존성에 대해 트러블 슈팅 하는 과정을 작성하였다.
환경
- Spring Boot: 3.1.0 (kotlin, gradle)
- jjwt-api 0.11.5
jjwt 와 jjwt-api 의 상관관계
- 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 관련 정보
패키지 구조
- jjwt-api 는 패키지 관리에 있어서 implemenation 과 runtimeonly 로 구분하여 의존성 추가를 권장하고 있다.
- 링크의 설명을 해석해보면 경고 없이 언제든 변화할 수 있는 패키지는 runtimeonly 로 관리하고 그렇지 않은 것은 implemenation 으로 관리하여 안정적인 jjwt-api 라이브러리 사용을 하겠다는 의도이다.
- 즉, jjwt-impl, jjwt-gson 은 경고없이 언제든 변화할 수 있고 jjwt-api 는 하위호환성을 맞춰가며 개발한다는 의미일 듯 싶다.
- 실제로 코드를 보면서 하위호환성에 대한 언급과 @Deprecated 를 통해 코드를 유지관리하는 노력을 살펴 볼 수 있다.
jjwt-api 에서 사용하는 Serializer 구현체 설명
- 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