- 안녕하세요. 이번 게시글에서는 클라이언트가 요청을 보냈을 때 WAS에서 일어나는 작업에 대해 알아보도록 하겠습니다.
- 특히 디버거를 사용해서 직접 코드를 살펴보며 자세히 알아볼게요~!
- 먼저 클라이언트에 요청이 일어난 후 내부적으로 진행되는 순서를 아래와 같이 그려보았습니다..
- 저번 게시글에 그렸던 그림과 약간 차이가 있죠? 저번 게시글이 틀렸었습니다.. Connector와 ThreadPool을 별도의 모듈로 표현했었는데, Connector내부에 ThreadPool이 존재하는 것이었습니다.. (이전글 수정은 나중에...)
- 각 모듈의 사양을 구분하기 위해서 Connector(Coyote) 모듈은 노란색, ServletContainer(Catailna)는 파란색, Spring은 초록색으로 표현했습니다.
- 위 그림의 화살표를 순서대로 보시면
- 엔드포인트 - Acceptor → Poller → ThreadPool
- 프로세서
- 코요테어댑터
- 필터
- 서블릿
클라이언트의 요청은 위와 같은 순서를 통하게 되는데요.
각각의 사항에 대해 알아보실까요...? (이번 게시글에 마무리 실패..)
EndPoint
- 엔드포인트란 보통 네트워크 서비스나 API에서 통신을 시작하거나 종료하는 지점을 가리키는 용어입니다.
- 따라서 요청이 발생하면 이 엔드포인트에서 가장 먼저 요청을 받아서 처리하는데요
- 현재 톰캣의 디폴트 엔드포인트는 NioEndPoint입니다.
NioEndPoint
- NioEndPoint는 내부적으로 크게 Acceptor, Poller, ThreadPoolExecuter로 이루어져 있습니다.
- 또한 요청을 전달하기 위한 객체인 PollerEvent와 Selector도 사용됩니다.
- NioEndPoint는 비동기적으로 요청을 처리하는데 특화된 모듈입니다.
- 좀 더 자세히 알아보기 위해 각 부분들을 소스코드를 참고해보겠습니다.
Acceptor
- 톰캣이 실행되면 Acceptor는 별도의 쓰레드로 생성되어 루프를 돌며 소켓 정보를 기다립니다.
- 여기서부터 소스코드단을 참고하며 살펴볼까요?
- EndPoint는 최초로 요청을 받은 후 Acceptor 쓰레드에 요청 정보를 할당시킵니다.
- 아래 디버거를 보면 쓰레드명이 http-nio-8080-Acceptor 임을 확인해볼 수 있습니다.
- setSocketOptions() 위 주석을 보면, 성공할 경우 소켓을 적당한 프로세서에 전달한다고 나와있네요.
- setSockerOptions() 내부는 아래와 같은데요, Accetor가 소켓 정보를 PollerEvent에 담아 Poller 전달하고 있는 것을 확인해볼 수 있습니다.
- socketWrapper에 SelectionKey를 등록시켜주고 있는데, 이렇게 등록된 SelectionKey를 가지고 Selector가 이벤트를 감지하면서 어떤 유형의 이벤트인지 판단한다고 합니다.
- addEvent()는 PollerEvent를 Queue에 담습니다.
Poller
- 자, Acceptor쓰레드에서 addEvent() 메소드 호출까지 이루어지면 Acceptor 쓰레드에서 Poller 쓰레드로 전환이 되게 되는데요!
- Poller 쓰레드 또한 루프를 돌면서 이벤트 발생 여부를 계속 감지하고 있었습니다.
- 폴러에 소켓을 추가하는 백그라운드 스레드는 폴러에서 트리거된 이벤트를 확인하고 이벤트가 발생하면 관련 소켓을 적절한 프로세서에 전달한다고 주석에 나와있는 것을 볼 수 있습니다.
- 폴러는 events()를 반복적으로 호출하면서 이벤트 존재 여부 확인합니다.
- PollerEvent를 담고 있는 Queue의 사이즈를 판단하여 Event 유무를 확인하고 있네요.
- 해당 메소드 내부에서 셀렉터를 사용하며 SocketChannel 객체에 소켓 정보를 담고 있습니다.
- Poller는 작업을 계속 진행하다가 processSocket()이라는 메소드 내부에서 ThreadPoolExecutor를 호출합니다
- ThreadPoolExecutor.execute()에 소켓정보를 담아 호출하면 내부적으로 TaskQueue에 소켓정보를 offer()하게 됩니다.
- 디버거를 확인해보면 현재 큐 인스턴스의 해시코드는 5928임을 확인해볼 수 있습니다.
- 부모 클래스는 LinkedBlockingQueue입니다.
ThreadPool
- 짜잔 ! TaskQueue에 소켓 정보를 전달한 순간 Poller 쓰레드에서 Worker 쓰레드로 바뀌게 되었습니다! 아래 디버거를 참고해주세요
- Worker 쓰레드도 마찬가지로 run()을 내부적으로 진행하며 계속해서 루프를 돌고 있었음을 아래의 코드에서 확인해볼 수 있습니다.
- getTask()에서 뭔가 가져오는 것 같은데 확인해볼까요?
- workQueue에서 poll()을 하고 take()를 하고 있습니다. 이 workQueue는 혹시 위에서 Poller쓰레드가 소켓정보를 담아둔 그 Queue가 아닐까요?
- 확인해보니 workQueue또한 TaskQueue 였습니다. 해시코드도 확인해볼까요?
- 해시코드는 "5928"로 같은 큐 인스턴스임을 증명할 수 있었습니다!
- take() 메소드에서 소켓정보를 갖고 오는 것도 확인할 수 있습니다. unlock()을 호출하고 있네요.
- 이어진 작업에선 가져온 task의 run()을 호출하여 작업을 이어나가는 것을 볼 수 있습니다 !
- 현재 task 는 SocketProcessorBase 라는 추상클래스인데 여기서부턴 다음 게시물에서 알아보겠습니다 ㅎㅎ!!
- 이번 게시물에선 여기까지.
- 요청이 Acceptor 쓰레드, Poller쓰레드를 거처 ThreadPoolExecutor에 TaskQueue에 요청정보를 담고
- 워커 쓰레드가 ThreadPoolExecutor에서 Queue에 담긴 Task를 가져오는 것까지 확인해보겠습니다.
- 다음 게시물엔 ThreadPoolExecutor에서 Processor로 요청을 전달한 이후의 작업을 정리해보겠습니다!!
'백엔드 > Spring' 카테고리의 다른 글
XSS 방지 삽질기 - 1. HTTP Method & Content-Type (0) | 2024.08.29 |
---|---|
@ResponseBody VS ResponseEntity 클래스 VS 커스텀 응답 클래스 (2) | 2024.06.25 |
AOP 적용 방법 5가지 정리 (프록시를 이용한 트랜잭션 AOP) (0) | 2023.12.13 |
WAS(톰캣)의 내부 구성 및 작동 과정 - 1. 초기화 단계 (0) | 2023.12.10 |