들어가며

  • 이전 포스트에서 WAS와 서블릿 그리고 서블릿 컨테이너에 대해 알아보았다.
  • 이번 포스트에서는 서블릿과 멀티 쓰레드에 대해 알아본다.

 

 

 

서블릿 호출과 스레드

  • 이전 포스트의 그림을 다시 가져왔다. 클라이언트가 요청을 보내면 그 요청을 통해 Request 객체가 생성되고, url 패턴과 동일한 서블릿이 존재하는지 서블릿 컨테이너에 확인한 뒤 해당 서블릿을 호출한다.
  • 그런데 이 서블릿을 누가 호출하는 것인가에 대해서 한번 생각할 필요가 있다. 누가 이 서블릿을 호출하고 내부 로직을 실행시키는 것일까
  • 그 답은 쓰레드이다. 웹 애플리케이션이 실행되면 웹 애플리케이션은 고유의 프로세스(Process)를 가지게 되고 그 내부에는 기본적으로 하나의 쓰레드를 가지게 된다.
  • 바로 이 쓰레드가 서블릿 컨테이너에서 찾은 서블릿을 호출한다.
    • 자바 애플리케이션은 실행될 시 기본적으로 main이라는 이름의 쓰레드를 생성하여 이 쓰레드를 통해 코드를 읽고 실행한다.

  • WAS는 클라이언트와 연결이 수립되면 해당 서블릿을 호출할 하나의 쓰레드를 할당한다. 이 쓰레드가 서블릿 컨테이너에 존재하는 서블릿을 호출하고, 내부의 애플리케이션 로직을 수행하도록 한다.
  • 이후 서블릿은 애플리케이션 로직을 수행한 뒤에 응답을 도출한다(response 객체에 세팅함으로서) 이후 서블릿이 해당 Response 객체를 기반으로 응답 메시지를 만들어 클라이언트에게 반환한다.
  • 일련의 작업이 끝나면 쓰레드는 다시 휴식 상태로 되돌아간다.

 

 

 

싱글 쓰레드의 한계와 해결

  • 위의 그림처럼 하나의 쓰레드로 요청을 처리하는 것을 싱글 쓰레드를 사용하여 요청을 처리한다고 한다. 하지만 이 방식은 여러 요청이 들어올 시 문제가 발생할 수 있다.

  • 위의 그림은 클라이언트 1의 요청이 어떠한 사유로 인해 처리가 지연되고 있는 모습이다.
  • 문제는 클라이언트 2도 지금 요청을 WAS에 보내어 처리를 하려고 하는데 쓰레드가 하나라 이미 다른 요청을 처리하고 있기 때문에 해당 요청이 처리될 때 까지 강제로 기다리게 된다.
  • 이는 다음과 같은 문제를 발생시킨다.
    • 클라이언트 2의 요청이 빠르게 끝날 수 있음에도 불구하고 클라이언트 1의 요청 처리가 지연되고 있기 때문에 불필요한 대기를 수행해야 한다.
    • 또한 클라이언트 1의 요청 처리 중 문제가 발생할 경우 클라이언트 1, 2 모두 원하는 응답을 받을 수 없게 된다는 문제도 발생한다.
  • 이를 해결하기 위해서는 간단하게 새로운 쓰레드를 생성하여 클라이언트 2의 요청을 처리하도록 하면 된다.

  • 쓰레드 2는 현재 처리 지연이 되고 있는 쓰레드 1과는 별개로 동작하기 때문에 호출과 응답을 수행할 수 있다. 만약 클라이언트 2의 요청에 문제나 지연이 없다면 정상적인 응답이 돌아올 것이다.

 

 

 

