문제 상황
- 환경: 폐쇄망(인터넷이 차단된 내부망)에서 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;
}
}