Spring/Test-Driven Develop

Spring Boot WebClient Mocking 하기

민돌v 2025. 1. 14. 23:04
상당히 오랜만에 글을 쓰네영
반성하고 열심히 재밌게 다시 써보겠습니다 야호
코드는 깃허브에 있습니다.

 

새로운 회사로 이직한지 1달이 되었습니다.
여느 회사나 마찬가지로 제가 모르는 코드를 수정해야하기 때문에 테스트 코드는 저에게 필수적인데요.

오늘은 Spring Boot WebFlux 와 함께 나온 WebClient 를 Mocking 하는 방법에 대해 기록해보고자 합니다.

 

[목차]

  1. WebClient 를 Mocking 하려는 이유
  2. WebClient Mocking 방법

1. WebClient를 Mocking 하려는 이유

사실 WebClient 자체를 Mocking 하는 상황이 좋은 상황은 아니라고 생각합니다.

코드단에서 더 좋은 방향은 WebClient 를 사용하는 부분을 추상화하고 호출부에서 추상화하는(or 책임을 가지는) 클래스를 의존하는 방향이면, WebClient 를 Mocking 할 필요가 아예 없어지겠죠

하지만 코딩이라는게! 항상 이상적인 방향으로 갈 수는 없겠죠! (코딩 잘하고 싶다!)

 

✔️ 주어진 상황

현재 저에게 주어진 상황은 다음과 같습니다.

  1. WebClient 의 기본적인 객체를 생성하는 util 클래스가 존재
  2. "어플리케이션의 하나의 작업 단위(집합)를 정의하고 도메인의 각 동작을 조율하는 역할" 해야할 객체에 동작을 수행하는 비즈니스로직이 결합 → 많은 의존성과 책임을 가짐
  3. WebClientUtil 클래스에서 객체를 생성해화 커스텀한 설정을 거쳐 타 서비스를 호출하고 로직을 수정해야하는 상황
  4. 해당 부분을 따로 추출하기에는 너무 많은 변경점이 생김
  5. 빠른 요구사항을 적용하고 테스트하기위해서 WebClient 자체에 대한 Mocking 테스트가 필요

 

⚙️ 예시코드

  • 아래는 간단하게 외부 의존성을 가지는 Service 객체를 만들어 보았습니다.
  • 👏 목표는 WebClient 호출 결과값으로 인한 Exception 처리에 대한 테스트 코드를 작성입니다.
@Service
@RequiredArgsConstructor
public class WebClientLegacyService {

    private final JpaRepository repository;
	private final WebClientUtil webclientUtil;
    
    public boolean callWebClient() {

        String string = webclientUtil.getBaseUrl("/test/할수/없는/주소지롱")
            .put()
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
                Mono.error(new RuntimeException("400 에러 입니다"))
            )
            .onStatus(HttpStatusCode::is5xxServerError, clientResponse ->
                Mono.error(new RuntimeException("500 에러 입니다.")))
            .bodyToMono(String.class)
            .block();

        return Objects.nonNull(string);
    }
}

 


2. WebClient Mocking 방법

WebClient 를 Mocking 하는 방법도 여러가지가 존재합니다. 저는 그 중에 2가지를 사용해보고자 시도했습니다.

  1. MockWebServer 를 사용하는 방법
  2. Mockito 를 사용하는 방법

1) MockWebServer 로 WebClient Mokcing

먼저 MockWebServer 를 사용해보고자 합니다.

 

✔️ MockWebServer 란

트위터의 공동 창업자인 잭 도시(Jack Dorsety)가 그의 친구와 함께 설립한 Square(현 Block) 에서 만든 okhttp 오픈소스 엔터프라이즈에 속한 라이브러리 중 하나입니다.

OkHttp

  • okhttp 는 HTTP 통신을 간편하게 구현할 수 있도록 다양한 기능을 제공해주는 Java 라이브러리입니다. Retrofit와 Okio 라이브러리를 사용하고있습니다.
  • 많은 자료를 찾아보지않아지만, Spring 라이브러리인 WebClient 에 비해 사용 방법이나 가독성, Webflux 에 대한 러닝 커브가 존재하지 않아 꽤 괜찮은 라이브러리로 보여집니다.
  • 다만, Spring Boot 3.2 부터 RestCleint 를 지원하기 때문에 3.2 이상의 버전을 사용한다면 굳이 OkHttp 를 사용할 이유 또한 없어보이긴 합니다.
  • 벤치마크 수행결과에 의하면 약 30%정도 WebClient 보다 빠르다라는 결과가 존재하기도 하네요

출처 : https://github.com/ok2c/httpclient-benchmark/wiki


