RestTemplate vs WebClient 속도 테스트

LSH·2024년 6월 5일
0

설정

목록 보기
2/2

5월 23일에 OpenAPI 데이터를 가져오는 방식으로 동기,비동기를 각각 사용해서 속도 테스트를 해보았다.

근데 단순히 로그를 이용해서 테스트를 했다 말하기엔 좀 아쉬워서, 다른 테스트 도구들을 이용해봐야겠다 생각했다.

처음에는 jmh 라는 라이브러리를 사용해보았다.
jmh란 Java Microbenchmark Harness 약어로, JVM 위에서 동작하는 코드의 성능을 측정해주는 라이브러리이다.

jmh를 사용하기 전 이게 무엇인지에 대해 한번 알아보도록 하자.

  • https://oneny.tistory.com/45 해당 블로그 내용을 참고했으며,공부할겸 손으로 타이핑해서 해당 내용을 적는다.

우리가 작성한 Java코드는 1차적으로 중간언어로 컴파일 해야 한다. 주로 Byte Code는 jar,war파일로 아카이브하여 활용하게 되는데, 빌드된 파일을 실행하게 되면 JVM에서는 이 바이트 코드를 번역하여 기계어로 만들고, 이 기계어를 CPU에서 처리하는 절차를 갖는다. 이렇게 빌드된 바이트 코드는 별도의 추가 빌드 없이 자바가 실행 가능한 CPU 아키텍처, 즉 여러 OS에서 실행할 수 있는 장점이 있다. 이렇게 자바는 compile,Interpret라는 두 가지 동작에 의해 실행되는 하이브리드 언어이다.

이렇다 보니 컴파일 과정에서 바로 기계어를 만드는 C/C++ 같은 컴파일 언어에 비해 성능이 뒤쳐지게 된다. 컴파일 언어들은 런타임 환경에서 준비된 기계어를 즉시 실행 가능하기 때문이다. 또한, 컴파일을 통해 기계어를 만들 때 코드 최적화 또한 진행하기 때문에 일반적으로 인터프리터 언어 보다는 더 빠른 성능을 보장할 수 있다. 단점으론, 이렇게 컴파일을 통해 생성된 기계어는 빌드된 CPU 아키텍처에 종속적이기 때문에 다른 아키텍처 환경에서는 돌아가지 않아 해당 환경에서 빌드를 다시해야 한다.

Java에서는 이러한 컴파일 언어보다 느린 성능 차이를 해결하기 위해 위 그림과 같이 JVM에 JIT Compiler를 도입하고 있다. 바로 기계어를 만들어 낸다는 의미다.
JIT Compiler는 바이트 코드를 Machine Code를 변환하는 과정에서 Machine Code를 캐시에 저장하고 활용한다. 이를 통해 반복되는 기계어 변환 과정을 줄이게 되어 성능 향상시키고, 런타임 환경에 맞춰 코드를 최적화함으로 추가적인 성능 향상을 이뤄낸다.

Warm Up
JIT Compiler가 자바 성능 향상에 도움은 되지만 애플리케이션이 시작되는 단계에서는 캐시된 내역들이 없기에 자연스레 성능 이슈가 발생할 수 있다. 그래서 애플리케이션 시작 후 의도적으로 미리 로직을 실행하여 기계어가 캐시에 저장되고, 최적화될 수 있도록 하는 Warm up 절차가 필요하다 할 수 있다.

  • JMH 옵션에서 warmupTime을 늘리면 JIT 컴파일러가 더 많은 코드를 최적화하여 측정된 성능 지표의 표준편차를 낮출 수 있다.

그리고 성능을 테스트할 때에도 startTime,endTime을 측정해서 테스트 결과를 출력해서 비교하는데 로직 성능만을 검사하고 싶지만 warm up, 스레드 갯수등 고려해야할 사항들이 많다. 이러한 부분을 해결해주기 위한 성능 측정 프레임워크인 벤치마크 프레임워크를 사용해서 로직에 대한 성능을 위해 부수적인 것들을 최대한 줄여볼 수 있다.

JMH(Java MicrobenchMark Harness)
JMH는 OpenJDK에서 만든 벤치마크 라이브러리이다. JVM warm-up 기능을 제공하여 편리하게 성능을 측정할 수 있다.

JMH 관련 애노테이션
@Fork: fork 실행 횟수 지정
@Warmup: warm up 수행 횟수 지정
@Mearsurement, iterations: 측정 횟수 지정
@Timeout:테스트 함수의 타임아웃 지정
@Threads:사용할 스레드 개수 지정
@BenchmarkMode

  • Throughput:디폴트 값, 초당 작업 수 측정
    AverageTime:작업이 수행되는 평균 시간 측정
    SampleTime:최대,최소 시간 등 작업이 수행되는 시간 측정
    SingleShotTime:단일 작업 수행 소요 시간 측정
    All:모든 옵션 측정