멀티쓰레드와 쓰레드 풀(Thread Pool)

  • 이렇게 요청이 하나 들어올 떄 마다 새로운 쓰레드를 생성하여 요청을 처리하도록 하는 것은 하나의 쓰레드에서 문제가 발생하더라도 전체가 마비되는 것을 방지하는 장점이 있으나 여러 단점 역시 가지고 있다.
  • 먼저 쓰레드의 생성 비용이 매우 비싸다는 것이다.
    • 쓰레드를 만들기 위해서는 프로세스 내부에서 또 자원을 해당 쓰레드에 할당을 해주어야 한다. 또한 이렇게 쓰레드를 생성하는데 드는 시간 역시 무시할 수 없다.
    • 이러한 동작을 매 요청마다 수행해야 한다면 결국 클라이언트에 대한 응답이 늦어질 것이다.
  • 또한 쓰레드는 컨텍스트 스위칭 비용이 발생한다.
    • 컨텍스트 스위칭(Context Switching)은 한 프로세스에서 다른 프로세스로 실행 대상이 변경될 때 이전 프로세스의 데이터들을 별도의 공간에 저장하는 과정을 통틀어 말한다.
    • 이 컨텍스트 스위칭 비용은 프로세스가 가지고 있는 상태(데이터)가 많을수록 증가한다. 만약 많은 쓰레드가 존재한다면 이 각각의 쓰레드에 대한 상태를 저장해야 하기 때문에 컨텍스트 스위칭 비용이 커질 것이다.
  • 쓰레드 생성에 대한 제한이 걸려있지 않다.
    • 이것은 장점이자 동시에 단점이 될 수 있다. 제한이 없기 때문에 필요하다면 많은 수의 쓰레드를 생성하여 요청을 처리하게 할 수 있다.
    • 하지만 쓰레드 역시 자원을 먹기 때문에 무분별하게 생성하다가는 CPU나 메모리 임계점을 넘어 서버가 모두 셧다운될 수 있다.
  • 이러한 단점들을 어느정도 극복하기 위해서 쓰레드 풀(Thread Pool)이라는 개념이 생겨났다.

  • 쓰레드 풀은 어느 정도의 쓰레드들을 미리 생성하여 보유하고 있는 쓰레드 집합체이다. 보통 생성 가능한 쓰레드의 최대치를 관리하며 톰캣 WAS의 경우 200개가 디폴트로 되어 있다.
  • 이후 클라이언트 1의 요청이 들어오면 해당 요청을 처리할 쓰레드를 이 쓰레드 풀에 요청을 하여 쓰레드를 가져옴으로서 할당받는다.
  • 클라이언트 2의 요청이 들어올때도 동일하다. 쓰레드 풀에 쓰레드를 요청하고, 쓰레드 풀에 여유가 있다면 쓰레드를 반환한다.
  • 만약 쓰레드가 모두 사용되어서 여유가 없는 경우 쓰레드 요청을 거절하거나 쓰레드가 반환되어 올 때 까지 대기하도록 설정이 가능하다.
  • 이 쓰레드 풀은 미리 쓰레드들을 생성하였기 때문에 쓰레드 요청이 올 시에 쓰레드를 만들어서 제공하는 부분이 생략된다. 따라서 쓰레드 생성만큼의 시간이 절약된다.
  • 생성 가능한 쓰레드의 최대치가 설정되어 있다. 이는 위에서 보았던 쓰레드 생성에 대한 제약이 없음으로 인해 발생하는 단점들을 해결할 수 있다.

 

 

 

WAS와 쓰레드 풀

  • 쓰레드 풀에 대해 다시 생각해보자. 일정 수의 쓰레드를 "미리" 만들어 놓는다.
  • 이는 즉 WAS가 기동하면 WAS의 자원을 사용하여 쓰레드를 미리 만들고 대기하고 있다는 것이다. 따라서 이 쓰레드 풀에 할당할 자원을 어느 정도로 잡을 것이냐가 WAS의 주요 성능 튜닝 포인트가 된다.
  • 만약 쓰레드 풀에 할당할 자원을 너무 낮게 잡는다고 생각해보자.
    • 자원을 적게 할당할수록 쓰레드 풀에 저장된 쓰레드 수는 적어진다.
    • 쓰레드 수가 적어지면 한번에 처리할 수 있는 요청 수가 적어질 것이다. 하지만 WAS의 메모리 등의 자원은 여유로워진다.
    • 한번에 처리할 수 있는 요청 수가 적어짐으로 인해 클라이언트는 많은 요청이 들어오는 상황에서는 응답을 받기까지의 시간이 지연될 것이다.
  • 만약 쓰레드 풀에 할당할 자원을 너무 많이 잡는다고 생각해보자.
    • 자원을 많이 할당할수록 쓰레드 풀에 저장된 쓰레드 수는 많아진다.
    • 이는 WAS의 자원이 적어지는 대신에 한번에 처리할 수 있는 요청의 수가 많아진다.
    • WAS의 가용 자원이 매우 적어지기 때문에 안정성에 문제가 생길 수 있다. 만약 쓰레드 풀에 할당한 자원 이외에 다른 곳에 자원이 많이 투입되게 된다면 임계치를 넘어 서버가 내려갈 수 있다.
  • 이는 쓰레드 풀에 WAS의 자원을 어느 정도 할당할 것인지에 대해 생각하게 한다.
  • 정답은 없다. 왜냐하면 WAS 내부에 있는 로직의 복잡도, CPU와 메모리의 상태, IO 리소스 상황마다 달라지고, 이 조건들이 모두 동일한 서버들은 많이 없기 때문이다.
  • 이를 알아보기 위해서는 성능 테스트가 수반되어야 한다.
    • 실제 서비스하는 상황과 최대한 유사한 테스트를 시도해야 한다. 이는 실제 서비스와 거의 비슷한 수준으로 요청이나 IO를 줌으로서 어느 정도의 쓰레드 풀을 만들어야 하는지를 확인할 수 있다.
    • 이러한 테스트에 사용되는 툴들은 아파치 ab, Jmeter, nGrinder가 있다.

 

 

 

WAS와 멀티 쓰레드

  • 여기까지 보면 개발자가 쓰레드라는 요소에 대해 많은 고민을 해야 하는 것 처럼 보인다.
  • 다행스럽게도 WAS는 이러한 멀티 쓰레드 부분을 담당하여 처리한다. 따라서 개발자는 멀티 쓰레드 프로그래밍에 대한 생각을 하지 않아도 되며 싱글 스레드 프로그래밍을 하는 것 처럼 개발하여도 상관없다.
  • 단 실행되는 환경은 멀티 쓰레드 환경에서 수행되기 때문에 싱글톤 객체에 대해서는 주의해서 사용해야할 것이다.(동시성 문제 등)
복사했습니다!