땃쥐네

[Spring MVC] 서블릿 컨테이너와 멀티 스레드 본문

Spring/Spring MVC

[Spring MVC] 서블릿 컨테이너와 멀티 스레드

ttasjwi 2022. 7. 25. 01:13

 

결국 스레드마다 자바코드가 실행된다. 요청당 스레드!

애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 스레드입니다. 실제로 JVM(Java Virtual Machine)의 Runtime Data Area를 확인해보면 각 스레드마다 stack, pc register, native method stack 을 할당받습니다.(JVM 구조 및 각 요소들의 역할에 대해서는 이 포스팅의 영역을 벗어나므로 다루지 않겠습니다.)

 

어떤 자바 코드든 결국 스레드 위에서 동작합니다. java 애플리케이션을 실행하면 main이라는 이름을 가진 스레드가 생성되고 main 메서드는 main 스레드의 스택 프레임에 올려져 여기서 자바 코드가 한 줄 한 줄 실행됩니다.(싱글 스레드 프로그래밍)

 

이후 java 코드 상에서 Thread 클래스의 인스턴스(혹은 기본 Thread를 비롯한 여러 가지를 추상화 한 다른 인스턴스)를 명시적으로 생성하여 start 메서드를 호출하면 새로운 스레드를 생성하여 여러 스레드가 OS로부터 번갈아가면서 자원을 할당받아 작업을 처리할 수 있습니다.(멀티 스레드 프로그래밍)

 

여기서 포인트는, 한 스레드마다 자바코드를 하나씩 실행한다는 것이죠. 복수의 작업을 동시에 수행하려면(정확히는 동시에 진행되는 것처럼 느끼게하려면) 두개 이상의 스레드가 필요합니다.

 

 

실제로 Http 요청 하나마다 하나의 스레드가 할당되고, 각 스레드마다 서블릿을 호출하여 작업을 실행합니다. 요청이 오고 TCP/IP 커넥션을 맺은 뒤 스레드 하나가 할당되어 그 위에서 서블릿의 자바코드가 실행되는 것입니다. 


싱글 스레드의 한계 / 스레드가 부족할 경우

만약 서블릿 컨테이너 상에서 생성될 수 있는 스레드가 하나뿐일 때는 어떻게 될까요?

 

각 요청을 처리하기 위해서는 하나의 스레드를 할당받아야하기 때문에, 한 요청을 처리하는 동안 다른 요청은 스레드를 할당받지 못 하게 됩니다. 스레드를 할당받지 못 하는 클라이언트 측은 그 시간만큼 대기를 해야합니다. 병목이 발생하는거죠.

 

심지어 서비스 처리 성능이 최적화가 덜 되서(예 : DB 조회), 처리가 지연된다면 다음 사용자가 스레드를 할당 받을 때까지 걸리는 시간은 배로 늘어나겠죠. 응답성이 좋지 않다는 것은 고객 입장에선 안 좋은 경험(UX : User Experience) 이 될테고서비스의 매출에 매우 악영향을 끼치게 될겁니다. 이러면 안 돼요.


멀티 스레드 - 요청마다 스레드를 매번 생성/소멸 한다면?

결국 싱글 스레드로 모든 요청을 처리하는 것은 문제가 많아서, 여러 스레드를 생성해야합니다.

일단 매 요청이 들어올 때마다 스레드를 생성하고, 요청이 끝날 때마다 스레드를 제거하는 방식을 생각해볼 수 있습니다.

장점

이렇게 하면 들어오는 동시요청을 WAS의 리소스(CPU, 메모리, ...)가 허용할 때까지 처리할 수 있게됩니다. 하나의 스레드에서 어떤 사정으로 작업 처리가 지연되어도 나머지 스레드는 정상적으로 동작될테니 전체적으로 봤을 때 고객 입장에서는 응답성이 좀 더 좋은 서비스를 이용할 수 있게 됩니다.

단점

일단 요청이 올 때마다 스레드를 매번 생성하고, 매번 스레드를 제거하는 것은 생각보다 비용이 정말 비쌉니다. 각 처리마다 생성, 소멸시간이 생기면 그만큼 각 요청에 대한 응답 속도가 늦어지는 결과를 초래합니다.

 

