업데이트:

Spring이나 Spring Boot로 프로젝트를 진행하면 보통 많은 프로젝트가 MVC 구조로 작업을 진행하게 됩니다.

spring-mvc

위 이미지는 SpringMVC 구조 flow 많이 볼 수 있는 이미지입니다.

그런데 생각해보면 SpringController, Service, Repository는 모두 Bean으로 등록되고 따로 Spring에서 Bean은 따로 지정하지 않는한 singleton으로 생성하여 관리합니다.

그럼 여기서 의문점이 듭니다.

“Controller 하나가 수 많은 request를 받는 역할을 하는것인가?”, “클래스 안에 멤버 변수의 상태 동시접근 이슈는 왜 안발생하지?”

그 원인을 알아보고자 이번 포스트를 작성하게 되었습니다.

Controller


먼저 Controller는 1개가 맞습니다. Bean 객체 하나를 Spring Container에서 관리하여 필요할 때 사용하는 것인데, 핵심은 클래스를 공유하는 것일 뿐이기 때문에 동시성 이슈가 없는 것입니다.

이게 무슨 말이냐면 보통 Controller, Service, Repository에는 변화할 소지가 있는 상태값, 즉 멤버 변수를 갖지 않습니다.

(상태값을 가지게 되면 싱글톤으로 쓰면 안되겠죠)

사실 제 의문점은 여기서 끝났으나, 통신 부분에 있어 더 배울게 있어 추가로 내용을 작성했습니다.

Servlet


Spring Boot는 내장 서블릿 컨테이너인 Tomcat을 지원합니다. 이 TomcatThreadPool로 스레드를 관리하여 사용합니다.

❓혹시 스레드를 모르시는 분들은 구글링 검색해보시면 좋은 포스트가 많습니다. 참고로 “프로세스 VS 스레드”로 검색하셔서 2개의 차이를 확인해보시는걸 추천드립니다.

ThreadPool

thread-pool

ThreadPool이란 일정 갯수의 스레드를 생성해놓고 Pool에 가지고 있습니다. Request가 올때 작업 Queue에 담게 되는데, 이 작업 Queue에서 순서대로 유휴 상태에 있는 스레드를 Request에 대한 작업을 할당하고 작업이 끝나면 스레드를 반환받아 다시 유휴 상태로 돌려 놓습니다.

만약 작업 Queue가 꽉 차고 그 이상의 요청이 들어온다면 connection-refused 오류를 반환합니다.

💡Spring Boot에서는 Tomcat에서 관리할 TheradPool의 스레드 갯수를 지정할 수 있는데, 이에 따른 적정 갯수 계산법이 따로 있습니다. 이는 따로 포스팅해보도록 하겠습니다.

Tomcat은 요이 ThreadPool을 사용하여 성능 최적화를 꾀하는 것입니다.

Connector


제가 학습하면서 제일 애먹었던 부분입니다.

Connector란 소켓 연결을 하여 Connection을 얻고 이로부터 데이터 패킷을 획득, 패킷을 파싱하여 HttpServletRequest 객체로 변환하고, Servlet 객체에 전달하는 역할을 합니다. 간단히 말하면 통신으로 부터 데이터를 HttpServletRequest로 만들어 Servlet에 전달하는 역할을 합니다.

BIO Connector

BIO ConnectorJavaI/O 기술을 사용하여 Socket Connection을 처리합니다.

ThreadPool에 있는 스레드들은 “소켓 연결 → 요청 작업 처리 → 소켓 연결 종료”라는 하나의 플로우를 처리하고 다시 Pool에 들어오게 됩니다.

이렇게 되면 결국 ThreadPool에 있는 “스레드 갯수 == 동시 접속 갯수”가 될 것입니다.

그리고 이런 방식은 스레드가 충분히 사용되지 않고 유휴 상태로 낭비되는 시간이 많이 발생하게 됩니다.

이런 문제점을 해결하고자 NIO Connector가 나오게 되었습니다.

NIO Connector

위의 BIO ConnectorJavaI/O 기술을 이용한다고 말씀 드렸는데, 이는 특정 스레드가 I/O 작업을 하는 동안 추가 작업을 요청한 스레드는 블록킹 됩니다. 그래서 나온 것이 NIO(Non-Blocking I/O)이고 이를 이용하여 소켓 연결하는 것이 NIO Connector입니다.

NIO(Non-Blocking I/O)

NIO에는 3가지 핵심 포인트가 있는데 버퍼, 채널, 셀렉터이다.

  1. 버퍼
    • 커널에 의해 관리되는 시스템 메모리를 직접 사용할 수 있는 버퍼 (NIO에서 제공하는 클래스입니다.)
  2. 채널
    • 스트림처럼 읽기, 쓰기가 단방향이 아닌 양방향으로 읽기, 쓰기가 가능한 입출력 클래스
  3. 셀렉터
    • 여러개의 채널에서 이벤트를 모니터링하는 것으로 하나의 스레드로 여러 채널에 대해 처리하기 위함

개인적으로 제일 중요한 포인트가 셀렉터라 생각합니다. BIO Connector에서 가장 중요한 포인트가 스레드 이슈이고, 그것을 해결한 것이 NIO Connector니까요.

Poller

NIO Connector에선 Poller라고 하는 별도의 스레드가 커넥션을 처리하는데요, Poller는 소켓들을 캐시로 들고 있다가 해당 소켓에 데이터 처리가 가능해진 순간에 스레드를 할당해 줍니다.

원리는 다음과 같습니다.

Poller는 위에서 말한 셀렉터를 가지고 있는데, 이 셀렉터가 소켓 채널들을 모니터링하고 있고, 데이터를 읽을 수 있는 소켓에 커넥션을 얻습니다.

그리고 Worker ThreadPool에 스레드를 얻어 해당 소켓 커넥션을 넘깁니다.

Poller에선 Max Connection까지 연결을 수락하고, 셀렉터를 통해 채널을 관리하므로 작업 Queue의 사이즈와 관계없이 connection-refused 없이 커넥션을 받아 놓을 수 있습니다.

💡Max connection 같은 경우는 Spring 설정 파일(application.yml 혹은 application.properties)에서 세팅할 수 있습니다.

참고로 아래는 TomcatConnector 비교 표 입니다.

tomcat-connector

💡Tomcat 9.0에선 BIO Connectordeprecated 되었다고 합니다. 그리고 제가 학습용으로 현재 Spring Boot 2.6.3을 사용중이고, org.springframework.boot:spring-boot-starter-web 의존성을 추가하여 확인 결과 Tomcat 9.0.56을 사용중인 것으로 확인되었습니다.

마무리


처음에는 SpringBeanController의 뭔가 다른 동작방식이 있나? 라는 생각으로 가볍게 접근했었는데 Spring Boot 내장 Tomcat의 요청에 대한 동작 방식까지 알게 되었네요. 또한 NIO Connector 동작 방식도 알게 되어 진짜 좋은 학습이었다 생각합니다.

그래도 이번엔 겉핥기로 알아본거라 아직 알아야 할 부분이 많네요. (무엇보다 스레드풀 테스트 코드를 직접 짜서 확인해보고 싶네요.)

오늘도 긴 제 포스트를 읽어주셔서 감사합니다. 😄

📌참고 블로그


스프링부트는 어떻게 다중 유저 요청을 처리할까? (Tomcat9.0 Thread Pool)

댓글남기기