본문 바로가기

개념정리(Spring)

비동기 작업과 Thread Pool

비동기 작업이란 요리를 할 때, 밥을 지어놓고 다른 식재료를 준비하거나, 밥이 완성될 때까지 다른 작업을 처리하는 것과 비슷하다. 만약 작업량이 많거나 오래 걸리는 작업을 순차적으로 진행하면 응답 속도가 느려지고, 사용자 경험이 나빠질 수 있다. 또한 서버 자원이 비효율적으로 낭비될 수도 있다. 비동기 작업은 특히 파일 처리, 외부 API 호출, DB 작업 등을 실행할 때 더 필요하게 된다.

스프링에서 비동기 작업 처리하기

비동기 작업을 수행하려면 복잡한 과정이 필요할 것 같지만, 스프링에서는 @Async를 통해 쉽게 비동기 선언을 하고 작업을 비동기적으로 처리할 수 있다. 스프링에서 제공하는 비동기 작업은 ThreadPool 기반으로 실행된다.

ThreadPool이란?

Thread는 하나의 프로그램 내에서 실행되는 흐름 또는 작업 단위를 의미한다. ThreadPool은 이러한 Thread들을 효율적으로 관리하기 위한 개념이다. ThreadPool은 작업이 들어올 때 마다 Thread를 생성하는 것이 아니라 미리 정해진 개수만큼 Thread를 만들어놓고 작업이 들어오면 할당해준다. 또한 작업이 완료된 Thread 들도 다음 작업이 들어올 때 재사용하게 된다. 때문에 단순히 Thread를 직접 생성해서 사용하기보다는 ThreadPool을 활용하는 것이 더 효율적이다.

ThreadPool을 사용하는 이유

  1. 매번 새로운 Thread를 생성하는 비용을 절약할 수 있다.
  2. 동시에 처리할 수 있는 작업의 개수를 조절할 수 있다.
  3. 일정 개수의 Thread만 사용하여 리소스 낭비를 방지할 수 있다.

ThreadPool 설정하기

@Configuration
@EnableAsync
public class CompleteFutureConfig {
    private static final int CORE_POOL_SIZE = 10;
    private static final int MAX_POOL_SIZE = 10;

    @Bean(name = "customFutureThreadPoolExecutor")
    public Executor customFutureThreadPoolExecutor() {
        ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
        threadPoolExecutor.setCorePoolSize(CORE_POOL_SIZE);
        threadPoolExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        threadPoolExecutor.setThreadNamePrefix("likelion-future-");
        threadPoolExecutor.setQueueCapacity(50);
        threadPoolExecutor.setKeepAliveSeconds(60);
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolExecutor.initialize();
        return threadPoolExecutor;
    }
}

위 코드는 기본적인 ThreadPool 설정을 위한 설정 파일이다. @Configuration을 통해 설정 타입의 Bean임을 선언하고, @EnableAsync를 추가하여 비동기 작업이 가능하도록 한다.

 

CompletableFuture를 활용한 비동기 작업

하지만 @Async만 사용하면 결과값을 다루기 어렵고, 작업 간 연속적인 처리가 불편하며, 예외 처리가 복잡하다는 단점이 있다. 이를 해결하기 위해 Future 객체를 활용할 수 있지만, Future는 추가적인 비동기 작업을 연결하기 어렵다. 이러한 문제를 보완하기 위해 Java 8부터 CompletableFuture가 제공되었다.

CompletableFuture는 체이닝 방식을 활용하여 여러 개의 비동기 작업을 쉽게 연결할 수 있다.

CompletableFuture의 주요 기능

  • thenApply(), thenCombine() 등을 사용해 비동기 작업을 연결할 수 있다.
  • thenAccept(), thenApply() 등을 활용하여 결과값을 변환하고 처리할 수 있다.
  • exceptionally(), handle()을 통해 예외 처리가 가능하다.
  • allOf(), anyOf()를 사용하여 여러 개의 비동기 작업을 조합하거나 동시에 실행할 수 있다.

CompletableFuture 예제 코드

CompletableFuture.supplyAsync(() -> "데이터 가져오기")
    .thenApply(data -> data + " → 데이터 처리")
    .thenApply(data -> data + " → 완료")
    .thenAccept(result -> System.out.println(result)); // 최종 결과 출력

위 코드는 다음과 같은 순서로 실행된다.

  1. supplyAsync에서 가장 무거운 작업을 실행하여 데이터를 가져온다.
  2. thenApply에서 데이터를 변환하여 추가 처리를 한다.
  3. thenApply를 한 번 더 사용하여 데이터를 최종적으로 완성한다.
  4. thenAccept를 사용하여 결과를 출력한다.

예를 들어 사용해보면,

CompletableFuture.supplyAsync(() -> {return "Hello";})
	.thenApply(result -> {return result + " World";})
    .thenAccept(System.out::println);

이런 식으로 사용할 수 있고, 결과 값으로는 "Hello World"가 print된다. 이러한 구조에서 @Async를 함께 사용하면 작업이 비동기적으로 실행되며, 이를 위해 앞에서 설정한 @EnableAsync 설정이 필요하다.

마무리

비동기 작업은 서버의 성능을 향상시키고, 사용자 경험을 개선하는 데 중요한 역할을 한다. 스프링에서 @Async를 활용하면 간단하게 비동기 처리를 할 수 있으며, 보다 세밀한 처리가 필요할 경우 CompletableFuture를 활용하면 유연하게 작업을 조합할 수 있다. ThreadPool을 적절히 활용하여 효율적으로 쓰레드를 관리하고, 안정적인 서비스를 구축하는 데 사용하면 좋을 것 같다.

'개념정리(Spring)' 카테고리의 다른 글

Spring Test  (0) 2025.04.07