카테고리 없음

Java ExecutorService를 활용한 API 병렬 호출

Code Canvas 2025. 7. 21. 10:06

문제 상황

  • 환경: 폐쇄망(인터넷이 차단된 내부망)에서 RestTemplate을 활용해 UKM 솔루션 API를 호출
  • UKM (Universal SSH Key Manager): SSH 키 수명 주기 관리를 중앙화·자동화하는 솔루션으로, 키 삭제 시 서버 접속 → 키 삭제 → 서버 종료 등 여러 내부 프로세스를 수행하기 때문에 응답 속도가 느림.

 

초기 성능

  • 200건의 API 호출 완료까지 약 1분 소요
  • 동일 방식으로 1,000건 이상 API를 호출할 경우, 매우 긴 처리 시간이 걸릴 것으로 예상됨

 

성능 개선 시도

  • 여러 자료를 검토한 결과, Java의 ExecutorService(스레드 풀)를 활용하면 대량 API 호출 속도를 병렬 처리 방식으로 대폭 개선할 수 있음을 확인.
  • 기존 순차 처리 로직을 멀티스레드 기반 병렬 호출 방식으로 변경

 

개선 결과

  • 기존: 200건 API 호출 → 약 1분 소요
  • 개선 후: 200건 API 호출 → 약 3초 소요
  • 약 20배 성능 향상 달성.

 

ExecutorService

  • 개념
    • Java의 비동기 작업 실행 프레임워크
    • 개발자가 직접 Thread를 생성/관리하지 않고, 스레드 풀(Thread Pool) 기반으로 병렬 작업을 처리
  • 특징 
    • 스레드 풀 관리
      • 한 번 생성한 스레드를 재사용해 성능과 자원 사용 최적화
    • Future와 함께 사용
      • 작업 완료 여부 및 결과를 쉽게 가져올 수 있음
    • 타임아웃 제어 가능
      • future.get(30, TimeUnit.SECONDS)처럼 작업 제한 가능
메서드 설명 사용예시
Exectuors.newFixedThreadPool(n) 고정된 n 개의 스레드 풀  API 병렬 호출에 적합
Exectuors.newCachedThreadPool() 필요 시 스레드를 무제한 생성, 유후시 제거 단기 brust 작업
Exectuors.newSingleThreadExecutor() 단일 스레드 순차적 작업에 적합


Future

  • Java의 비동기 작업 결과를 담는 객체
  • 특징
    • 작업 완료 여부 확인 
      • future.isDone() 
    • 작업 결과 가져오기 
      • future.get()
      • future.get(timeout, TimeUnit.SECONDS)
    • 작업 취소
      • future.cancel()

 

HttpComponentsClientHttpRequestFactory

  • RestTemplate이 내부적으로 사용하는 HTTP 요청처리용 팩토리 클래스
  • HttpComponentsClientHttpRequestFactory는 Apache HttpClient 기반의 커넥션 풀의 사용하기 때문에 병렬 성능이 높음
  • RestTemplate은 기본적으로 SimpleClientHttpRequestFactory(매 요청마다 새커넥션) 사용 하기때문에 병렬 성능이 낮음

 

예시코드

import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.apache.http.impl.client.HttpClients;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

import java.util.*;
import java.util.concurrent.*;

@Bean
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setHttpClient(HttpClients.custom().build());
    factory.setMaxConnTotal(200);        // 전체 최대 연결 수
    factory.setMaxConnPerRoute(100);     // 동일 도메인 최대 동시 연결 수
    factory.setConnectTimeout(3000);     // 연결 타임아웃 (ms)
    factory.setReadTimeout(5000);        // 응답 타임아웃 (ms)
    return new RestTemplate(factory);
}

@Service
@RequiredArgsConstructor
public class AsyncApiCallService {

    private final RestTemplate restTemplate;

    public void executeApiCall(String url, Integer id,
                               List<Integer> successList,
                               List<String> failList) {
        try {
            HttpEntity<String> entity = new HttpEntity<>(null, createHeaders());

            ResponseEntity<String> response =
                    restTemplate.exchange(url, HttpMethod.POST, entity, String.class);

            if (response.getStatusCode().is2xxSuccessful()) {
                successList.add(id);
            } else {
                failList.add("ID: " + id + " | Status: " + response.getStatusCode());
            }
        } catch (Exception e) {
            failList.add("ID: " + id + " | Error: " + e.getMessage());
        }
    }

    private HttpHeaders createHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer your_token_here"); // 필요시 토큰 교체
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }
}

@Service
@RequiredArgsConstructor
public class UkmKeyDeleteService {

    private final AsyncApiCallService asyncApiCallService;

    public Map<String, Object> deleteKeys(List<Integer> ids) throws InterruptedException {
        List<Integer> successList = Collections.synchronizedList(new ArrayList<>());
        List<String> failList = Collections.synchronizedList(new ArrayList<>());

        // 스레드 풀(최대 100개 동시 실행)
        ExecutorService executor = Executors.newFixedThreadPool(100);
        List<Future<?>> futures = new ArrayList<>();

        for (Integer id : ids) {
            futures.add(executor.submit(() -> {
                String url = String.format("http://xxx.xxx.xxx/v1/%d/test", id);
                asyncApiCallService.executeApiCall(url, id, successList, failList);
            }));
        }

        // 각 요청 타임아웃 30초
        for (Future<?> future : futures) {
            try {
                future.get(30, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                failList.add("Timeout Error: 30초 초과");
                future.cancel(true);
            } catch (Exception e) {
                failList.add("Future Error: " + e.getMessage());
            }
        }

        // 전체 작업 최대 5분 대기
        executor.shutdown();
        if (!executor.awaitTermination(5, TimeUnit.MINUTES)) {
            failList.add("Global Timeout: 5분 초과");
            executor.shutdownNow();
        }

        Map<String, Object> result = new HashMap<>();
        result.put("successCount", successList.size());
        result.put("failCount", failList.size());
        result.put("failedDetails", failList);
        return result;
    }
}