JDK 21 에서 mockito를 못쓴다고요? (feat. byte buddy)

겔로그·2024년 9월 19일
0
post-thumbnail

자극적인 제목으로 어그로를 끌어봤습니다.

오늘은 JDK 21로 버전 업 하는 과정에서 경험한 신기한 이슈를 정리해 공유하고자 합니다.

개요

때는 2024년 9월 19일... 신규 프로젝트 버전 호환을 위해 기존에 운영하던 사내 오픈소스 라이브러리 JDK 버전을 올리는 과정에서 발생한 일입니다.

기존 사내 오픈소스는 JDK 8 기반으로 구현이 되어있었고, 내부적으로 JDK 11 버전까지 정상적으로 호환이 되는 라이브러리였습니다. 이번에 진행하는 신규 프로젝트에서 해당 라이브러리가 필요해 연동하고자 하였으나, JDK 21 기반으로 개발하여 일부 호환이 안되는 문제가 발생해 라이브러리 버전 업을 진행하게 되었습니다.

문제 발생

다음과 같은 프로세스로 버전업을 진행했습니다.

  • JDK 버전 업 수행 (8 -> 21)
  • 테스트 실행 및 jar 생성 후 신규 프로젝트에서 동작 테스트
  • 에러 부분 확인 후 수정

이 과정에서 발생하는 대표적인 몇가지 이슈(jaxb, jakarta 변환, maven 컴파일러 버전 등)는 구글링을 통해 해결을 하였으나 신기하게도 하나의 문제가 계속해서 발생하는 것을 확인했습니다.

? 왜 mock 에서 에러가 나지?

일단 해당 내용을 무시하고 다른 문제 케이스들 해결이 완료될 때까지 주석처리한 채 개발을 진행했습니다.

대부분의 에러를 수정한 이후, 다시 문제가 되는 테스트를 확인했으나, 원인을 알 수 없었습니다. 당시 발생한 에러 코드를 공유드립니다.

문제의 그 코드

ReactiveHealthContributorRegistry registry = mock(ReactiveHealthContributorRegistry.class); // 여기서 발생함

// when
new ConditionChecker(statusCheckers, registry);

ReactiveHealthContributorRegistry.java

package org.springframework.boot.actuate.health;

public interface ReactiveHealthContributorRegistry extends ContributorRegistry<ReactiveHealthContributor> {
}

사실상 ReactiveHealthContributorRegistry 인터페이스를 모킹해 클래스에 넣은것 뿐인데... jdk 8, 11에서는 정상 동작하고 21에서는 동작을 안하는 이슈였습니다.

해결 방법

사실 해결 방법은 쉽습니다. 해당 인터페이스를 구현한 Faker 객체를 직접 구현하고, 해당 클래스를 ConditionChecker에 주입하면 되는 문제입니다.

내가 대충 해결한 방법

private ReactiveHealthContributorRegistry getReactiveHealthContributorRegistry() {
    return new TestReactiveHealthContributorRegistry();
}

private class TestReactiveHealthContributorRegistry implements ReactiveHealthContributorRegistry {

    @Override
    public void registerContributor(String name, ReactiveHealthContributor contributor) {

    }

    @Override
    public ReactiveHealthContributor unregisterContributor(String name) {
        return null;
    }

    @Override
    public ReactiveHealthContributor getContributor(String name) {
        return null;
    }

    @Override
    public Iterator<NamedContributor<ReactiveHealthContributor>> iterator() {
        return null;
    }
}

기존 mock()이 있던 자리에 getReactiveHealthContributorRegistry()를 넣으면 끝!

하지만 저 에러의 원인이 뭔지는 알아봐야겠죠?

구글링 시작 (원인 찾기)

찾아보니 이 이슈는 굉장히 뜨거운 감자였던 거시였습니다..!
JDK 21 - Dynamic Loading of Agent (byte-buddy-agent-1.14.4.jar)

JDK 21과 특정 mockito 버전에서 충돌이 나기 시작한 것인데요.
확인한 내용으로는 byte-buddy 1.14.5 버전으로 인해 발생한 것으로 보입니다.