또, 멀티스레드 환경은 동시에 작업이 수행되는 것처럼 느껴지는 것일 뿐 실제로는 매우 짧은 시간을 각각의 스레드가 OS로부터 번갈아가면서 할당받는 것이기 때문에 정말 많은 스레드가 생성되버리면 그만큼 컨텍스트 스위칭(Context Switching, 문맥 교환) 비용이 폭발적으로 증가합니다. 매번 OS로부터 스레드가 작업의 기회를 받고 다시 반납하는 과정을 거치는 만큼의 시간이 늘어나게 되니 각 사용자 입장에선 응답 처리 시간이 늘어날 수밖에 없습니다.

 

스레드 생성 갯수에 제한이 없는 것도 매우 위험합니다. WAS의 성능상 100-200명정도의 요청을 동시에 처리하는 것은 어떻게든 버틸 수 있겠지만 동시에 1만명 혹은 그 이상의 요청마다 스레드를 생성한다면(대학교 수강신청, 자격증 시험 접수, 선착순 할인 이벤트,...) 스레드가 1만개 이상 생성되 버리는 겁니다. 너무 많은 요청을 동시에 처리하려하면 되면 CPU가 지나치게 무리를 하게 되는 겁니다. 그리고 각 요청마다 요청을 처리하기 위해 메모리 공간을 사용하게 될텐데, 메모리 임계점을 넘게 되면 OutOfMemory 에러가 발생해서 서버가 죽습니다...! 장애 나는 겁니다.


스레드 풀 : 서블릿 컨테이너의 스레드 관리 방식

 

방식을 바꿔서, 애플리케이션 구동 시점에 스레드를 정해진 갯수만큼 만들어두는 방식을 도입합니다. 이게 서블릿 컨테이너의 멀티 스레드 관리 방식입니다. 스레드 풀(Thread Pool)에 필요한 갯수만큼 생성해 모아둔 뒤, 스레드가 필요하면 스레드 풀에 요청하여 이미 생성되어 있는 스레드를 꺼내고, 사용을 마치면 다시 스레드 풀에 사용한 스레드를 반납하는 방식입니다.

 

아까와 방식을 비교해보면 스레드를 매번 생성, 소멸시키는 방식이 아니라, 반납하여 자원을 재사용하는 방식이라는 점입니다. 

# application.yml
server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    accept-count: 100
  port: 8080