MockWebServer

  • MockWebServer는 OkHttp 라이브러리에서 제공하는 테스트 유틸리티로, 네트워크 요청을 테스트 환경에서 실행하기 위해 사용됩니다.
  • MockWebServer 네트워크 요청에 대해 예상되는 응답을 미리 정의해 두고, 해당 응답을 반환하여 실제 서버 없이도 클라이언트 코드가 서버와 통신하는 것처럼 동작하도록 테스트 환경을 구성하도록 해주는 도구입니다.
  • MockWebServer의 Junit5 가능  Oct 24, 2020 에 처음 커밋되었고, 최근(2025.1.14 기준) 최근까지도 지속해서 라이브러리가 관리되어지는 모습이 보여져서 꽤 믿음이 갑니다. (심지어 코틀린으로 구현된 라이브러리 입니다..!)

mvnrepository

 

Star 수도 꽤 많고 무엇보다 테스트 환경에서 실행속도에 영향을 주지않아서 가벼운 마음으로 사용해보고자 합니다!

https://github.com/square/okhttp


✔️ MockWebServer  Test Code 작성하기

⚙️ gradle.build

// https://mvnrepository.com/artifact/com.squareup.okhttp3/mockwebserver
testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14'

⚙️ Test Config

  • 가상의 웹서버 포트를 만들어 mocking 하고 응답값을 전달해주는 방식입니다.
@ExtendWith(MockitoExtension.class)
class WebClientLegacyServiceTest {

    private static MockWebServer mockWebServer;
    private static WebClient mockWebClient;

    @Mock
    JpaRepository repository;
    @Mock
    WebClientUtil webClientUtil;

    @InjectMocks
    WebClientLegacyService webClientLegacyService;

    @BeforeEach
    void setUp() throws IOException {

        mockWebServer = new MockWebServer();
        mockWebServer.start();

        mockWebClient = WebClient.builder()
            .baseUrl(mockWebServer.url("/").toString())
            .build();
    }

    @AfterEach
    void tearDown() throws IOException {
        mockWebServer.shutdown();
    }
    //...
}

 

⚙️ Test Code

@ExtendWith(MockitoExtension.class)
class WebClientLegacyServiceTest {

    @Test
    @DisplayName("요청 성공 테스트")
    void success() {

        // given
        String bodyJson = """
                {
                 "result": true,
                 "message": ""
                 }
            """;

        MockResponse mockResponse = new MockResponse()
            .setBody(bodyJson)
            .addHeader("Content-Type", "application/json")
            .setResponseCode(200);

        mockWebServer.enqueue(mockResponse);

        when(webClientUtil.get(anyString())).thenReturn(mockWebClient);

        // when
        boolean result = webClientLegacyService.callWebClient();

        // then
        Assertions.assertTrue(result);

    }

    @Test
    @DisplayName("실패 테스트 - 400 error")
    void fail400() {

        // given
        String bodyJson = """
                {
                 "result": false
                 }
            """;

        MockResponse mockResponse = new MockResponse()
            .setBody(bodyJson)
            .addHeader("Content-Type", "application/json")
            .setResponseCode(400);

        mockWebServer.enqueue(mockResponse);

        when(webClientUtil.get(anyString())).thenReturn(mockWebClient);

        // then
        assertThatThrownBy(() -> webClientLegacyService.callWebClient())
            .isInstanceOf(RuntimeException.class)
            .hasMessage("400 에러 입니다");

    }
}

 

 

빠르다..!

 


3. 마무리하며

사실 테스트를위한 라이브러리를 추가하고싶지는 않았습니다.

다만, Mockito를 이용한 WebClient 를 Mocking 하는 방식이 너무 복잡하고 손이많이가,, 이것 또한 자원 낭비가 아닐까 하고 찾아보고 적용해보니 테스트코드상에서는 러닝커브 없이 간다하고 가볍게 수행할 수 있었습니다.

👏 별개

  • 이건 다른 이야기지만, 성능상으로 OkHttp 가 WebClient 보다 높은 성능을 보인다고 하지만 내부적으로 Direct buffer memory 를 사용하여 빠른 성능을 보이지만, JVM Heap 을 사용하는게 아니라 GC 의 대상이 되지 않아 명시적으로 사용자가 메모리를 반환해 주지않으면 메모리 누수 (Native Memory Leak Detection) 의 위험성이 존재한다고 합니다.
  • 이건 직접 코드를 까본게 아니라서 한번 살펴볼 필요는 있어보이네요. 코드를 살펴본 후 내용 업데이트 하겠습니다
  • → 이전에는 대용량 데이터의 전달을 위해 Nio.ByteBuffer를 사용했는지 모르겠지만, 현재는 Okio 라이브러리에서 생성한 별도의 Buffer 객체를 사용하여 Connect 시 사용하는 것 같습니다.(아마도,,, 정말 어렵네요)

👏 결론

  • MockWebServer는 테스트 코드에서만 사용되며 가볍고 빠르며, 적용하기 쉬워서 매우 적합해 보인다.

 

그럼 이만!
끝!

 


참고