개발 일기

[Spring Boot] CGI, Servlet, Front Controller, Spring Web MVC 본문

Back-End/Spring

[Spring Boot] CGI, Servlet, Front Controller, Spring Web MVC

개발 일기장 주인 2024. 5. 2. 03:40

Servlet

CGI의 등장

처음 웹 프로그래밍에서 웹 서버는 정적 데이터만 전달하면 됐다. 누가 접속하던지에 관계 없이 똑같이 정적인 데이터를 기반으로 뷰를 렌더링하면 됐다. 즉, 웹 서버는 사용자(요청)에 맞게 동적인 데이터를 처리할 필요가 없었다.

그러나 웹 페이지에서 동적인 컨텐츠의 제공의 필요성이 생기게 됐다. 이를 위해 CGI라는 것이 나오게 된다.

 

CGI의 문제점 및  Servlet의 등장

동적 데이터를 처리하는 CGI (Common Gateway Interface)는 이름에서 알 수 있듯이 인터페이스 이다. 인터페이스는 하나의 규약이라고도 한다. 구현체는 이 인터페이스를 기반으로 구현되어야 한다. 

그래서 웹 서버(Apache)와 CGI 구현체(C, PHP 등) 사이의 규약을 CGI라고 할 수 있다. 

이를 통해 동적인 컨텐츠를 제공받을 수 있게 됐다.

그러나 이 CGI에는 2가지 문제점이 있었다.

첫번째, ㄴ 들어올때마다 메모리에 적재된 실행중인 프로그램 인스턴스인 프로세스를 사용한다는 것이다.

두번째, 위 사진과 같이 똑같은 CGI 구현체를 사용하는 요청이더라도 요청 자체가 다르다면 CGI 구현체를 또 따로 만든다는 것이였다. 매번 객체를 생성하고 종료하는 작업은 서버 리소스를 불필요하게 낭비하게 된다.

 

그래서 Servlet을 도입하여 첫번째는 프로세스 대신에 쓰레드를 사용하고 두번째 문제는 싱글톤 패턴을 적용하여 문제를 해결했고 추가로 웹 어플리케이션 서버(WAS)를 추가로 도입하게 된다.

Servlet

Servlet도 CGI와 마찬가지로 Interface이다.

이전 블로그에서도 정리했듯이 Web Server는 정적 컨텐츠를 처리하고 WAS 내부의 웹 컨테이너에서 웹 서버로 부터 들어오는 요청을 받아서 요청 마다 쓰레드를 생성하고 이를 Servlet 구현체와 연결시켜 준다.

서블릿은 개발자가 HTTP 요청을 일일이 파싱하지 않고 API를 통해 쉽게 처리할 수 있도록 돕고 요청을 처리하기 위한 메소드인 service() 메소드를 HttpServlet 객체를 찾아가 확인해 보니

 위 사진과 같이 각 HTTP Method 별 분기처리가 되어 있는 것을 확인할 수 있다. 그래서 이제 HTTP Method에 따라 doGet(), doPost() 등에 비즈니스 로직을 담아서 오버라이딩해주면 처리되는 것이다.

이게 전체적인 Servlet의 흐름이다.

 

Servlet Container가 HttpServlet을 상속받아 구현한 Servlet 구현체들을 관리한다. Servlet Container는 Servlet의 생명주기를 관리하고, 클라이언트로부터의 요청에 따라 적절한 Servlet 인스턴스를 생성하고 호출하게 되는데 HttpServlet에는 Servlet 인터페이스를 상속받았기 때문에 init(), destroy() 메소드를 포함하고 있다. 즉,  이러한 생명주기를 Servlet Container에서 관리하게 된다.

 

시나리오를 통해 어떻게 동작하는지 살펴보자.

사용자가 요청을 보내면 서블릿 컨테이너에서 해당 요청과 매핑된 서블릿을 조회하고 아직 없으면 서블릿 인스턴스를 생성하고 요청을 처리한다. 작업이 종료되도 destroy()되지 않고 서블릿 컨테이너 안에서 관리된다.