@OutputTimeUnit:측정 단위, 디폴트는 ns
@State:argument 상태 지정. State 애노테이션을 지정한 클래스는 public이어야하고, 기본 생성자를 가지고 있어야 한다.
Scope.Thread: 스레드 별로 인스턴스 생성
Scope.Benchmark:동일한 테스트 내의 모든 스레드에서 동일한 인스턴스 공유, 멀티 스레딩 성능 테스트에 사용
Scope.Group:스레드 그룹마다 인스턴스 생성
@Setup/@Teardown:Junit의 Before,After같은 역할을 한다.

  • 전자는 벤치마크가 시작되기 전 오브젝트를 설정하기 위해 사용
    후자는 벤치마크가 종료된 후 오브젝트를 정리 하기 위해 사용

해당 라이브러리는 Maven을 사용할 것을 권장하지만 Gradle를 사용해도 상관은 없다.

**build.gradle**
plugins {
    id "me.champeau.jmh" version "0.7.1"
}
dependencies {
    // JUnit Jupiter API
    implementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.1'
   // JMH dependencies
    implementation 'org.openjdk.jmh:jmh-core:1.35'
    implementation 'org.openjdk.jmh:jmh-generator-annprocess:1.35'
}

build.gradle에 해당 코드를 넣어주자.
https://github.com/melix/jmh-gradle-plugin#what-plugin-version-to-use <-Gralde 버전에 따라 jmh plugin 버전도 맞춰줘야 오류가 안난다.

기본 테스트 방법은 타 블로그들이 잘 정리해놨으니 여기서는 내가 했던 코드와 테스트 결과를 적도록 하겠다.

먼저, 테스트를 위해서
사진과 같이 폴더경로를 만들어 놔야한다.
처음에는 src-main-java 폴더에 BenchmarkTest 클래스파일을 넣어두고 테스트하려하니까 오류가나서, 왜 그런가 해맸는데, jmh폴더를 따로 만들어두고 여기에다 테스트 할 클래스를 넣어둬야 했던 것이었다.

package com.finedust.project;
-import 생략-
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 3, jvmArgs = {"-Xms8G", "-Xmx8G"})
public class BenchmarkTest {

    private String serviceKey = "~~";


    private final static String BASE_URL = "https://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty";

    private RestTemplate restTemplate = new RestTemplate();

    // 기본 생성자 추가
    public BenchmarkTest() {
        // 필요한 초기화 코드 작성
    }



    public List<String> OpenAPiData() {

        String[] sidoNames = {"서울", "부산", "대구", "인천", "광주", "대전", "울산", "경기", "강원", "충북", "충남", "전북", "전남", "경북", "경남", "제주", "세종"};
        List<String> responses = new ArrayList<>();

        for(String sidoName : sidoNames){
            try {
                String encodedServiceKey = URLEncoder.encode(serviceKey, "UTF-8");
                String encodedSidoName = URLEncoder.encode(sidoName, "UTF-8");
                URI uri = UriComponentsBuilder
                        .fromUriString(BASE_URL)
                        .queryParam("serviceKey", encodedServiceKey)
                        .queryParam("returnType", "json")
                        .queryParam("sidoName", encodedSidoName)
                        .queryParam("numOfRows", "200")
                        .queryParam("ver", "1.0")
                        .build(true)
                        .toUri();

                HttpHeaders headers = new HttpHeaders();
                headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); // JSON 형식 강제
                HttpEntity<String> entity = new HttpEntity<>(headers);

                ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, entity, String.class);
                responses.add(response.getBody());
            } catch (RestClientException e) {
                throw new RuntimeException("API 호출 실패", e);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }
    return responses;
    }


    @Test
    public void launchBenchmark() throws Exception {
        Options opt = new OptionsBuilder()
                .include(BenchmarkTest.class.getSimpleName())
                .mode(Mode.AverageTime)
                .timeUnit(TimeUnit.MICROSECONDS)
                .warmupTime(TimeValue.seconds(30)) 
                .warmupIterations(6)
                .measurementTime(TimeValue.seconds(1))
                .measurementIterations(30)
                .threads(3)
                .forks(3)
                .shouldFailOnError(true)
                .shouldDoGC(true)
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public void benchmarkCreateWebClient() throws UnsupportedEncodingException {
        OpenAPiData();
    }
}

Intellij 에 Terminal 에서 ./gradlew jmh명령어를 입력하면되고, 해당 터미널 창에서도 결과가 나오고, 위에 ~/result.txt 로 결과가 저장이된다.

각 테스트 결과 용어를 설명해보자면

  • Benchmark: 벤치마크 테스트 이름
  • Mode: 벤치마크 모드를 나타내며, 해당 테스트에서는 avgt(Average Time)로 설정, 평균 시간 측정
  • Cnt : 테스트를 몇번 반복 했는지 나타냄
  • Score : 벤치마크 측정 값의 평균
  • Error : 측정 값의 표준 오차를 나타냄
  • Units : 측정 단위, 여기서는 ms

