본문 바로가기
SpringBoot

스프링 부트에서 외부 API 서비스를 병렬 처리하여 응답하는 방법

by ByteBridge 2023. 10. 9.
반응형

많은 서비스에서 다양한 자산 정보를 사용자에게 제공해야 하는 경우가 있습니다.

예를 들어, 사용자가 보유한 적립금, 쿠폰 개수, 코인 수량 등의 정보를 한번의 요청으로 빠르게 얻고 싶어합니다.
스프링 부트와 자바를 활용하여 이런 자산 정보들을 병렬로 처리하고, 그 결과를 사용자에게 효과적으로 제공하는 방법에 대해 살펴보겠습니다.

스프링 부트에서는 @Async 어노테이션과 CompletableFuture를 활용하여 쉽게 비동기 호출을 구현할 수 있습니다.
이를 통해 여러 API 호출을 동시에 수행하고, 모든 결과를 기다린 뒤 하나로 합치는 병렬 처리를 구현할 수 있습니다.

본 글에서는 적립금, 쿠폰 개수, 코인 수량과 같은 사용자의 다양한 자산 정보를 병렬로 조회 및 처리하여 응답하는 API 서비스 구현에 대해 설명합니다.
1. 환경 설정

프로젝트 세팅: 스프링 부트 초기화(Spring Boot Initializer)를 통해 웹(Web) 의존성을 포함한 프로젝트를 생성합니다.
비동기 지원 활성화: @EnableAsync 어노테이션과 함께 적절한 Executor 설정을 추가합니다.
 
주의점
병렬 API 호출이므로 외부 시스템의 부하를 고려해야 합니다.
 
@Async를 사용할 때는 메서드 호출이 프록시 기반으로 이루어지기 때문에 동일 클래스 내부에서 @Async 메서드를 다른 메서드에서 직접 호출하는 것은 비동기로 작동하지 않습니다.
 
외부 API의 응답 시간이 길거나, 대량의 사용자가 동시에 요청할 경우, ThreadPool의 크기와 QueueCapacity를 적절히 설정하는 것이 중요합니다.
 
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "asyncExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.initialize();
        return executor;
    }
}
 
 
2. 응답 객체 DTO 생성
 
// UserAssetDto.java
public class UserAssetDto {
    private SaveMoneyDto saveMoney;
    private CouponDto coupon;
    private CoinDto coin;

    // Getters and Setters...
}

// SaveMoneyDto.java
public class SaveMoneyDto {
    private boolean status;
    private int amount;

    // Getters and Setters...
}

// CouponDto.java
public class CouponDto {
    private boolean status;
    private int count;

    // Getters and Setters...
}

// CoinDto.java
public class CoinDto {
    private boolean status;
    private double quantity;

    // Getters and Setters...
}
 
 
3. 비동기 서비스 구현
각 자산 정보를 조회하는 서비스를 비동기로 구현합니다.
 
@Service
public class AssetService {

    @Async("asyncExecutor")
    public CompletableFuture<SaveMoneyDto> getSavedMoney(String userId) {
        // 외부 API 호출 로직 (적립금 조회)
        // return CompletableFuture.completedFuture(new SaveMoneyDto(status, amount));
    }

    @Async("asyncExecutor")
    public CompletableFuture<CouponDto> getCouponCount(String userId) {
        // 외부 API 호출 로직 (쿠폰 개수 조회)
        // return CompletableFuture.completedFuture(new CouponDto(status, count));
    }

    @Async("asyncExecutor")
    public CompletableFuture<CoinDto> getCoinQuantity(String userId) {
        // 외부 API 호출 로직 (코인 수량 조회)
        // return CompletableFuture.completedFuture(new CoinDto(status, quantity));
    }
}
 
 
4. API 컨트롤러 구현
외부 API를 병렬로 호출하고, 그 결과를 UserAssetDto 객체로 응답합니다.
 
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private AssetService assetService;

    @GetMapping("/assets")
    public ResponseEntity<UserAssetDto> getUserAssets(@RequestParam String userId) {
        try {
            CompletableFuture<SaveMoneyDto> savedMoney = assetService.getSavedMoney(userId);
            CompletableFuture<CouponDto> couponCount = assetService.getCouponCount(userId);
            CompletableFuture<CoinDto> coinQuantity = assetService.getCoinQuantity(userId);

            // 모든 비동기 작업이 완료될 때까지 기다림
            CompletableFuture.allOf(savedMoney, couponCount, coinQuantity).join();

            UserAssetDto userAssetDto = new UserAssetDto(
                    savedMoney.get(),
                    couponCount.get(),
                    coinQuantity.get()
            );

            return new ResponseEntity<>(userAssetDto, HttpStatus.OK);

        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
 
여기서 각 CompletableFuture 객체의 get() 메서드는 작업의 결과를 가져오며, 작업이 완료되지 않은 경우 완료될 때까지 대기합니다.
CompletableFuture.allOf(savedMoney, couponCount, coinQuantity).join();는 세 작업이 모두 완료될 때까지 기다립니다.
 
5. 성능 테스트 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StopWatch;
import static org.assertj.core.api.Assertions.assertThat;

public class UserAssetApiTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void performanceTest() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        
        // 여기서는 1000회 호출을 예시로 들고 있지만, 
        // 실제 테스트 케이스에서는 상황에 맞게 조정 필요
        for (int i = 0; i < 1000; i++) {
            ResponseEntity<UserAssetDto> responseEntity = 
                    restTemplate.getForEntity("/user/assets?userId=exampleUser", UserAssetDto.class);
            
            assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
            assertThat(responseEntity.getBody()).isNotNull();
        }

        stopWatch.stop();
        System.out.println("Total time taken for 1000 requests: " + stopWatch.getTotalTimeSeconds() + " seconds");
    }
}
 
이 테스트는 /user/assets 엔드포인트를 1000번 호출하고, 그에 걸리는 총 시간을 측정합니다.
StopWatch 클래스를 사용하여 이 시간을 측정할 수 있습니다.

성능 테스트에는 여러 변수들이 있기 때문에, 최적의 성능을 얻기 위해 다양한 조건 하에서 테스트를 반복적으로 진행해야 합니다.
또한 네트워크 환경, 하드웨어 스펙, DB 상태, 외부 API의 상태 등도 모두 고려해야 합니다.

실제 서비스 운영 환경에서는 더 복잡한 테스트 케이스들이 필요하며,
성능 테스트 도구나 서비스 (예: Apache JMeter, Gatling 등)를 이용하는 것을 추천합니다.

이렇게 성능 테스트를 진행하여, 시스템이 스케일링할 때 얼마나 안정적인지, 어디까지 버틸 수 있는지를 알아보고,
그를 기반으로 인프라와 애플리케이션을 개선해 나가도록 해야 합니다.
 
결론
스프링 부트와 자바의 CompletableFuture를 활용하여 비동기 API 호출을 병렬로 수행하고,
그 결과를 하나의 응답으로 통합하여 제공하였습니다.
이 방식을 활용하면 여러 외부 API를 호출할 때 시스템 자원을 효율적으로 활용하면서 빠른 응답 시간을 제공할 수 있습니다.
 
참고자료
Spring Boot - @Async
Java - CompletableFuture
반응형