공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
비동기 프로그래밍이란
- 비동기 프로그래밍은 실시간 응답이 필요하지 않은 상황에서 주로 사용된다.
- 예시로 Notification, Email 전송, Push 알림 등이 있다.
- 비동기 프로그래밍을 개발자 관점에서 설명하자면, 메인 스레드가 아닌 서브 스레드에게 작업을 위임하는 행위라고 할 수 있다.
Spring에서 비동기 프로그래밍
- Spring Framework를 사용하면 비동기 프로그래밍을 구현할 수 있다.
- 이를 위해서는 ThreadPool을 정의할 필요가 있다.
ThreadPool 생성이 필요한 이유
- 비동기 작업은 메인 스레드가 아닌 서브 스레드에서 작업을 진행한다.
- Java에서는 ThreadPool을 생성하여 이러한 비동기 작업을 처리한다.
ThreadPool 생성 옵션
- CorePoolSize: 해당 스레드 풀에서 최소한으로 유지해야 할 스레드 수를 지정한다.
- MaxPoolSize: 최대 할당할 수 있는 스레드 수를 지정한다.
- KeepAliveTime: 스레드가 작업을 하지 않을 경우, 유지될 시간을 설정한다.
- unit: KeepAliveTime의 시간 단위를 지정한다.
- WorkQueue: 대기 중인 작업을 담아놓는 큐이다.
순서에 대해서 설명하자면, 다음과 같다.
- 먼저, CorePoolSize만큼의 스레드를 생성한다.
- 작업이 CorePoolSize를 초과하면 WorkQueue에 작업을 대기시킨다.
- WorkQueue가 가득 차면, MaxPoolSize까지 스레드를 늘려 작업을 처리한다.
- MaxPoolSize에 도달했을 때 작업이 더 이상 처리되지 않으면 RejectedExecutionHandler가 동작하게 된다.
ThreadPoolExecutor 생성자
코드에서 사용된 ThreadPoolExecutor 생성자를 설명하겠다.
위 코드의 의미는 다음과 같다.
- CorePoolSize = 5: 최소 5개의 스레드를 유지한다.
- MaxPoolSize = 10: 최대 10개의 스레드를 생성할 수 있다.
- KeepAliveTime = 3초: 5개 이상의 스레드는 3초 동안 대기 상태일 경우 제거된다.
- WorkQueue = ArrayBlockingQueue(50): 최대 50개의 작업을 대기시킬 수 있는 큐다.
즉, 5개의 스레드는 항상 유지되며, 대기 중인 작업이 있을 경우 최대 10개까지 스레드가 생성된다. 하지만, 5개를 초과한 스레드는 3초 동안 작업이 없을 경우 제거되며, 큐에는 최대 50개의 작업이 대기할 수 있다.
ThreadPool 생성 시 주의해야 할 부분
- CorePoolSize 값을 너무 크게 설정할 경우 성능 저하 등의 부작용이 발생할 수 있다.
- IllegalArgumentException : 다음 조건 중 하나라도 성립하면 발생한다.
- NullPointerException : workQueue가 null일 경우 발생한다.
ThreadPool 정리
CorePoolSize
- CorePoolSize는 스레드 풀에서 항상 유지되는 최소한의 스레드 수를 의미한다.
- 스레드 수가 CorePoolSize보다 작을 경우 새로운 스레드를 생성하여 작업을 처리한다.
- 스레드 수가 CorePoolSize보다 많으면, 새로 생성하지 않고 대기 큐(WorkQueue)에 작업을 추가한다.
조건 요약
- if (Thread 수 < CorePoolSize): 새로운 스레드를 생성한다.
- if (Thread 수 >= CorePoolSize): 대기 큐에 작업을 추가한다.
MaxPoolSize
- MaxPoolSize는 스레드 풀에서 생성할 수 있는 최대 스레드 수를 의미한다.
- 대기 큐(WorkQueue)가 가득 차 있고, 현재 스레드 수가 MaxPoolSize보다 작을 경우 새로운 스레드를 생성하여 작업을 처리한다.
- 대기 큐가 가득 차 있고, 현재 스레드 수가 MaxPoolSize보다 크거나 같을 경우 요청이 거절된다.
- if (Queue Full && Thread 수 < MaxPoolSize): 새로운 스레드를 생성한다.
- if (Queue Full && Thread 수 >= MaxPoolSize): 요청이 거절된다.
조건 요약
- if (Queue Full && Thread 수 < MaxPoolSize): 새로운 스레드를 생성한다.
- if (Queue Full && Thread 수 >= MaxPoolSize): 요청이 거절된다.
실습
비동기 프로그래밍을 위해 프로젝트를 생성한다.
프로젝트에서 비동기 프로그래밍을 구현하기 위해 다음과 같은 패키지 구조를 만든다.
- config: 설정 파일들을 포함.
- controller: 컨트롤러 클래스를 포함.
- service: 서비스 로직을 포함.
AppConfig 클래스
비동기 처리를 위한 스레드 풀을 정의한다.
package dev.be.async.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class AppConfig {
@Bean(name = "defaultTaskExecutor")
public ThreadPoolTaskExecutor defaultTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(200);
executor.setMaxPoolSize(300);
return executor;
}
@Bean(name = "messagingTaskExecutor", destroyMethod = "shutdown")
public ThreadPoolTaskExecutor messagingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(200);
executor.setMaxPoolSize(300);
return executor;
}
}
- defaultTaskExecutor와 messagingTaskExecutor 두 개의 스레드 풀을 정의하였다.
AsyncConfig 클래스
비동기 처리가 가능하도록 설정을 추가한다.
package dev.be.async.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
}
- @EnableAsync 애노테이션을 사용하여 Spring에서 비동기 메서드를 활성화한다.
AsyncController 클래스
세 가지 테스트 케이스를 확인하기 위한 컨트롤러를 구현한다.
package dev.be.async.controller;
import dev.be.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AsyncController {
private final AsyncService asyncService;
@GetMapping("/1")
public String asyncCall_1() {
asyncService.asyncCall_1();
return "success";
}
@GetMapping("/2")
public String asyncCall_2() {
asyncService.asyncCall_2();
return "success";
}
@GetMapping("/3")
public String asyncCall_4() {
asyncService.asyncCall_3();
return "success";
}
}
AsyncService 클래스
서비스 로직을 구현하며, 비동기 작업을 수행한다.
package dev.be.async.service;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AsyncService {
private final EmailService emailService;
public void asyncCall_1() {
System.out.println("[asyncCall_1] :: " + Thread.currentThread().getName());
emailService.sendMail();
emailService.sendMailWithCustomThreadPool();
}
public void asyncCall_2() {
System.out.println("[asyncCall_2] :: " + Thread.currentThread().getName());
EmailService emailService = new EmailService();
emailService.sendMail();
emailService.sendMailWithCustomThreadPool();
}
public void asyncCall_3() {
System.out.println("[asyncCall_3] :: " + Thread.currentThread().getName());
sendMail();
}
@Async
public void sendMail() {
System.out.println("[sendMail] :: " + Thread.currentThread().getName());
}
}
EmailService 클래스
비동기 메서드를 정의하고, 특정 스레드 풀을 사용하도록 설정한다.
package dev.be.async.service;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class EmailService {
@Async("defaultTaskExecutor")
public void sendMail() {
System.out.println("[sendMail] :: " + Thread.currentThread().getName());
}
@Async("messagingTaskExecutor")
public void sendMailWithCustomThreadPool() {
System.out.println("[messagingTaskExecutor] :: " + Thread.currentThread().getName());
}
}
실행 결과 및 분석
첫 번째 경우: 스프링이 비동기로 처리하도록 설정된 메서드에서 컨테이너에 등록된 프록시 객체를 사용하여 비동기 작업을 수행한다. 이 경우, 스레드 이름이 다르게 출력되어 비동기 처리가 정상적으로 이루어졌다.
두 번째 경우: 직접 생성된 인스턴스를 사용하여 비동기 처리가 이루어지지 않았다. 동일한 스레드에서 동작하게 되었다.
세 번째 경우: 동일한 인스턴스 내에서 직접 메서드를 호출한 경우, 비동기 처리가 되지 않았다.
결론
- 비동기 프로그래밍에서 스프링 컨테이너에 등록된 빈을 사용하는 것이 중요하다.
- 비동기 프로그래밍은 개발 환경에서 문제가 되지 않더라도, 실무에서는 장애가 발생할 수 있으므로, 스레드 네임을 통해 비동기 동작을 확인하는 것이 필요하다.
'BackEnd > Project' 카테고리의 다른 글
[RealPJ] Ch02. 실무 스타일로 Feign Client 사용해보기 - Interceptor (0) | 2024.08.30 |
---|---|
[RealPJ] Ch02. 실무 스타일로 Feign Client 사용해보기 - 기본 설정 (0) | 2024.08.30 |
[RealPJ] Ch01. Profile 설정 (0) | 2024.08.30 |
[RealPJ] Ch01. Spring Multi Module 개념 및 실습 (2) | 2024.08.30 |
[RealPJ] Ch01. Git Flow 전략 알아보기 (0) | 2024.08.29 |