
Spring Boot 애플리케이션에서 멀티스레딩을 도입하면 I/O 바운드 또는 CPU 집약적인 작업의 성능을 크게 향상시킬 수 있습니다.
작업을 여러 스레드에 분산시켜 더 짧은 시간에 완료할 수 있으며, 이는 특히 대용량 처리나 실시간 응답이 필요한 서비스에서 필수적인 기술입니다.
Spring Boot에서 멀티스레딩과 비동기 처리를 구현하는 방법은 다양합니다.
각 방법은 상황과 요구사항에 따라 선택할 수 있습니다.
Spring에서는 메서드를 비동기적으로 실행하기 위해 @Async 어노테이션을 제공합니다.
이 어노테이션을 사용하면 별도의 스레드에서 메서드가 실행되며, 특히 "fire-and-forget" 시나리오(작업을 시작하고 완료 여부를 신경쓰지 않는 상황)에 유용합니다.
@Service
public class AsyncService {
@Async
public void performTask(int id) {
// 비동기로 실행될 코드
}
}
Java의 CompletableFuture는 비동기 프로그래밍을 위한 강력한 도구입니다.
여러 비동기 작업을 연결(체이닝)하고 결과를 처리할 수 있는 기능을 제공합니다.
@Service
public class AsyncService {
@Async
public CompletableFuture<String> asyncMethod() {
// 작업 수행
return CompletableFuture.completedFuture("결과");
}
}
스레드 관리에 더 많은 제어가 필요한 경우, ThreadPoolTaskExecutor에 작업을 직접 제출할 수 있습니다.
@Autowired
private ThreadPoolTaskExecutor executor;
public void executeTask() {
executor.submit(() -> {
// 작업 코드
});
}
주기적이거나 지연된 작업을 위해 ScheduledExecutorService를 사용할 수 있습니다
@Autowired
private ScheduledExecutorService scheduler;
public void scheduleTask() {
scheduler.schedule(() -> {
// 지연 작업 코드
}, 5, TimeUnit.SECONDS);
}
Spring은 비동기 작업 실행을 위한 TaskExecutor 인터페이스를 제공합니다.
@Autowired
private TaskExecutor taskExecutor;
public void executeTask() {
taskExecutor.execute(() -> {
// 작업 코드
});
}
리액티브 Spring Boot 애플리케이션을 구축하는 경우, Project Reactor를 사용하여 비동기 작업을 처리할 수 있습니다.
public Mono<String> reactiveMethod() {
return Mono.fromCallable(() -> {
// 작업 코드
return "결과";
}).subscribeOn(Schedulers.boundedElastic());
}
이제 @Async 어노테이션을 사용하여 Spring Boot 애플리케이션에서 멀티스레딩을 구현하는 과정을 단계별로 살펴보겠습니다.
Spring Boot 프로젝트에 Lombok과 필요한 스프링 의존성을 추가합니다.
<!-- Maven -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
비동기 작업을 처리할 스레드 풀을 설정하는 구성 클래스를 작성합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 기본 스레드 수
executor.setMaxPoolSize(10); // 최대 스레드 수
executor.setQueueCapacity(25); // 대기 큐 용량
executor.setThreadNamePrefix("Async-"); // 스레드 이름 접두사
executor.initialize();
return executor;
}
}
CPU 집약적이거나 I/O 바운드 작업을 시뮬레이션하는 비동기 서비스를 구현합니다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class AsyncService {
@Async("taskExecutor")
public void performTask(int id) {
log.info("Task {} is starting. Thread: {}", id, Thread.currentThread().getName());
try {
// 5초간 작업 시뮬레이션
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Task {} is completed. Thread: {}", id, Thread.currentThread().getName());
}
}
비동기 서비스를 호출하는 REST API를 구현합니다.
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class AsyncController {
private final AsyncService asyncService;
@GetMapping("/tasks")
public String executeTasks() {
for (int i = 1; i <= 10; i++) {
asyncService.performTask(i);
}
return "10개의 작업이 시작되었습니다. 콘솔에서 진행 상황을 확인하세요.";
}
}
비동기 처리 기능을 활성화하기 위해 메인 클래스에 @EnableAsync 어노테이션을 추가합니다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync // 비동기 처리 활성화
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
애플리케이션을 실행한 후, 브라우저나 Postman을 통해 http://localhost:8080/api/tasks 엔드포인트에 GET 요청을 보내면 10개의 작업이 비동기적으로 시작됩니다.
콘솔에서 다음과 같은 로그를 확인할 수 있습니다.
Task 1 is starting. Thread: Async-1
Task 2 is starting. Thread: Async-2
Task 3 is starting. Thread: Async-3
Task 4 is starting. Thread: Async-4
Task 5 is starting. Thread: Async-5
Task 6 is starting. Thread: Async-6
...
Task 1 is completed. Thread: Async-1
Task 3 is completed. Thread: Async-3
...
만약 @EnableAsync 어노테이션을 제거하거나 메서드에서 @Async 어노테이션을 제거하면, 모든 작업이 메인 스레드에서 순차적으로 실행되어 총 50초(10개 작업 × 5초)가 소요됩니다.
반면, 비동기 처리를 활성화하면 구성된 스레드 풀(위 예시에서는 최대 10개 스레드)에 따라 작업이 병렬로 실행되어 약 5초 내에 모든 작업이 완료됩니다.
비동기 메서드에서 발생한 예외는 호출자에게 전파되지 않으므로, 적절한 예외 처리 전략이 필요합니다.
@Async
public CompletableFuture<String> asyncMethodWithExceptionHandling() {
try {
// 작업 코드
return CompletableFuture.completedFuture("결과");
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
스레드 풀의 크기를 적절히 설정하는 것이 중요합니다.
너무 작으면 병렬 처리의 이점을 활용할 수 없고, 너무 크면 리소스 소모가 증가합니다.
일반적으로 CPU 코어 수를 기준으로 설정합니다.
int processors = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(processors);
executor.setMaxPoolSize(processors * 2);
비동기 메서드에서는 트랜잭션 전파가 예상대로 작동하지 않을 수 있습니다.
각 비동기 메서드는 별도의 트랜잭션 컨텍스트에서 실행되므로, 필요한 경우 명시적인 트랜잭션 관리가 필요합니다.
Spring Boot에서 멀티스레딩을 구현함으로써 다음과 같은 장점을 얻을 수 있습니다.
적절한 비동기 처리 방법을 선택하여 애플리케이션의 성능을 최적화하고, 사용자 경험을 향상시킬 수 있습니다.