Spring에서의 비동기 처리
안녕하세요! SSAFYcial 이예림입니다. SSAFY에서 여러 프로젝트를 진행하다보니, Spring 환경에서도 비동기 처리를 해줘야 하는 상황이 생깁니다. 각각의 기능, 비즈니스 로직에 따라 동기보다는 비동기를 적용하는 게 성능 상 유리하기도 합니다. 그렇다면 어떻게 Spring에서 비동기 처리를 해줄 수 있을지 알아보겠습니다!
1. 비동기 처리를 사용하는 이유
비동기 처리의 목적은 응답 시간을 단축하고 자원을 효율적으로 사용하는 것입니다. 동기(Synchronous) 방식에서는 작업이 끝날 때까지 기다려야 하기 때문에 다른 작업을 수행할 수 없습니다. 그렇지만, 비동기(Asynchronous) 방식에서는 메인 스레드가 작업의 종료를 기다리지 않고 다른 요청을 처리할 수 있습니다.
(1) 응답 시간 단축
첫번째로는, 응답시간이 줄어듭니다.
예를 들어, 외부 API를 호출하거나 데이터베이스 작업, Websocket 통신이 이뤄질 때는 해당 작업이 끝날 때까지 대기해야 하므로, 비동기 처리를 해주게 되면 클라이언트 응답 시간을 단축할 수 있습니다. 특히, 파일을 업로드하고 다운로드하는 I/O 작업이 많은 경우에는 비동기 처리가 유용합니다.
(2) 자원 관리 효율
두번째로, 자원 관리를 효율적으로 할 수 있습니다.
동기 방식에서는 스레드를 많이 생성하게 되면서, 시스템 자원을 많이 사용합니다. 반대로, 비동기 처리 방식에서는 작업이 끝날 때까지 스레드를 차지하지 않기 때문에 시스템 자원을 효율적으로 사용할 수 있습니다. 이를 통해 서버의 확장성을 높일 수 있어 많은 요청을 처리할 수 있답니다.
2. 비동기 처리 시 고려해야 할 점
그렇지만 무작정 응답 시간을 단축하기 위해 비동기 처리를 도입하면 안됩니다. 사용할 때 아래 사항을 주의하면서 도입해야 최대의 효율을 낼 수 있습니다.
(1) 코드 복잡도
비동기 처리를 하게 되면, 여러 개의 스레드가 동시에 실행됩니다. 이를 관리하기 위해 어쩔 수 없이 코드가 복잡해지게 됩니다. 그만큼 디버깅이 어려워질 수 있습니다.
(2) 동시성 문제
이렇게 여러 스레드가 동시에 실행되고, 다 같이 동시에 접속하는 경우 같은 데이터를 처리하여도 결과가 일치하지 않는 경우가 발생할 수 있습니다. 그러니 이러한 것을 고려하여 코드를 작성해야 합니다.
(3) 메모리 문제
스레드를 생성하고 관리하는 데에도 어느 정도의 오버헤드가 발생합니다. 또, 스레드 풀로 스레드를 생성하면 그 스레드는 다른 스레드와 동일한 메모리 공간을 공유하게 되는데, 이로 인해 메모리 문제가 발생할 수도 있습니다! 따라서 처리량이 많아지면 오히려 전체 성능이 떨어질 수 있다고 합니다.
3. Spring에서의 비동기 처리 방법
(1) Spring Boot Async
Spring Boot에서도 자체적으로 비동기 프로그래밍을 지원합니다. 이를 통해 메소드 호출은 즉시 반환되고, 실제 작업은 별도 스레드에서 비동기적으로 실행할 수 있습니다.
@Async는 Spring 3.0부터 지원되는 비동기 처리 어노테이션입니다. 이 어노테이션을 통해 비동기를 적용해줄 수 있는데, 먼저 @EnableAsync를 통해 비동기 처리를 활성화 해준 후, 해당 함수에 어노테이션을 붙여주면 됩니다.
@Service
public class AsyncService {
private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);
@Async
public CompletableFuture<String> asyncMethod() throws InterruptedException {
logger.info("Start async method: {}", Thread.currentThread().getName());
Thread.sleep(2000);
logger.info("End async method: {}", Thread.currentThread().getName());
return CompletableFuture.completedFuture("Async result");
}
}
(2) Executor
Spring에서는 Executor를 통해 비동기 작업을 설정하고 관리할 수 있습니다. 이렇게 Executor를 사용하면 직접 스레드를 관리하지 않아도 비동기 작업을 실행할 수 있어 코드의 가독성과 유지보수성을 높여줍니다.
Spring에서는 기본적으로 SimpleAsyncTaskExecutor를 통해 비동기 작업을 처리해줍니다. SimpleAsyncTaskExecutor는 Spring Framework에 내장되어 있습니다. 각각의 작업을 새로운 스레드에서 실행하여 스레드 풀을 사용하지 않습니다. 따라서 다수의 작업을 동시에 실행할 수 있긴 하지만, 스레드 수가 많아질 경우 시스템의 리소스를 과도하게 사용할 수 있습니다.
그렇기 때문에, ThreadPoolTaskExecutor를 사용하기도 합니다. ThreadPoolTaskExecutor는 java.util.concurrent 패키지에 포함된 클래스 중 하나인데, 스레드 풀을 사용하여 비동기 작업을 효율적으로 처리해줍니다. 그렇기 때문에, 다수의 작업을 동시에 실행하면서도 시스템 리소스를 최적화할 수 있습니다.
아래와 같이 AsyncConfig에서 각각의 executor를 정의하고, 메소드에서 해당 executor를 명시해서 사용할 수 있습니다.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
// SimpleAsyncTaskExecutor 설정
@Bean(name = "simpleAsyncTaskExecutor")
public Executor simpleTaskExecutor() {
return new SimpleAsyncTaskExecutor();
}
// ThreadPoolTaskExecutor 설정
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3); // 최소 스레드 수
executor.setMaxPoolSize(5); // 최대 스레드 수
executor.setQueueCapacity(10); // 큐 크기
executor.setThreadNamePrefix("MyThread-");
executor.initialize();
return executor;
}
}
@Service
public class AsyncService {
@Async("simpleAsyncTaskExecutor")
public void simpleAsyncMethod() {
System.out.println("Executing simple async task in thread: " + Thread.currentThread().getName());
}
@Async("threadPoolTaskExecutor")
public void threadPoolAsyncMethod() {
System.out.println("Executing thread pool async task in thread: " + Thread.currentThread().getName());
}
}
(3) 비동기 처리 반환
이렇게 비동기 작업이 끝나면, 그 결과를 처리하기 위해 Future, ListenableFuture, CompletableFuture 같은 반환 타입을 사용할 수 있습니다.
Future은 비동기 작업의 결과를 나중에 받아오기 위해 사용되지만, get() 호출 시 블로킹이 발생할 수도 있습니다.
ListenableFuture은 Spring 4.0 버전부터 지원되는 인터페이스입니다. 이 인터페이스는 콜백을 통해, 비동기 작업이 완료된 후 처리 작업을 지정할 수 있습니다.
CompletableFuture은 더 복잡한 비동기 흐름을 처리할 수 있습니다. 체이닝과 예외 처리 등이 가능해, 더욱 유연한 비동기 처리가 가능해집니다.
4. 서블릿 기반 비동기 처리
Servlet 3.0에서부터, 서블릿 스레드를 비동기적으로 처리할 수 있도록 지원합니다. 비동기 작업이 시작되면 즉시 서블릿 스레드를 반납하고, 완료되면 다시 스레드가 할당됩니다. HTTP Connection은 Non-Blocking이지만 요청을 읽고 응답을 쓰는 과정에서의 I/O 작업은 여전히 Blocking 방식으로 동작할 수 있단 단점이 존재했습니다. 그렇지만 Servlet 3.1에서 서블릿 요청, 응답에도 Non-blocking 처리가 이뤄졌습니다. Servlet 4.0에서는 HTTP/2가 지원되기 시작했고, Servlet 6.0에서는 API 종속성이 없는 전체 쿠키, URL 보안 보호 등의 기능이 추가되었습니다.
Spring에서는 Callable, DeferredResult, ResponseBodyEmitter 등을 통해 비동기 서블릿을 사용할 수 있습니다.
DeferredResult는 Spring 3.2 버전 이후로 지원되는 비동기 처리 기술로, '지연된 결과'를 의미합니다. 즉, 응답을 나중에 반환할 수 있습니다. 소켓 통신 없이도 간단한 채팅방이나, 대기 상태로 있는 여러 대상들에게 결과를 쏴주어야 할 때 유용합니다. 가장 중요한 점은, 워커 스레드가 별도로 만들어지지 않는다는 점입니다.
ResponseBodyEmitter는 Spring 4.2부터 지원되는 기술로, 비동기 요청의 결과를 여러번 나누어 전달할 때 사용되는 기술입니다. StreamingResponseBody의 경우 파일 Byte 데이터를 Stream으로 나누어 전달도 가능하고, SseEmitter를 사용하면 Server-Sent-Events(SSE) 기술 적용도 가능해집니다. (SSE: 클라이언트 측에서 폴링을 따로 사용하지 않고, HTTP 커넥션을 통해 서버에서 이벤트 발생 시 클라이언트 측으로 데이터를 푸시하는 기술)
이렇게 비동기 처리를 적재적소에 사용하면 더욱 성능을 향상시킬 수 있을 것 같습니다😍 아직 도입해보지 않으셨다면, 이번 기회에 도입하는 게 어떨까요? 읽어주셔서 감사합니다!
📌 SSAFYcial 인스타그램 바로가기 @hellossafycial
'TRACE > SSAFYCIAL' 카테고리의 다른 글
[SSA이다 #12] 요구사항 정의서 VS 기능 명세서 (0) | 2024.12.02 |
---|---|
[SSA이다 #10] Java 버전 별 차이 알아보기 (0) | 2024.09.25 |
[SSA이다 #9] IntelliJ 단축키 완벽 정복 (7) | 2024.08.28 |
[SSA이다 #8] Figma에 대해 알아보자! (0) | 2024.07.28 |
[SSA이다 #7] SSAFY 금융권 간담회에 참여하다! (0) | 2024.06.23 |
댓글