기본적으로 톰캣의 기본 최대 스레드 갯수는 250개로 설정되어 있고, 스프링 부트 기준 application.yml 파일의 설정값을 변경해서 WAS의 사양에 맞게 최대 생성 가능한 스레드의 갯수를 조절할 수 있습니다. (참고자료 : https://www.baeldung.com/java-web-thread-pool-config)

스레드 풀의 최대 스레드 갯수만큼 스레드가 이미 사용 중이면, 그 이후의 요청은 거절하거나 대기를 하게 할 수 있습니다. 물론 싱글 스레드 방식과 다르게 스레드를 할당받을 기회가 더 많으니 비교적 응답성은 더 나을 것입니다.

이렇게, 스레드 풀 방식을 사용하면 스레드가 미리 생성되어 있으니 재사용을 함으로서 스레드를 생성, 소멸시키는 비용이 절약되고 비교적 응답 시간도 빨라집니다.

 

또, 사용 가능한 스레드의 최대 갯수가 정해져 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있게 됩니다.


WAS의 주요 튜닝 포인트는 최대 스레드 수

백엔드 엔지니어 입장에서는 고객의 응답에 신속하고 안전하게, 경제적으로 대응할 수 있도록 웹 애플리케이션을 개발, 관리하여야하는데요. 요청-비즈니스 로직-응답의 일련의 과정에서 결국 저희가 손봐야 할 부분은 WAS 및 DB가 될 것입니다. 일단 WAS 자체의 성능을 놓고 보면 가장 중요한 성능 튜닝 포인트는 스레드 풀이 관리하는 최대 스레드의 갯수가 됩니다.

스레드 갯수가 너무 적으면

동시 요청이 너무 많더라도, 일하는 스레드는 적으니 서버 리소스는 여유롭겠지만 클라이언트의 응답성이 안 좋아집니다.

CPU가 어느 정도는 일해줘야하는데 쓸 수 있는 자원을 놀게 하는 상황이죠. 일할 수 있는 스레드를 더 늘려도 좋은 상황인데 여기서 엉뚱하게 서버 대수를 늘린다면 경제적으로 비효율적이겠죠?

 

스레드 갯수가 너무 많으면

매우 많은 스레드들이 컨텍스트 스위칭을 계속 반복하게 됩니다. 정말 CPU의 한계를 넘어 탈탈 갈아 넣고 메모리에 각 스레드별 저장 데이터가 쌓이는데 잘못되면 CPU/메모리 리소스의 임계점을 넘어서 서버가 죽어버리게 됩니다.

 

장애 발생 시

WAS가 죽었다는 것은 위에서 언급했듯, 현재 스레드 풀에서 관리하는 스레드의 갯수가 너무 많아 비즈니스 로직 각각에서 사용하는 CPU 자원, 메모리 리소스 자원의 총합이 임계점을 넘었을 가능성이 큽니다. 일단 유연하게 서버를 증설할 수 있는 클라우드 환경(AWS, ...)이라면 당장 급한 불을 끄기 위해 서버를 증설한 뒤 기존 WAS 환경의 튜닝(스레드 최대 갯수 조정, 비즈니스 로직 리팩토링, ...)을 진행하고, 클라우드 환경이 아닌 경우... 눈물을 머금고 성능 튜닝을 진행하셔야 할겁니다. 디테일한 부분은 부분은 저도 아직 취준생인지라... 실무에서 경험해봐야할 문제 같습니다.


적정 스레드 갯수 찾기

스레드를 몇 개정도 만드는게 적절할지는 딱 정해져 있지 않습니다.

 

결국 운영 서비스마다 다 다릅니다.

 

비즈니스 로직의 복잡도, CPU/메모리 리소스, IO 리소스 상황 등... 여러가지 상황마다 모두 최적의 스레드 갯수가 달라지기 마련입니다. 결국 개발 환경에서 성능 테스트를 지속적으로 해보고, 병목 포인트를 찾아내고 튜닝해야합니다. 결국 끊임없이 측정하고 튜닝을 계속 해야합니다.

 

최대한 실제 서비스와 유사한 환경에서 성능 테스트를 시도해보는게 중요합니다. 이를 지원하는 툴로는 아파치의 ab,  제이미터, nGrinder 등이 있다고 합니다.


서블릿 컨테이너의 멀티 스레드 관리 지원

앞에서 길게 스레드 풀에 관한 이야기를 했습니다.

 

이 부분을 매번 개발자가 직접 짜는 것은 복잡도도 높고, 오류 발생 여지가 너무 큰데 결국 멀티스레드 환경에 대한 구현은 서블릿 컨테이너(WAS, 톰캣)가 알아서 해주기 때문에 개발자 입장에서는 멀티 스레드 코드를 신경쓰지 않아도 됩니다.

 

그냥 서블릿을 등록해두고, 서블릿의 비즈니스 로직 구현에만 집중하면 됩니다.(물론 각각의 서블릿은 싱글톤 인스턴스이므로 공유변수 사용에 주의해야하구요.)


이어서 학습해야할 주요 키워드들

- HttpServletRequest, HttpServletResponse

- 서블릿 방식에서 요청, 응답을 어떻게 처리할 것인가

- 서블릿 방식에서 동적 HTML을 어떻게 만들 것인가

- 서블릿 방식의 한계, JSP는 무엇인가

- JSP와 서블릿을 함께 사용하기

- 프론트 컨트롤러 패턴을 적용하여 유사 Spring MVC 구현

- 스프링 MVC 구조

- 스프링 MVC 사용법


참고자료

- 인프런 : 김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

 

'Spring > Spring MVC' 카테고리의 다른 글

[Spring MVC] 서블릿(Servlet)이란?  (6) 2022.07.24
Comments