[백엔드] @Async 활용 법, 개념 정리

유승선 ·2024년 1월 14일
0

Spring 강의

목록 보기
12/12

오늘은 실무에서 겪었던 내용을 블로그에 정리하고 기술적으로 어떤 변화가 있었는지 작성할 예정이다.


장애 상황

실무에서 겪은 식은 땀 났던 경험

때는 크리스마스 이브... 영화관이 가장 바쁜 성수기 시즌이고 평소 트래픽에 2배, 3배가 높아지는 시기다. 이 성수기를 대비해서 높은 트랙픽에서 기능적으로 정상 작동 되는지 많은 테스트를 하고 당직, 비상당직 근무 조를 나눠 혹여나 있을 모든 장애 상황에 대비해 모두가 긴장하는 그런 날이다.

영화관 매출에 가장 중요한 시기 만큼, 많은 준비를 했었기 때문에 아무 문제 없이 오전에는 정상적으로 영업이 되었다. 하지만, 문제는 오후 4시, 5시쯤 사람들이 몰려오면서 발생했다. 영화관에 입장하는 손님들이 매점 주문을 넣으면 주문 인입 알림톡을 받고, 매점 주문이 끝나면은 제조 완료 알림톡을 받는데.. 어느 순간부터 이 순서가 역전이 되어서 제조 완료 알림톡을 받고... 한참 뒤에 주문 인입 알림톡을 받는것이다.

그냥 알림톡 순서가 바뀐거 아니야? 뭐가 문제야?

라고 질문 할 수 있지만.. 영화관 현장에서 실제로 주문을 계속 기다리는 사람은 많이 없다. 보통 쇼핑을 하거나 구경을 하면서 앱이나 웹으로 매점 주문을 넣고 기다리는데 주문 인입 알림톡이 안오니깐 문의가 폭팔 했던 것이다. 분명 결제는 됐는데.. 주문이 인입이 되었는지 모르는 상황이어서 문의만 무려 1만 8천건이 왔었다.

대체 주문 알림톡이 왜 늦어진거야??


@Async

소스 코드 분석

급한 마음에 소스 코드를 분석하면서 알림톡을 보내는 메소드를 집중해서 분석하기 시작했다. 알림톡은 다른 채널에서 주문이 들어오면 빠르게 처리해야하기 때문에 비동기 방식으로 @Async 어노테이션이 붙어 있었는데, Async 어노테이션에서 사용되는 비동기 코드로 들어가보니.. 엥?

원인

스프링 MVC에서 병렬 프로그래밍을 구현할 때 @Async 어노테이션을 많이 사용한다. 이 어노테이션을 메소드에 붙혀주기만 하면 간단하게 사용할 수 있기 때문이다. 하지만, @Async 를 사용할때는 Spring 에서 제공하는 thread pool 설정 bean인 ThreadPoolTaskExecutor bean을 사용하는데... 이 설정이 기본으로 되어 있었고 아무런 커스텀이 안되어 있었다. 이게 왜 원인이 된걸까??


ThreadPoolTaskExecutor

Thread Pool의 개념은 나중에 더 언급할 생각이지만, 기본적으로 개별로 Thread를 계속 생성하고 죽이고 하는것이 아니라.. 이미 만들어진 Thread 를 사용하면서 자원을 아낄 수 있게 도와주는 개념이다. @Async 어노테이션에서 사용되는 ThreadPool은 아무런 커스텀을 안해줬을 경우, 기본 값으로 아래와 같은 설정을 가지고 있다.

조금 더 설명을 붙히자면, corePoolSize는 thread의 최소 값이다. 그리고 기본으로 설정한 경우 ThreadPool은 한개의 Thread 만을 사용해서 요청을 처리하는데 이것이 원인 이었다. 알림톡을 보내는 비동기 로직이 Thread 1개로 처리되고 있었던 것이다! 성수기에 초당 요청은 8000건이 넘었다. 즉, 8000건의 요청을 Thread 1개가 처리하고 있었기 때문에 처음 주문 인입 후 보내는 알림톡이 요청이 계속 쌓이면서 지연이 되고 있었던 것이다.


ThreadPoolTaskExecutor 설정 튜닝

급하게 튜닝을 해야 하는 상황에서.. 스프링에서 사용되는 ThreadPoolTaskExecutor을 좀 더 깊이 있게 파해쳤다.
corePoolsize 는 Thread의 최소 값이며.. maxPoolSize는 사용할 수 있는 Thread의 최대값, 그리고 QueueCapacity의 경우 요청이 대기할 수 있는 Queue의 크기다.

튜닝..그렇다면 그냥 corePoolSize 를 엄청나게 늘려주고 maxPoolSize도 늘려주면 되는거 아니야?

라고 생각하고 접근 했다가는 정말 큰 일이 날 수 있었다. Thread의 적절한 숫자는 서버의 CPU도 고려해야 하고 Queue에서 대기할 수 있는 크기, maxPoolSize의 숫자 모두 고려해야 했다.

maxPoolSize까지는 어떻게 늘어나는 걸까?


corePoolSize와 maxPoolSize, 그리고 queueCapacity의 관계

난 단순하게 corePoolSize 의 Thread 양보다 더 높은양의 요청이 들어오면 우선적으로 Thread가 늘어난 다음에 나머지 요청은 queueCapcity에 들어가서 대기하는 것이라 생각했다. 그런데 순서가 바뀌었다.

core 에서 max로의 thread 수의 증가는 queue 의 사이즈가 가득차게 되면 추가로 thread를 생성해서 사용한다

즉, 순서는 corePoolSize -> queue 대기 -> maxPoolSize 순서로 간다.

추가적으로, maxPoolSize 까지 도달 했을때 queue 가 또 한번 꽉 차게 된다면 어떻게 될까? 그때는 RejectedExecutionHandler 오류가 발생한다.

이떄, 오류를 handle할 수 있는 여러가지 방법이 있는데 영화관의 특성상 어떤 고객의 주문도 무시되거나 버려지면 안되고, exception이 발생해도 안되기 때문에.. 익셉션을 처리할 수 있게 CallerRunsPolicy를 사용했다.

이 순서를 생각하면서..queue에서 대기하게될 요청 숫자, 그리고 픽업 서버의 CPU 사양 등을 고려해서 튜닝을 했다.


결과

결과적으로 이 판단을 옳았고, 더 트래픽이 많아진 크리스마스 당일에 안전하게 장애를 모면할 수 있었다. 그런데 Async 어노테이션의 경우 좀 더 깊게 알아볼 필요가 있다고 느꼈다. 스프링에서의 비동기 프로그램을 이해하려면 AOP의 개념또한 알아야 하고 @Async 방법으로 반환되는 메서드 타입의 종류도 알아야지 다음 장애 상황에도 (없으면 좋겠지만) 더 유연하게 대처가 가능할거 같다.

profile
성장하는 사람

0개의 댓글