Controller 1개가 어떻게 수 많은 Request를 처리하는가
배경 의식의 흐름
Controller는 한 개인가?
지금와서 생각하면 조금 바보같았지만, 저런 질문이 떠오른 이유는 아래와 같습니다.
스프링에서 웹 애플리케이션을 개발할 때, (대부분의 경우에) 스프링 빈(Bean)으로 객체를 생성합니다.
Bean의 스코프를 prototype으로 변경하면 HTTP Request 마다 해당 빈을 새롭게 생성해서 처리하도록 할 수 있지만, 그렇게 하는 경우를 거의 본 적이 없습니다.
그러면 내가 만든 Controller(Bean) 하나가 1만 개의 요청이든 10만 개의 요청이든 처리를 한다는 얘긴데, 이게 감당가능한가? 이런 생각이 갑자기 스쳐지나가면서 의문이 생겼습니다.
혹시 여러 컨트롤러가 만들어지는 것은 아닐까 의문을 품고 조금 찾아보니 우선은 Controller 객체(Bean)는 한 개가 생성되는 맞고 그 하나의 컨트롤러를 사용하는게 맞았습니다. (일단 OK)
tomcat worker(?) thread가 200개라던데?
그리고 WAS마다 다르겠지만 학습할 때 많이 쓰이는 톰캣(tomcat)... 이게 옛 기억을 더듬었을 때 default로 HTTP Request를 받아주는 스레드(Thread)가 200개라 들었던게 생각났습니다.
찾아보니까 쓰레드 풀에 생성될 수 있는 쓰레드 의 수가 MAX가 있는 것이고, 실질적으로는 최소로 유지될(idle 상태) 쓰레드 수(10개?)가 있고, 요청이 많아지면 그에 따라 큐에 쌓였다가 쓰레드를 만드는 등 일반적인 쓰레드풀의 동작을 하고 있었습니다.
그래도 결국에는 동시에 10만건이 오면 쓰레드풀 동작에 따라 200개의 쓰레드가 작업을 할 수도 있는 것이라고 생각했습니다.
그러면 어쨋든 10만 개의 Request가 있다면 Controller 1개가 전부다 처리하는거잖아?
네. 저런 위험한(?) 생각을 해버렸습니다.
Request별로 쓰레드가 따로 생성되고 각각의 Context를 갖는다는 것을 인지하고 있었지만, 그 쓰레드들이 Controller 객체를 공유하니까 이거 문젠데? 라는 생각을 말이죠.
어떻게 돌아가는지 보자
놓치고 있는게 있는지 살펴본다
위와 같은 의식의 흐름 끝에 도달한 결론인 "Controller 객체 한개가 10만 개의 Request를 처리하는 건 문제다." 라는 것을 갖고 뭔가 빠진게 있나 고민하기 시작했습니다.
왜냐하면 스프링은 그 10만 개의 Request를 하나의 Controller가 이슈 없이 잘 처리하고 있으니까 제가 이상한게 분명한 것 같았기 때문입니다.
나와 같은 의문을 갖는 사람들이 예전에도 있었다
- okky 커뮤니티에서 2011년에 올라왔던 글 (https://okky.kr/article/163660)
- stack overflow에서 질문은 다르지만 의도가 유사한 글 (https://stackoverflow.com/questions/40496651/how-spring-mvc-controller-handle-multiple-long-http-requests)
위의 내용들을 간략하게 요약하면, 결국에는 내가 생성한 Controller 클래스에 대한 정보가 JVM 메모리 영역 중에 '메소드 영역'에 올라간다는데에 있다는 것입니다.
무슨 얘기냐면, Controller 객체를 생성하면 객체는 힙에 생성되지만 해당 클래스의 정보(메소드라면 메소드 처리 로직(명령들))는 메소드영역에 생성된다는 것입니다.
결론적으로 JVM 구조를 언급했던 지난 포스트를 참고했을 때, 힙영역이든 메소드영역이든 결국에 모든 쓰레드가 객체의 메서드(바이너리 코드 자체이자 메모리)를 공유할 수 있다는 것일 뿐 공유를 위해 블록이 되는 것은 아닌 것입니다.
이게 내 결론이다
바보같은 질문
"공유"라는 말이 단순하게 같이 쓸 수 있다! 이렇게 생각하고 끝냈어야하는데 바보같이 공유하면 동기화를 해야지 이러면서 lock을 걸고... lock이면 병목... 이런 생각을 했던게 문제였습니다.
그냥 말 그대로 공유였습니다. 내부적으로 상태를 갖는게 없으니 그냥 메소드 호출만 하기 때문에 동기화할 필요가 없고 처리 로직만 쓰이기 때문에 1만 개의 요청이든 10만 개의 요청이든 상관없다는 얘기인 것이지요.
(단, 매번 Controller 객체를 새로 생성하는 방식(scope=prototype)은 언제 쓰이는지 여전히 의문입니다.)
앞으로 해야할 일
- 스프링 Bean을 생성할 때, 절대로 상태를 갖는 Bean을 생성하지 말아야 한다.
- 스프링 Bean이 상태를 갖게 되었을 때는 그 상태를 공유하는 모든 쓰레드들로 부터 안전할 수 있게 동기화를 해줘야하고 동기화를 하는 순간 싱글톤으로써의 혜택이 날아간다고 봐야하기 때문입니다.
의식의 흐름
- Controller(Bean) 하나가 어떻게 수 많은 쓰레드로부터 처리를 할 수 있는지 알게되었다.
- 그런데 이제 생각이 확장되어 '톰캣 MaxThreadPoolSize는 200개인데 DBCP인 HikariCP는 왜 maximumPoolSize가 default 10개인가에 대한 고민이 시작되었다. (ThreadPool이랑 ConnectionPool이 다른 것 같긴 한데...)
- 실질적인 쓰레드의 수가 아니라 DB 연결에 쓰이는 Connection 객체를 공유하는 것이라 개수가 다르다는 결론!
- 톰캣 쓰레드는 왜 200개까지나 늘릴까?, 그 이상 튜닝도 하는거로 아는데...? 어차피 서버 코어 쓰레드 수 이상으로는 작업 못하지 않나? 하는 생각도 했다. (
톰캣이 blocking 방식이라 여러 요청을 처리하기 위해 그렇다는 것 같은데...) -> (수정합니다. tomcat 8부터는 nio 즉, non-blocking 방식을 쓴다고 합니다. 댓글 참조! 그러고 보니 스프링 부트 애플리케이션 실행하면 아래와 같이 나왔던 것도 같다. nio 라고 써있네!)
...Starting ProtocolHandler ["http-nio-8080"] ...
그런데 상기의 고민들은 여기서의 주제와 약간 벗어나기 때문에 일단은 마무리를 짓는다.
추후에 저 고민에 대한 포스팅이 있기를 바라며...