Situation
현재 피파온라인 전적검색 프로젝트에서 사용자의 전적을 갱신해야하는 기능이 있다. 이 기능은 사용자의 정보를 갱신하고, 사용자의 Match 리스트를 불러온 뒤, 리스트에 포함된 각각의 Match 상세 정보를 Nexon API에 요청해야하는 작업이 필요했다. 이 과정에서 평균적으로 약 200번의 API 요청이 필요했고, 이를 처리하기 위해 @Async 와 ThreadPoolTaskExectuor를 사용하여 RestTempalte을 통해 비동기적으로 요청을 하는 로직을 구현했다. 기존에 동기적인 방식에 싱글스레드는 약 100s가 걸렸지만, 해당 비동기 로직으로 변경한 후 15s 로 줄일 수 있었다. 하지만, 이것도 매우 느렸다. 왜 이렇게 처리 속도가 늦을까 생각을하다가, 2가지 고민을 하게되었다.
- 200개의 API 요청 시 각각 Tcp/Ip 연결을 매번 생성하는게 아닐까? keep - alive 설정이 안되어있는가?
- RestTemplate은 동기적 I/O 작업이기에 느린걸까?
1번의 경우는 아니었다. 설정이 되어있음에도 불구하고 느린것이었다.
2번의 경우, RestTemplate을 사용하여 API 요청을 처리할 때, 각 요청은 이전 요청이 완료될 때까지 대기해아 하며, 이는 다수의 요청을 동시에 처리하는 경우 성능 저하의 주요 원인이었다. 요청 처리 과정에서 I/O 작업이 블로킹 되어, CPU 리소스를 효율적으로 활용하지 못하고, 네트워크 지연 시간 동안에도 스레드가 대기 상태에 머무르게 된다. 따라서 이 문제를 해결하기 위해서는 Non-Blocking 방식을 선택해야 했다.
RestTemplate 구조
HttpMessageConverter
는 HTTP 바디 메시지를 자바 객체로 변환하는 역할을 한다.
REST-API 서버와 커넥션을 맺고 요청 메시지를 전달하며 응답 메시지를 받아오는 모든 네트워킹 과정을 처리하는 것은 ClientHttpRquestFactory
가 담당한다. 해당 인터페이스의 구현체는 아래와 같고, 스프링부트는 기본으로 SimpleClientHttpRequestFactory
를 사용한다.
-
OkHttp3ClientHttpRequestFactory
- 안드로이드에서 많이 사용하는 OkHttp 로 작성한 구현체
- 동기식/비동기식 모두 가능한 장점이 있음
-
Netty4ClientHttpRequestFactory
- 비동기 논블로킹 프레임워크인 Netty 를 사용하여 작성된 구현체
- 비동기 프로그래밍을 계획한다면 가장 먼저 고려하는 것이 좋음
-
SimpleClientHttpRequestFactory
- 동기식으로 동작하며, JDK 에서 제공하는 라이브러리를 사용하여 작성된 구현체
-
HttpComponentsClientHttpRequestFactory
- 동기식으로 동작하며, JDK 에서 제공하는 라이브러리를 사용하여 작성된 구현체
Task
현재 Blocking I/O 작업을 하는 로직을 Non-Blocking I/O 작업으로 변경해야한다.
Action
따라서 RestTemplate 대신 비동기적이고 non-blocking I/O 작업을 지원하는 WebClient를 사용하는 것이 바람직하다. WebClient는Spring 5에서 도입되었으며, 비동기적으로 작동하고, Reactive Programming을 지원하여, 더 나은 성능과 리소스 활용 효율성을 제공한다.
WebClient
- Single Thread + Non-Blocking 방식이다.
- Core 당 1개의 Thread 이용
- 각 요청은 Event Loop 내에 Job으로 등록된다.
- Event Loop는 각 Job을 제공자에게 요청한 후, 결과를 기다리지 않고 다른 Job을 처리한다.
- 즉, 외부 API 요청이 많은 서비스에 적합하다.
Code
WebClientConfig 클래스에서 웹 클라이언트의 설정을 분리하여 관리함으로써, 나중에 설정을 변경하거나 추가할 때 코드의 다른 부분을 변경하지 않고 관리할 수 있도록 하였다.
이제 실제 refresh가 수행되는 서비스에서는
- 사용자의 ouid(사용자 고유 식별자) 를 동기적으로 가져오고 (block() 메소드를 통해)
- ouid 를 이용하여 사용자의 정보(Level 등)을 비동기로 가져온다.
- ouid 를 이용하여 사용자의 Match List를 가져온다.
- Match List를 이용하여 모든 Match 세부 정보를 비동기적으로 가져온다.
- Match List 는 String[] 이며 Match 에 대한 고유ID값이 들어가있다.
- Match 고유 ID 값을 통해 Match의 세부 정보를 비동기적으로 요청한다. Non-Blocking
- Match 고유 ID 값을 통해 해당 Match 에 참여하는 사용자들의 티어를 비동기적으로 요청한다. Non-Blocking.
- 모든 요청에 대한 응답을 받게되면 DB에 저장을한다.
- 2번에서 요청한 응답값을 반환한다.
Result
비교
- RestTemplate - 동기
- RestTmplate + @Aync , ThreadPool - 비동기 --- 현재
- WebClient - Non-Blokcing & 비동기
RestTemplate - 동기
RestTemplate + @Async , ThreadPool
WebClient - Non-Blocking & 비동기
ThreadPoolTaskExecutor와 @Async의 사용으로 데이터 처리 시간을 기존 100초에서 15초로 단축했고, 이후 WebClient 도입으로 15초에서 2.8초로 줄여, 총 처리 시간을 약 97.2% 단축할 수 있게되었다.
'개발 이야기 > Spring' 카테고리의 다른 글
Spring paging count query 조건 (0) | 2024.03.29 |
---|---|
ResponseEntityExceptionHandler 사용하는 이유 (1) | 2024.03.15 |
Spring ThreadPoolTaskExecutor @Async 비동기 (1) | 2024.02.20 |
Spring Profile 개발 서버와 운영 서버 분리, Environment Configurations (0) | 2024.02.03 |