byte buddy

Byte Buddy is a Java library for creating Java classes at run time. This artifact is a build of Byte Buddy with all ASM dependencies repackaged into its own name space.

JEP 451: Prepare to Disallow the Dynamic Loading of Agents

Issue warnings when agents are loaded dynamically into a running JVM. These warnings aim to prepare users for a future release which disallows the dynamic loading of agents by default in order to improve integrity by default. Serviceability tools that load agents at startup will not cause warnings to be issued in any release.

mockito에서 사용되는 byte buddy 라이브러리가 바이트를 동적으로 조작하는 라이브러리인 것에 반해, JDK 21에서는 무결성을 위해 에이전트의 동적 로딩을 기본적으로 허용하지 않는 것을 정책으로 하는 것을 확인할 수 있습니다.

현재 상황

현재 시점에서 해결 가능한 방법은 다음과 같습니다.

  • javaagent 사용: 라이브러리가 에이전트를 필요로 할 경우, 반드시 -javaagent 플래그를 사용하여 JVM을 실행한다.
  • Gradle 또는 Maven 설정 추가: 빌드 도구에서 테스트를 실행할 때, -javaagent 플래그를 추가하여 JVM을 구성해야 합니다. 예를 들어, Gradle에서는 test 태스크에 에이전트를 추가하는 설정
  • -XX:+EnableDynamicAgentLoading 옵션 사용

하지만 여기서 -XX:+EnableDynamicAgentLoading 방식은 현재 Java 진영에서 가져가는 정책과 반대되는 방향이며 사라질수도 있는 정책이기 때문에 커뮤니티에서는 사용하지 않는것을 권장하고 있습니다.

mockito에서도 해당 문제에 대해 Byte Buddy를 직접적으로 노출하지 않고도 Mockito 내에서 에이전트를 로드할 수 있는 옵션을 추가하겠다.는 답변을 2주 뒤에 달아 추후 개선이 되지 않을까... 싶습니다.

결론

JDK 21로 버전 업을 진행하면서 여러 이슈들이 발생할 수 있습니다. 구글링을 하시다보면 나보다 먼저 이 작업을 진행하면서 여러 이슈를 경험해보신 여러 개발자분들이 작성한 글들이 있으니 작업 이전에 한 번 발생할 수 있는 이슈들을 목록화 하신 뒤 진행하시면 조금 더 수월하지 않을까 싶습니다.

저는 사전에 목록화했었던 이슈들 뿐만 아니라 Java의 dynamic loading이 버전에 따라 달라지는 것(allow -> disallowed)을 추가로 알게 되었습니다.

이로 인해 mockito의 byte buddy 라이브러리와 충돌할 수 있다는 것을 직접 경험했네요 ㅎㅎ.. 다른 dynamic loading을 수행하는 라이브러리 또는 코드에서도 발생할 수 있다는 점 확인하고 가시면 좋을 것 같습니다!

읽어주셔서 감사합니다.

별첨. JDK version에 대해....

회사에서 보편적으로 사용하는 JDK 버전이 8, 11, 17, 21일텐데요. 왜 그런지 알고 계신가요?

이유는 다음과 같습니다.

  • 기능상 변화가 큰 버전들이다
  • LTS(Long Term Support)가 가장 긴 버전

위 표를 보시면 JDK 8 버전보다 더 긴 LTS를 제공하는 버전이 JDK 21 밖에 없음을 알 수 있습니다. Java의 신규 기능 사용 및 성능상 이점을 얻기 위해 버전업을 수행하나 이러한 부분도 고려해서 버전업을 수행하시는 것을 추천드립니다.

Spring Boot3 부터는 JDK 17이 필수 사항이 되기 때문에 보편적으로 Spring Boot 버전 업을 하면서 JDK 버전 또한 업그레이드를 하는 점 참고 부탁드립니다~

profile
Gelog 나쁜 것만 드려요~

0개의 댓글