📌 1. 애플리케이션 성능 향상을 위한 멀티스레딩

Spring Boot 애플리케이션에서 멀티스레딩을 도입하면 I/O 바운드 또는 CPU 집약적인 작업의 성능을 크게 향상시킬 수 있습니다.

작업을 여러 스레드에 분산시켜 더 짧은 시간에 완료할 수 있으며, 이는 특히 대용량 처리나 실시간 응답이 필요한 서비스에서 필수적인 기술입니다.

📌 2. Spring Boot에서 비동기 처리를 구현하는 다양한 방법

Spring Boot에서 멀티스레딩과 비동기 처리를 구현하는 방법은 다양합니다.
각 방법은 상황과 요구사항에 따라 선택할 수 있습니다.

1) @Async 어노테이션 사용하기

Spring에서는 메서드를 비동기적으로 실행하기 위해 @Async 어노테이션을 제공합니다.
이 어노테이션을 사용하면 별도의 스레드에서 메서드가 실행되며, 특히 "fire-and-forget" 시나리오(작업을 시작하고 완료 여부를 신경쓰지 않는 상황)에 유용합니다.

@Service
public class AsyncService {
    @Async
    public void performTask(int id) {
        // 비동기로 실행될 코드
    }
}

2) CompletableFuture 활용하기

Java의 CompletableFuture는 비동기 프로그래밍을 위한 강력한 도구입니다.
여러 비동기 작업을 연결(체이닝)하고 결과를 처리할 수 있는 기능을 제공합니다.

@Service
public class AsyncService {
    @Async
    public CompletableFuture<String> asyncMethod() {
        // 작업 수행
        return CompletableFuture.completedFuture("결과");
    }
}

3) ThreadPoolTaskExecutor 직접 사용하기

스레드 관리에 더 많은 제어가 필요한 경우, ThreadPoolTaskExecutor에 작업을 직접 제출할 수 있습니다.

@Autowired
private ThreadPoolTaskExecutor executor;

public void executeTask() {
    executor.submit(() -> {
        // 작업 코드
    });
}

4) ScheduledExecutorService 사용하기

주기적이거나 지연된 작업을 위해 ScheduledExecutorService를 사용할 수 있습니다

@Autowired
private ScheduledExecutorService scheduler;

public void scheduleTask() {
    scheduler.schedule(() -> {
        // 지연 작업 코드
    }, 5, TimeUnit.SECONDS);
}

5) Spring의 TaskExecutor 인터페이스 활용하기

Spring은 비동기 작업 실행을 위한 TaskExecutor 인터페이스를 제공합니다.

@Autowired
private TaskExecutor taskExecutor;

public void executeTask() {
    taskExecutor.execute(() -> {
        // 작업 코드
    });
}

6) Project Reactor를 통한 리액티브 프로그래밍

리액티브 Spring Boot 애플리케이션을 구축하는 경우, Project Reactor를 사용하여 비동기 작업을 처리할 수 있습니다.

public Mono<String> reactiveMethod() {
    return Mono.fromCallable(() -> {
        // 작업 코드
        return "결과";
    }).subscribeOn(Schedulers.boundedElastic());
}

📌 3. @Async 어노테이션을 활용한 멀티스레딩 구현

이제 @Async 어노테이션을 사용하여 Spring Boot 애플리케이션에서 멀티스레딩을 구현하는 과정을 단계별로 살펴보겠습니다.

3.1. 프로젝트 설정

필요한 의존성 추가

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>

3.2. 스레드 풀 구성 클래스 생성

비동기 작업을 처리할 스레드 풀을 설정하는 구성 클래스를 작성합니다.

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;
    }
}

3.3. 비동기 서비스 클래스 생성

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());
    }
}

3.4. REST 컨트롤러 구현

비동기 서비스를 호출하는 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개의 작업이 시작되었습니다. 콘솔에서 진행 상황을 확인하세요.";
    }
}

3.5. 메인 애플리케이션 클래스 - @EnableAsync 추가

비동기 처리 기능을 활성화하기 위해 메인 클래스에 @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);
    }
}

📌 4. 테스트 및 결과 분석

4.1. API 호출 및 결과 확인

애플리케이션을 실행한 후, 브라우저나 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
...

4.2. 동기 vs 비동기 처리 비교

만약 @EnableAsync 어노테이션을 제거하거나 메서드에서 @Async 어노테이션을 제거하면, 모든 작업이 메인 스레드에서 순차적으로 실행되어 총 50초(10개 작업 × 5초)가 소요됩니다.

반면, 비동기 처리를 활성화하면 구성된 스레드 풀(위 예시에서는 최대 10개 스레드)에 따라 작업이 병렬로 실행되어 약 5초 내에 모든 작업이 완료됩니다.

📌 5. 주의사항 및 고려사항

5.1. 예외 처리

비동기 메서드에서 발생한 예외는 호출자에게 전파되지 않으므로, 적절한 예외 처리 전략이 필요합니다.

@Async
public CompletableFuture<String> asyncMethodWithExceptionHandling() {
    try {
        // 작업 코드
        return CompletableFuture.completedFuture("결과");
    } catch (Exception e) {
        return CompletableFuture.failedFuture(e);
    }
}

5.2. 스레드 풀 크기 설정

스레드 풀의 크기를 적절히 설정하는 것이 중요합니다.
너무 작으면 병렬 처리의 이점을 활용할 수 없고, 너무 크면 리소스 소모가 증가합니다.
일반적으로 CPU 코어 수를 기준으로 설정합니다.

int processors = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(processors);
executor.setMaxPoolSize(processors * 2);

5.3. 트랜잭션 관리

비동기 메서드에서는 트랜잭션 전파가 예상대로 작동하지 않을 수 있습니다.
각 비동기 메서드는 별도의 트랜잭션 컨텍스트에서 실행되므로, 필요한 경우 명시적인 트랜잭션 관리가 필요합니다.

📌 6. 결론 및 활용 사례

Spring Boot에서 멀티스레딩을 구현함으로써 다음과 같은 장점을 얻을 수 있습니다.

  • 성능 향상: I/O 바운드 작업(파일, 데이터베이스, API 호출 등)의 응답 시간 개선
  • 리소스 활용: CPU 및 시스템 리소스의 효율적인 활용
  • 사용자 경험: 웹 애플리케이션의 응답 시간 개선

다음과 같은 상황에서 멀티스레딩을 고려해볼 수 있습니다

  • 대량의 이메일 발송
  • 파일 처리 및 업로드
  • 외부 API 호출
  • 배치 작업 처리
  • 실시간 데이터 처리

적절한 비동기 처리 방법을 선택하여 애플리케이션의 성능을 최적화하고, 사용자 경험을 향상시킬 수 있습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글