티스토리 뷰

Java 애플리케이션에서 멀티 스레딩은 성능과 직결되는 중요한 요소입니다. 동시에, 잘못 설계된 멀티 스레딩 구조는 성능 저하뿐 아니라 디버깅이 어려운 장애로 이어지기 쉽습니다. 특히 실무에서는 “어디서 스레드를 생성하고, 누가 관리할 것인가”라는 질문이 반복적으로 등장합니다. 이 글에서는 Java의 공식 동시성 API와 JVM 동작 원리를 기반으로, Executor를 어떻게 생성하고 관리하는 것이 바람직했는지를 정리해봅니다. 그 과정에서 Holder Idiom 싱글톤을 활용한 Thread 생명주기 관리 전략이 왜 의미 있었는지도 함께 살펴봅니다.

 

 

멀티 스레딩 문제는 구현보다 구조에서 시작됩니다

 

Java에서 멀티 스레딩을 처음 접할 때는 Thread를 직접 생성하거나, Executors 유틸리티 클래스를 사용하는 방식이 비교적 단순해 보입니다. 하지만 서비스 규모가 커지고 요청 트래픽이 증가할수록, 스레드 생성 방식 자체보다 더 중요한 문제가 드러납니다. 바로 스레드의 생명주기를 애플리케이션 차원에서 어떻게 통제할 것인가입니다.

 

실무에서는 다음과 같은 상황을 자주 마주하게 됩니다. 특정 서비스 로직 내부에서 ExecutorService를 생성하고, 로직이 끝날 때까지 명시적으로 종료하지 않은 채 방치되는 경우입니다. 코드 자체는 정상적으로 동작하지만, 애플리케이션이 장시간 실행되면 스레드 수가 점진적으로 증가하거나, 불필요한 컨텍스트 스위칭으로 CPU 사용률이 높아지는 문제가 발생합니다. 이런 문제는 단일 코드 리뷰로는 잘 드러나지 않고, 운영 환경에서 서서히 성능 저하로 나타나는 경우가 많습니다.

 

Java 공식 문서에서도 스레드는 비용이 큰 자원이며, 무분별한 생성은 성능과 안정성에 영향을 줄 수 있다고 설명합니다. 따라서 멀티 스레딩 구현에서 중요한 것은 “어떻게 병렬로 처리할 것인가”보다 “스레드를 언제 생성하고 언제 종료할 것인가”를 명확히 정의하는 구조라고 느끼게 됩니다.

 

 

ExecutorService를 어디서 생성할 것인가에 대한 고민

 

Java 5 이후 도입된 Executor 프레임워크는 스레드 관리의 복잡성을 줄이기 위한 목적을 가지고 있습니다. ExecutorService는 작업 제출과 실행 정책을 분리해주며, ThreadPoolExecutor를 통해 스레드 재사용이 가능하도록 설계되어 있습니다. 다만 ExecutorService를 사용한다고 해서 자동으로 올바른 스레드 관리가 보장되지는 않습니다.

 

실무 코드에서는 ExecutorService를 메서드 내부에서 생성하거나, 특정 컴포넌트마다 개별적으로 생성하는 패턴을 종종 볼 수 있습니다. 이런 구조는 단기적으로는 구현이 편리하지만, 애플리케이션 전반에서 몇 개의 스레드 풀을 사용하고 있는지 파악하기 어려워집니다. 또한 종료 시점에 모든 Executor를 정상적으로 shutdown하지 않으면, JVM 종료 지연이나 리소스 누수로 이어질 수 있습니다.

 

Java 공식 가이드에서는 ExecutorService의 생명주기를 애플리케이션 생명주기와 맞추어 관리하는 것이 바람직하다고 설명합니다. 즉, Executor는 단순한 유틸리티 객체가 아니라, 애플리케이션의 핵심 인프라 자원에 가깝다고 볼 수 있습니다.

 

 

싱글톤 Executor 설계가 필요했던 이유

 

이런 배경에서 자연스럽게 떠오르는 선택지는 Executor를 싱글톤으로 관리하는 방식입니다. 애플리케이션 전체에서 공통으로 사용하는 Thread Pool을 정의하고, 모든 비동기 작업은 이 풀을 통해 실행하도록 제한하는 구조입니다. 이렇게 하면 스레드 수를 예측 가능하게 관리할 수 있고, 모니터링과 튜닝 포인트도 명확해집니다.

 

다만 싱글톤을 구현하는 방식 역시 신중하게 선택할 필요가 있습니다. 멀티 스레딩 환경에서의 싱글톤은 초기화 시점과 메모리 가시성 문제가 함께 고려되어야 하기 때문입니다. 이 지점에서 Holder Idiom은 비교적 단순하면서도 JVM 동작 원리에 잘 부합하는 선택지로 보였습니다.

 

 

Holder Idiom 싱글톤의 동작 원리

 