만약, 이미 서블릿 컨테이너에 관리되고 있는 서블릿과 매핑되는 요청이 발생한다면? 그대로 재사용하여 작업을 처리한다.

 

Servlet 문제점 및 Front Controller 등장

그러나 서블릿 또한 문제점이 있었다. 각 요청마다 하나의 서블릿이 1:1로 매핑되는 구조라 (문제1) N가지의 요청이 있다면 N개의 서블릿 정의가 필요하며 각 서블릿을 생성할 때마다 Web.xml에 등록해주어야한다. 이럴 경우 몇몇 서블릿 각각의 service()에서 공통되어서 실행되는 (문제2) 공통 로직이 발생할 수 있고 service(HttpServletRequest req, HttpServletResponse)를 보면 모든 요청들이 (문제3) 서블릿에 의존적인 코드를 작성해야만 했다.

 

이러한 문제들을 해결하기 위해 클라이언트의 요청을 받는 서버의 최 앞단에 모든 요청을 받을 수 있도록 컨트롤러를 하나 만들고 각 요청마다 처리하는 로직을 찾아서 묶어 놓고 요청이 들어왔을때 해당 로직으로 보내주도록 했다. 이를 Front Controller Pattern이라고 한다.

(해결1) 각각의 서블릿이 아닌 하나의 서블릿을 통해서 요청을 수행할 수 있게 됐고 (해결2) Front Controller에서 공통 로직을 처리할 수 있었다.

Front Controller = Dispatcher Servlet

 

이렇게 됐을때 Front Controller는 다음과 같은 역할을 가졌다.

  1. 클라이언트와 서버 연결
  2. 각 요청에 맞는 컨트롤러를 매핑하여 정보를 보관
  3. 요청이 들어오면 매핑 정보를 찾아 해당 컨트롤러 호출
  4. 전달할 결과를 생성
  5. 결과를 사용자에게 반환

이렇게 됐을때 (문제4) Front Controller의 책임이 너무 컸고 그래서 이 역할을 (해결 4) 분리하게 됐다.

요청이 들어오면 Front Controller에서 받고 요청 핸들러(컨트롤러) 검색 역할을 분리하여 핸들러 검색 요청을 하고 Front Controller는 createParamMap(HttpServletRequest req)를 통해 순수 자바 객체를 만들어서 컨트롤러로 전달하고 이때 결과를 전달 받을 자바 객체인 model도 같이 컨트롤러로 전달한다. 이때 알 수 있는 것이 서블릿 객체가 컨트롤러 인자로 전달되는 것이 아니라 (해결3) 자바 객체로 전달된다. 마지막으로 응답받은 String 타입의 viewName을 View Resolver의 인자로 보내어 뷰 네임과 일치하는 뷰를 조회하여 반환하고 Controller로 부터 완성된 model을 Front Controller가 응답받은 뷰에 심어주고 클라이언트한테 보내지게 되는 것이다.

이렇게 Front Controller의 역할을 분리함으로써 책임을 줄이고 서블릿으로 부터 독립적으로 개발할 수 있게 됐다.

 

이를 기반으로 만들어진 것이 Spring Web MVC이다.

 

Spring Web MVC

위에서 얘기했던 것 중 Front Controller가 Dispatcher Servlet역할을 하는 것이고 Controller(Handler)를 실제로 호출하기 전에  어댑터 역할을 수행하는 Handler Adapter라는 개념을 도입하여 다양한 종류의 핸들러를 호출할 수 있도록 하여  보다 확장성 있는 개발을 가능케 했다. 또한 비즈니스 로직에만 집중할 수 있게 됐다.

 

참고

https://www.youtube.com/watch?v=h0rX720VWCg

https://www.youtube.com/watch?v=2pBsXI01J6M

https://sigridjin.medium.com/servletcontainer%EC%99%80-springcontainer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80-626d27a80fe5-
-> 추가공부