Spring MVC

서블릿

cwchoiit 2024. 4. 24. 21:47
728x90
반응형
SMALL

참고자료:

 

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

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com

 

Spring MVC를 제대로 공부하려면, Spring MVC가 없던 시절로 돌아가서 어떤 웹 기술로부터 출발했고 어떻게 발전했는지 이해하는게 중요하다는 것을 깨닫고 맨 밑바닥부터 잡고 올라와 볼 생각이다. 그 시작엔 서블릿이 있다.

 

서블릿

간단한 HTML Form에서 데이터를 서버로 전송한다고 가정해보자.

유저는 Form에 username, age를 입력하고 전송 버튼을 누르면, HTTP 프로토콜을 이용하여 POST 방식으로 서버로 요청이 전달된다.

그럼 서버입장에선 날라온 요청(Request)을 분석하고 적절한 처리를 해야할 것이다.

 

근데 만약 아무런 도움을 받지 않고 0부터 100까지 전부 다 직접 구현해야 한다면 어떨까?

 

우선 TCP/IP 연결 대기부터 시작해서 소켓을 연결하는 코드를 작성하고, HTTP 요청 메시지를 파싱하고, 어떤 Method이고 어떤 URL로의 호출인지 파악하고, 바디를 읽어서 데이터를 가져오고, .... 응답 메시지를 생성하고, 응답을 전달하는 이 모든 과정을 직접 구현해야 한다면 정말 복잡한 일이 될 것이다. 그리고 이 안에서 의미있는 비즈니스 로직은 바디에 있는 데이터를 가져와서 비즈니스 로직에 맞게 데이터를 어떻게 정제하고 데이터베이스에 저장하는 딱 이 부분밖에 없다.

 

이 모든 일련의 과정을 전세계 모든 개발자가 다 일일이 하고 있다면 너무나 비효율적이지 않을까? 여기서 서블릿이 등장한다.

서블릿의 역할

서블릿은 저 부분에서 딱 비즈니스 로직을 제외한 모든 작업을 다 대신해준다.

그래서 지금 사진처럼 딱 초록색 박스에 있는 부분만 개발자는 신경쓰면 된다.

서블릿 코드를 한번 살펴보자.

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response){
    	//애플리케이션 로직
    }
}

 

최신 스타일의 서블릿 코드이지만 충분하다. 저렇게 urlPatterns"/hello"라고 설정하고 웹 브라우저에서 "서버URL/hello" 라고 입력하면 이 서블릿 코드가 실행이 된다. 그래서 실제로 요청과 응답에 필요한 모든 부분은 이 서블릿이 대신 다 해주고 저 코드에서처럼 service()에서 작성한 애플리케이션 로직(비즈니스 로직)에 관련된 코드만 개발자가 작성하면 되는것이다.

 

저 service()가 호출되면 두 개의 파라미터가 들어온다. 

  • HttpServletRequest request: HTTP 요청 정보를 편리하게 사용할 수 있는 객체
  • HttpServletResponse response: HTTP 응답 정보를 편리하게 제공할 수 있는 객체

위 그림처럼 클라이언트로부터 요청이 들어오면 그 요청 정보를 직접 파싱하지 않고 서블릿이 다 해준다고 했는데 예를 들면, Method 정보라던가, Parameter 정보라던가, Content-Type 정보라던가 등 요청 시 필요한 모든 정보를 서블릿이 우리대신 다 만들어서 가져다주고 그게 HttpServletRequest이다.

 

그리고, 위 그림처럼 응답을 해줘야 하는데 이 때 응답 메시지를 직접 만드는게 아니라 서블릿이 제공해주는 HttpServletResponse 객체를 통해 내가 원하는 것을 넣어서 응답만 해주면 된다.

 

서블릿과 HTTP 요청, 응답 흐름

그래서 이 내용을 그림으로 살펴보면 다음과 같다.

 

HTTP 요청이 딱 들어오면 웹 애플리케이션 서버(WAS)와 개발자가 다음과 같은 작업을 한다.

  • WAS는 Request, Response 객체를 새로 만들어서 서블릿 객체 호출
  • 개발자는 Request 객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용
  • 개발자는 Response 객체에 HTTP 응답 정보를 편리하게 입력
  • WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성

 

저 그림에서 서블릿 컨테이너라는 게 보이는데 쟤는 뭘까?

서블릿 컨테이너

  • 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 함
  • 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리
  • 서블릿 객체는 싱글톤으로 관리
    • 고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율
    • 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용
    • 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근
    • 공유 변수 사용 주의
    • 서블릿 컨테이너 종료시 함께 종료
  • 동시 요청을 위한 멀티 스레드 처리 지원

 

동시 요청을 위한 멀티 스레드 처리를 지원한다는 건 어떤 의미인지 살펴보자.

 

동시 요청과 멀티 스레드

지금까지 배운 내용은 웹 애플리케이션 서버가 있고 클라이언트가 이 WAS에 HTTP 요청을 보내면 그에 상응하는 미리 만들어 둔 서블릿 객체를 서블릿 컨테이너로부터 꺼내서 요청과 응답 데이터를 편리하게 꺼내쓰고 작성하여 다시 응답을 돌려준다는 것이다.

 

이런 그림이라고 보면 되는데 여기서 궁금한 점이 있다. 저 호출단계에서 서블릿 객체는 누가 호출할까?

서블릿 객체를 호출하는 것은 스레드이다.

 