Holder Idiom은 정적 내부 클래스를 이용해 싱글톤을 구현하는 패턴입니다. 이 방식의 핵심은 JVM의 클래스 로딩과 초기화 시점에 대한 보장에 있습니다. 외부 클래스가 로딩될 때 내부 클래스는 즉시 로딩되지 않으며, 내부 클래스를 실제로 참조하는 시점에 초기화가 이루어집니다.

 

Java Language Specification에 따르면 클래스 초기화는 한 번만 수행되며, 그 과정은 스레드 안전하게 보장됩니다. 이를 활용하면 명시적인 synchronized 블록이나 volatile 키워드 없이도 안전한 싱글톤을 구현할 수 있습니다. 이 특성은 Executor와 같이 애플리케이션 전역에서 단 하나만 존재해야 하는 자원에 적합하다고 판단되었습니다.

 

 

Holder Idiom을 활용한 Executor 생성 전략

 

Holder Idiom을 사용한 Executor 싱글톤 설계의 핵심은 “언제 Thread Pool을 생성할 것인가”를 명확히 제어하는 데 있습니다. 애플리케이션 시작 시점에 무조건 스레드를 생성하는 대신, 실제로 비동기 작업이 필요해지는 순간에 Thread Pool이 초기화됩니다. 이는 초기 부하를 줄이는 데에도 도움이 됩니다.

 

또한 Executor 인스턴스가 하나로 제한되기 때문에, ThreadPoolExecutor의 설정 값 역시 중앙에서 관리할 수 있습니다. corePoolSize, maximumPoolSize, queue 정책 등을 서비스 특성에 맞게 조정하고, 해당 설정이 전체 시스템에 일관되게 적용되도록 유지할 수 있습니다. 이는 운영 중 성능 튜닝을 진행할 때도 큰 장점으로 느껴졌습니다.

 

 

Thread 생명주기를 애플리케이션 관점에서 관리하기

 

Executor를 싱글톤으로 관리하면서 가장 크게 달라진 점은 Thread 생명주기를 코드 단위가 아닌 애플리케이션 단위에서 바라보게 되었다는 점입니다. 더 이상 “이 메서드에서 스레드를 몇 개 생성한다”가 아니라, “이 시스템은 동시에 몇 개의 비동기 작업을 허용할 것인가”라는 질문을 하게 됩니다.

 

또한 애플리케이션 종료 시점에 ExecutorService를 명시적으로 shutdown하는 구조를 갖추기 쉬워집니다. 이는 Spring과 같은 프레임워크 환경에서는 라이프사이클 훅과도 자연스럽게 연결할 수 있습니다. 공식 문서에서도 ExecutorService는 더 이상 사용하지 않을 경우 shutdown을 호출해야 하며, 그렇지 않으면 JVM이 종료되지 않을 수 있다고 명시하고 있습니다.

 

 

성능 최적화 관점에서 얻은 인사이트

 

멀티 스레딩 성능 최적화는 단순히 스레드 수를 늘리는 문제가 아니라는 점을 다시 한 번 체감하게 됩니다. 제한된 스레드 풀을 사용하면 오히려 시스템 전체의 응답성이 안정되는 경우도 많았습니다. 이는 CPU 코어 수, I/O 대기 시간, 컨텍스트 스위칭 비용 등 다양한 요소가 함께 작용하기 때문입니다.

 

Holder Idiom 싱글톤 Executor 구조는 이런 판단을 구조적으로 강제합니다. 스레드 생성이 쉬워지는 대신, 무분별한 확장을 막아주기 때문입니다. 물론 모든 시스템에 이 방식이 정답이라고 보기는 어렵습니다. 작업 특성에 따라 별도의 전용 Thread Pool이 필요한 경우도 충분히 존재합니다. 다만 적어도 “기본값”으로 삼기에는 충분히 검증된 구조라고 느꼈습니다.

 

 

마무리하며

 

Java 멀티 스레딩은 문법이나 API 자체보다, 설계 관점에서의 선택이 더 큰 영향을 미치는 영역이라고 생각합니다. Executor를 어디서, 어떤 방식으로 생성하고 관리할 것인지는 코드 몇 줄의 문제가 아니라 시스템 전체의 안정성과 직결됩니다.

 

Holder Idiom을 활용한 Executor 싱글톤 설계는 JVM의 공식적인 동작 보장을 기반으로 비교적 단순하게 구현할 수 있으면서도, 스레드 생명주기를 명확히 통제할 수 있는 구조를 제공합니다. 이 글에서 정리한 내용이 특정 패턴을 권장하기보다는, 멀티 스레딩 설계를 고민할 때 하나의 판단 기준으로 활용되기를 바랍니다. 결국 중요한 것은 패턴 자체가 아니라, 시스템의 특성과 운영 환경을 이해한 상태에서 내리는 선택이라는 점을 다시 한 번 정리하며 글을 마무리합니다.