기본 설정을 신뢰하지 말고, 명시적으로 설정하자 (스레드 풀, 타임아웃)

jione ·2025년 7월 17일
1

최근 비동기(@Async) 기능의 요청이 처리되지 않는 문제가 있었다. 두 가지의 주요 설정 누락이 문제의 원인이었고 이 글에서는 기본 설정에 의존했을 때 발생하는 문제점과 명시적 설정의 중요성에 대해 다루고자 한다.

원인 1. Spring Boot의 기본 ThreadPoolTaskExecutor 사용

문제가 되는 기능은 @Async을 통해 비동기로 처리되고 있다. 우리 서버에는 별도의 Executor를 지정해주지 않았고, 이 경우 Spring Boot는 기본 스레드 풀인 TaskPoolTaskExecutor를 사용한다.


기본 ThreadPoolTaskExecutor의 설정은 다음과 같다.

  • corePoolSize: 8
  • maxPoolSize: Integer.MAX_VALUE
  • queueCapacity: Integer.MAX_VALUE

문제는 기본 설정의 queueCapacity가 사실상 무제한이라는 점이다. 이로 인해 큐가 가득 차는 상황이 발생하지 않아 스레드 풀은 절대 corePoolSize인 8개를 초과하여 확장되지 않는다. 만약 8개의 코어 스레드가 모두 블로킹 작업으로 점유되면, 이후의 모든 비동기 작업은 스레드를 할당받지 못하고 무한정 큐에 쌓이게 된다. 이렇게 스레드 풀이 고갈된 것이 문제의 원인 중 하나이다.

따라서 반드시 TaskExecutor Bean을 직접 등록하여 상황에 맞게 스레드 풀 설정을 명시적으로 관리해야 한다. 위와 같이 queueCapacity를 유한한 값으로 설정하고, corePoolSize와 maxPoolSize를 적절히 조절하여 스레드 풀이 유연하게 확장되도록 구성해야 한다.


원인 2. 타임아웃 부재

두 번째 원인은 외부 서비스와 통신하는 gRPC 클라이언트 호출에 타임아웃이 설정되어 있지 않았다.

공식 문서의 내용처럼 gRPC 클라이언트는 디폴트로 Deadline이 설정되어있지 않다. 이는 클라이언트가 서버의 응답을 무한정 기다리는 상태에 빠질 수 있음을 의미한다.

문제 발생 시점 외부 gRPC 서버의 응답 시간이 급격히 지연되고 있었다. 결론적으로 타임아웃이 없는 gRPC 호출로 인해 corePoolSize인 8개의 스레드를 모두 블로킹 상태로 만들었고 새로운 비동기 작업을 처리할 수 없는 상태로 빠진 것이 이번 문제의 핵심 원인이다.

gRPC뿐만 아니라, Netty 기반으로 동작하는 WebClient 역시 기본적으로 응답 타임아웃이 설정되어 있지 않으므로 반드시 명시적으로 지정해야 한다.

타임아웃이 없으면 연동 서비스의 응답이 느릴 때 처리량이 급격히 떨어진다. 더 나아가, 응답을 대기하면서 시스템 자원을 고갈 시켜 전체 기능의 장애로 전파될 수 있다.

따라서 다음과 같은 효과를 위해 타임아웃 설정은 필수적이다.

  • 장애 격리: 외부 서비스의 문제가 우리 시스템으로 전파되는 것을 차단

  • Fail-Fast: 사용자가 무한정 대기하거나 반복적인 새로고침으로 서버 부하를 가중시키는 것보다, 빠르게 실패를 인지하고 에러 화면을 보여주는 것이 훨씬 낫다.


결론

이처럼 gRPC 클라이언트의 타임아웃, @Async 스레드 풀의 동작 방식처럼, "설정하지 않음"은 종종 "무한대기" 또는 "무제한"을 의미할 수 있다.

이번 이슈는 외부 서비스 호출과 내부 비동기 처리에서 기본 설정을 그대로 사용한 것이 원인이었기에
안정적인 서비스를 구축하기 위해 주요 설정 값들을 기본값에 의존하지 말고, 반드시 운영 환경의 특성을 고려하여 명시적으로 설정해야 한다는 것을 배웠다.

0개의 댓글