해당 결과에 아쉬운 점은 JMH는 자바 내 코드의 성능 측정에 최적화된 도구이지, OpenAPI,DB같은 외부 API 호출 및 시스템 에 대한 성능 측정은 불완전 하다. 이를 최대한 신뢰성 높은 결과로 나오도록 (Error 이 100미만) warmup이나 반복횟수를 늘려서 테스트를 하였다.(그래도 ERROR값이 신뢰성 있을만한 수치가 나오지 않았다.)

  • 이렇게 외부 api나 시스템(DB등) 사용 시에는 JMH를 사용하는건 적합하지 않는다. 이걸 다하고 나서 깨달았다.. 앞으로 어떤 기능을 사용하기 전에 해당 기능에 대해 더 철저히 공부해야겠다.

이렇게 외부 api나 시스템에 대해 이를 포함해 테스트를 하고자 할때는 다른 테스트 도구를 사용하는걸 추천한다. 해서 JMeter라는 것을 사용하였다. https://effortguy.tistory.com/164 해당 블로그를 참고해서 테스트를 진행하였다.

JMeter란, Apache에서 만든 자바로 만들어진 웹 어플리케이션 성능 테스트 오픈 소스 이라 한다.

테스트 할 수 있는 종류로는

웹 - HTTP, HTTPS (Java, NodeJS, PHP, ASP.NET, …)

  • SOAP / REST 웹 서비스

  • FTP

  • JDBC

  • LDAP

  • JMS - Message-oriented middleware (MOM)

  • Mail - SMTP(S), POP3(S) and IMAP(S)

  • Native commands or shell scripts

  • TCP

  • Java Objects
    이라 한다.

설치방법과, 세팅은 위 블로그를 참조하면 될것이다.
테스트 전 유의 사항으론 테스트하는 웹 어플리케이션 서버와 테스트를 돌리는 서버는 서로 달라야 한다.
이 두개가 같으면 같은 메모리를 사용하기 떄문에 정확한 값을 측정할 순 없다고 한다.

하지만, RestTemplate랑 WebClient의 속도 테스트 목적이니 로컬에서 2개 같이 썻다.

테스트 들어가기 전 해당 용어만 알고가자.


Summary Report

  • Label : Sampler 명

  • Samples : 샘플 실행 수 (Number of Threads X Ramp-up period)

  • Average : 평균 걸린 시간 (ms)

  • Min : 최소

  • Max : 최대

  • Std. Dev. : 표준편차

  • Error % : 에러율

  • Throughput : 초(분,시간)당 트랜잭션 처리량

  • Received KB/sec : 초당 받은 데이터량

  • Sent KB/sec : 초당 보낸 데이터량

  • Avg. Bytes : 서버로부터 받은 데이터 평균

Intellij에서 작성한 코드들을 바탕으로

사진처럼 경로를 Controller에 작성한 경로랑 맞춰주면 된다.


WebClient 테스트 결과

  • OpenApi 데이터 가져오고, DB저장

RestTemplate 테스트 결과

  • OpenApi 데이터 가져오고 DB 저장 동일(아래도)
    WebClient TPS 1.1/sec, RestTemplate TPS 9.6/min -> 0.16/sec

해당 테스트 방식은 jmh처럼 따로 폴더경로를 지정해둘 필요도 없고, 단순히 Intellij에서 Controller에 적은 경로만 HTTP Request에 입력하고 실행하면 테스트가 돼서 간편하다.

둘의 AVG 차이를 보면 WebClient는 86(ms), RestTemplate는 6459(ms) 이다.

  • 위 결과는 Thread를 1로 설정해 놓고 테스트를 한 결과이다. 아래는 스레드를 10으로 놓고 테스트를 해보았다.

  • WebClient

  • RestTemplate, tps가 40.2/min으로 나왔는데 이를 /sec로 바꿔보면 약 0.67/sec 정도이다. WebClient가 데이터 처리하는 속도가 약 2배이상 빠르다고 보면된다.

데이터 가져오는 속도면에서는 스레드가 증가해도, 여전히 WebClient방식이 빠르고,간단하다. 테스트 시 스레드가 30,50,100 이렇게 증가할수록 WebClient같은 경우는 문제없이 데이터를 openapi에서 가져오고 DB 저장 및 조회까지 하지만, RestTemplate같인 경우 "connection is not available, request timed out after 30012ms.]" 같은 에러가 발생한다.
application.properties에
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.connection-timeout=25000
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.max-lifetime=450000
spring.datasource.hikari.minimum-idle=30
개인적으로 설정해놔야 오류가 안난다.

참고:https://velog.io/@zini9188/%EC%98%A4%EB%A5%98-connection-is-not-available-request-timed-out-after-30000ms

profile
메모하자.

0개의 댓글