스레드

  • 애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 스레드
  • 자바 메인 메서드를 처음 실행하면 main이라는 이름의 스레드가 실행
  • 스레드가 없다면 자바 애플리케이션 실행이 불가능
  • 스레드는 한번에 하나의 코드 라인만 수행
  • 동시 처리가 필요하면 스레드를 추가로 생성

스레드가 서블릿 객체를 호출하고 사용한다는 것을 알았고 이 일련의 과정을 또 그림으로 이해해보자.

 

최초의 상태

 

요청이 들어옴

요청이 들어왔으니 해당 요청에 대해 스레드를 할당한 다음 스레드는 서블릿 객체를 호출한다.

 

요청을 처리하고 응답

모든 작업이 끝남

 

 

단일 요청일 땐 이렇게 간단하게 처리가 된다. 이제 다중 요청을 봐보자.

 

요청이 들어옴

 

요청1이 들어온 상태에서 또 다른 요청2가 들어옴

요청1이 이미 스레드를 사용중이기 때문에 요청2는 대기해야한다.

그런데, 요청1이 처리가 지연되면 결국 요청1, 요청2 모두 작업을 처리하지 못하는 상태가 일어날 수 있다.

요청1과 요청2 모두 처리 불능 상태로 빠짐

 

이런 문제를 해결하기 위해서 가장 간단한 방법은 스레드를 하나 더 생성하면 된다.

요청1이 스레드를 사용하고 있는 중에 요청2가 들어와서 새로운 스레드를 만듦

 

이렇게 요청이 들어올때마다 스레드를 생성하면 어떤 장단점이 있을까?

장점

  • 동시 요청을 처리할 수 있다.
  • 리소스(CPU, 메모리)가 허용할 때까지 처리 가능
  • 하나의 스레드가 지연되어도, 나머지 스레드는 정상 동작한다.

단점

  • 스레드 생성 비용은 매우 비싸다. 고객의 요청이 올 때 마다 스레드를 생성하면 응답 속도가 늦어진다.
  • 스레드는 컨텍스트 스위칭 비용이 발생한다.
  • 스레드 생성에 제한이 없다. 즉, 고객 요청이 너무 많이 오면 CPU, 메모리 임계점을 넘어서서 서버가 죽을 수 있다.

 

그래서 이런 문제를 해결하기 위해 보통의 WAS는 스레드 풀이라는 개념을 사용한다.

풀 안에 적절한 수의 스레드를 미리 만들어서 넣어두고 요청이 들어올 때마다 스레드 풀에서 하나씩 꺼내서 서블릿 객체를 호출해서 요청을 처리한다. 그래서 풀에 있는 모든 스레드가 다 사용중일 때 요청이 들어오면 다음과 같이 그 요청부턴 대기상태로 기다리거나 또는 거절하게 된다.

 

이렇게 풀을 만들어서 사용하면 요청이 들어올 때마다 스레드를 생성하는 단점을 보완할 수 있다.

스레드 풀 특징

  • 필요한 스레드를 스레드 풀에 보관하고 관리한다.
  • 스레드 풀에 생성 가능한 스레드의 최대치를 관리한다. 톰캣은 최대 200개 기본 설정 (변경 가능)

스레드 풀 사용

  • 스레드가 필요하면, 이미 생성되어 있는 스레드를 스레드 풀에서 꺼내서 사용한다.
  • 사용을 종료하면 스레드 풀에 해당 스레드를 반납한다.
  • 최대 스레드가 모두 사용중이어서 스레드 풀에 스레드가 없으면 기다리는 요청을 거절하거나 특정 숫자만큼만 대기하도록 설정할 수 있다.

스레드 풀 장점

  • 스레드가 미리 생성되어 있으므로 스레드를 생성하고 종료하는 비용이 절약되고 응답 시간이 빠르다.
  • 생성 가능한 스레드의 최대치가 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.

 

그럼, 이 최대 스레드 수는 어떻게 설정하면 좋을까?

WAS의 주요 튜닝 포인트는 최대 스레드(max thread)수이다.

  • 이 값을 너무 낮게 설정하면? - 동시 요청이 많으면 서버 리소스는 여유로워도 클라이언트는 금방 응답 지연
  • 이 값을 너무 높게 설정하면? - 동시 요청이 많으면 CPU, 메모리 리소스 임계점 초과로 서버 다운

이 값을 너무 낮게 설정한다면 사용가능한 리소스(CPU, 메모리)는 넘쳐나는데도 고객은 응답을 지연받으니 매우 안좋은 결과가 생기고 이 값을 너무 높게 설정하면 동시 요청이 많아질수록 사용하는 리소스도 많아지다가 임계점 초과로 서버가 다운된다. 그래서 적절한 수의 최대 스레드 수가 필요하다. 

 

그럼 적절한 수는 어떻게 찾아요?

정답은 없고 최대한 실제 서비스와 유사하게 성능 테스트를 해가면서 적절한 숫자를 찾아야 한다. (JMeter, nGrinder와 같은)

 

가장 중요한 핵심은,

멀티 스레드에 대한 부분은 WAS가 다 처리해준다는 것.

개발자는 멀티 스레드 관련 코드를 신경쓰지 않고 아까 저 위에 코드처럼 service() 코드만 작성하면 된다.

그리고 최대 스레드 개수만 적절하게 설정해주면 된다. 

멀티 스레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)의 공유변수만 주의해서 사용하면 된다.

728x90
반응형
LIST