[SpringBoot] @Async로 비동기 작업 처리하기
2년전 면접에서 @Async
어노테이션을 사용하면 스레드가 새로 생성되는지, 아니면 사용하고 있던 스레드를 계속 사용하는건지 질문을 받았던 적이 있다. 지금은 당연히 기본으로는 새로운 스레드를 생성해서 사용한다고 답변할 수 있지만, 그 당시에는 멘탈이 나가서 잘 모르겠다고 답변했다. 비동기적으로 실행하는건 아는데, 스레드에 대한 개념이 부족하고 그런것도 신경쓰지 않은채 사용해왔던 것이다. 결과는 당연히 좋지 않았다.
오랜만에 스프링을 다시 하는김에, @Async
를 어떻게 사용하는지, 어느 사례에서 활용하면 좋을지 정리해보았다.
@Async
@Async
어노테이션은 스프링에서 비동기작업을 처리하기 위해 제공해주는 메소드로, 순수 자바 코드나 다른 프레임 워크에서 사용할 수 있는 어노테이션은 아니다.
@Async
어노테이션을 사용하기 위해서는 애플리케이션을 실행해주는 main 메소드나, Configuration에서 @EnableAsync
어노테이션을 붙여줘야한다.
@SpringBootApplication
@EnableAsync
public class RankingApiApplication {
public static void main(String[] args) {
SpringApplication.run(RankingApiApplication.class, args);
}
}
@Async
어노테이션을 메소드에 선언해줄 수 있다. 단, 해당 메소드는 private이 될 수 없고, void나 CompletableFuture를 반환해야만 한다.
예제 코드
@Component
public class TestAsyncHandler {
@Async
public void test() {
try {
System.out.println("task 시작");
System.out.println("threadName = " + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("task 종료");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@RestController
@RequestMapping("/rankings")
@RequiredArgsConstructor
public class RankingController {
private final TestAsyncHandler testHandler;
@GetMapping("/test")
public String test() {
System.out.println(Thread.currentThread().getName());
testHandler.test();
System.out.println("response");
return "test";
}
}
위의 예제 코드로 애플리케이션을 실행한 뒤, /rakings/test 로 GET 요청을 하면 아래의 순서로 print된다.
http-nio-8080-exec-1
response
task 시작
threadName = task-1
// 3초 대기
task 종료
TestAsyncController의 test()
메소드를 호출해도 응답은 3초 대기 후 리턴되는것이 아니라, @Async
어노테이션이 붙은 메소드의 로직은 별도의 스레드에서 수행되고 클라이언트는 비동기 작업과 상관없이 응답을 받을 수 있다.
또한, 기존 요청에서 사용하는 스레드가 아닌, 별도의 스레드를 사용하는 것을 확인할 수 있다.
한 번의 요청에서 여러번 비동기 로직을 호출하면 어떻게 될까?
기본적으로는 요청이 들어올때마다 스레드를 새로 만든다.(이러한 방식때문에 리소스에 문제가 생길 수 있는데, 최적화 방법은 다음 포스팅에 다뤄볼 것이다.)
-> 테스트해보니 스레드풀을 사용하고 있었다.
@GetMapping("/test")
public String test() {
System.out.println(Thread.currentThread().getName());
testHandler.test();
testHandler.test();
testHandler.test();
System.out.println("response");
return "test";
}
http-nio-8080-exec-1
task 시작
task 시작
response
threadName = task-3
task 시작
threadName = task-1
threadName = task-2
task 종료
task 종료
task 종료
@Async 를 사용하면 좋은 이유
@Async
어노테이션이 붙은 메소드는 기존 로직의 흐름과 상관없이 다른 스레드에서 독립적으로 수행된다는 것을 알았다. 독립적으로 수행되는것은 알겠는데 실무에서는 어떻게 활용하는것이고 비동기는 왜 사용하는 것일까?
성능 향상과 장애의 최소화
만약, 유저가 랭킹 api를 조회하는 경우 해당 페이지 조회수도 +1 하는 기능을 구현한다고 가정해보자.
중요한 기능은 유저가 랭킹을 조회하는 것이고, 페이지 조회수를 +1하는 기능은 꼭 실시간으로 반영할 필요도 없고 일정 숫자 이상이면 999+ 로 표현할 수도 있다.
또한, 랭킹을 조회하는 기능은 성공적으로 수행했지만, 페이지 조회수 +1 하는 기능에 문제가 생겨서 유저가 랭킹을 조회하지 못하는 경우는 있어서는 안될것이다.
위와 같이 메인 로직과는 따로 독립적으로 수행되어도 상관없는 상대적으로 덜 중요한 로직을 비동기 작업으로 분리하여 유저는 조금 더 빠르게 응답을 받을 수 있고, 비동기 작업에 장애가 발생해도 메인 로직에는 영향을 끼치지 않는다.
예시로 덜 중요한 로직을 비동기로 분리했다고 설명했지만 이외에도 네트워크 통신, 데이터베이스 쿼리, 파일 읽기/쓰기 등의 지연시간이 긴 작업들도 비동기 작업으로 분리하면 응답을 기다리는 동안 다른 작업을 수행할 수 있으므로 비동기 작업으로 처리하기 적절하다.
정리
@Async
는 스프링에서 비동기 작업을 처리하기 위해 제공한다.@Async
를 사용하기 위해서는 main 메소드에 @EnableAsync를 해줘야 한다.@Async
메소드는 private일 수 없으며 void나 CompletableFuture를 리턴해야한다.@Async
메소드는 별도의 스레드에서 수행되며, 기본적으로 매번 새로운 스레드를 만든다.- 비동기로 효율적인 작업 처리를 할 수 있다.