웹 브라우저가 HTTP 요청을 하게 되면 HTTP Header, Body 등 웹 브라우저에서 보낸 다양한 정보가 서버로 넘어가게 된다.
웹 브라우저가 HTTP 요청을 하면 해당 메시지를 서버가 받아서 메시지를 파싱도 하고, URL이 뭔지 판단도 하고, content-type도 넘어오니까, 원하는 정보를 몇 글자인지 보고 파싱해서 사실하면 로직을 처리할 수 있다. 거기에 response를 client에게 전달해 주면 된다. 그러나, 이런 작업은 반복적인 작업도 많고, 단순한 비즈니스 로직 하나를 처리하기 위해서도 귀찮은 일이 생긴다. 이걸 더 편리하게 해 주기 위해서 나온 게 서블릿이다.
서블릿은 이런 HTTP 작업을 단순화해 준다. HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest 객체와 HTTP 응답 정보를 편리하게 사용할 수 있는 HttpServletresponse 객체를 제공해 줘서, 개발자는 HTTP 스펙을 쉽게 사용할 수 있게 되는 것이다.
서블릿(Servlet)
- 자바 웹 애플리케이션 개발을 위한 기술로, 동적인 웹 콘텐츠를 생성하는 데 사용
- HTTP 요청과 응답을 처리하기 위한 자바 클래스
- 서블릿 컨테이너(예: Apache Tomcat)에서 실행되며, 요청에 대한 처리와 스레드 관리를 담당
HTTP 요청과 응답을 서블릿이 어떻게 처리해주는지 알아보자.
- HTTP 요청이 들어오면 WAS는 Request 객체랑 Response 객체를 새로 만들어서 서블릿 객체를 호출한다.
- 그럼 Request 객체에서 HTTP로 어떤 요청이 들어왔는지 확인하고
- 해당 로직을 처리한 다음(해당 로직을 처리 주는 것이 바로 서블릿 객체이다)
- Response 객체에 Client에게 전달해야 할 응답 정보를 담아서 전달해 준다. 이때 WAS가 HTTP 응답 정보를 생성해 준다.
- Client가 해당 정보로 렌더링 해서 이제 보여준다.
서블릿 컨테이너
WAS 안에는 서블릿 컨테이너라는 게 있는데 서블릿 컨테이너가 서블릿 객체를 자동으로 생성해 준다. 호출도 해주고, 관리까지(예를 들면 WAS 종료될 때 서블릿도 같이 종료시키는) 해준다. 톰캣은 서블릿 컨테이너의 종류 중 하나이다.
즉, WAS는 웹 서버와 서블릿 컨테이너로 구성된다고 생각하면 된다. 사실 경계가 모호하기 때문에 종류로 생각하는 게 편하다.
웹 서버는 nginx와 apache가 해당하는 것이고, 서블릿 컨테이너는 apache tomcat(아파치 톰캣)이 해당한다.
서블릿 컨테이너의 장점 중 하나는 싱글톤으로 관리된다라는 사실이다. 싱글톤은 객체를 하나만 생성해놓고 서로 공유해서 사용하는 디자인 패턴을 의미한다. 만약 사용자가 10000명 이상으로 온다면 비즈니스 로직으로 개게를 엄청나게 만들어야 되는데, 싱글톤을 사용하면 하나를 공유해서 사용하면 되므로 딱 봐도 이점이 느껴진다.
그렇다고, request랑 response를 다 공유하는 건 아니다. 웹 브라우저에서 사용자마다 전달해오는 데이터는 당연히 다를 것이다.
(내 아이디 비번과 다른 사람의 아이디 비번이 다른 것과 마찬가지이다)
request, response 객체는 따로 생성하지만 비즈니스 로직인 서블릿은 공유해서 사용하는 것이다. 항상 재사용한다. 그래서 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근하게 된다. (그래서 공유 변수 사용에 주의해야 한다)
또 다른 중요한 장점은 서블릿 컨테이너는 멀티 스레드 처리도 지원을 한다는 사실이다.
위에 그림처럼 request가 생기면 서블릿을 호출하게 되는데, 그럼 서블릿은 누가 호출하는 건가??
쓰레드가 호출하게 된다. 스레드는 애플리케이션의 흐름이라고 알고 있는데, 말 그대로이다. 스레드가 아무 작업을 안 하고 쉬고 있다가, 만약에 요청이 들어오면 스레드를 할당해 줘서 서블릿을 호출해 주는 시스템인 것이다. 요청하고 응답까지 다 하면, 스레드는 다시 휴식하게 되는 것이다.
다중 요청이 들어올 때도, 쓰레드가 여러 개면 해결이 될 것 같다. 요청마다 스레드를 생성해 주면 되는 것이다. 이렇게 생각했는데 이 방법은 당연히 단점이 있다.
장점으로는 동시 요청을 처리하면서, 하나의 쓰레드가 지연되어도, 나머지 스레드는 정상 동작하기 때문에 상관이 없다.
그렇지만 쓰레드는 생성 비용이 매우 비싸고, 고객의 요청이 올 때마다 새로운 스레드를 생성하면 응답 속도가 늦어지고, 고객 요청이 너무 많이 오면 결국 임계점을 넘기면 서버가 죽을 수도 있는 것이다.
근데 WAS는 멀티 스레드를 다 처리해주기 때문에 멀티 스레드 관련 코드를 신경 안 써도 된다.
웹 서블릿의 동작 원리
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("HelloServlet.service");
System.out.println("request = " + request);
System.out.println("response = " + response);
String username = request.getParameter("username");
System.out.println("username = " + username);
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().write("hello " + username);
}
}
위의 코드를 한번 살펴보자. @WebServlet 어노테이션을 선언해주면 이제 서블릿 컨테이너가 관리를 해주게 되는 것이다.
서블릿은 클라이언트의 요청을 HTTP 형태로 받는다. @WebServlet 어노테이션의 Parameter로는 name과 urlPatterns를 받는다.
단어 그대로, name은 서블릿의 이름이고, urlPattern은 클라이언트에서 요청하는 URL이다. 이 둘을 매핑해 주는 것이다.
만약 HTTP를 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행한다.
protected void service(HttpServletRequest request, HttpServletResponse response);
위의 함수 파라미터 그대로 해석하면 request에는 클라이언트 요청, response는 서블릿이 이제 대답을 준다. 해당 함수를 어떻게 커스터마이징하고 조작하는 역할을 이제 서블릿 컨테이너 안의 서블릿이 해주는 것이다. 그 조작을 service함수 내에서 하면 되는 것이다.
그래서 서블릿의 이름과 urlPattern은 중복되게 하면 안된다.
그럼 위의 코드에서 request 객체와 response 객체에는 어떤 것들이 오는지 로그를 찍어보자.
apache.catalina는 tomcat쪽 라이브러리이다.(서블릿 컨테이너 쪽 라이브러리)
HttpServletRequest나 HttpServletResponse는 interface이다.
이제 Tomcat이나 Jetty 등등 이런 WAS 서버들이 서블릿 표준 스펙을 구현하는 것이다. 여기 찍히는 것은 Tomcat의 구현체라고 볼 수 있다.
쿼리 파라미터
서블릿의 장점 중 하나는 쿼리 파라미터를 아주 쉽게 읽을 수 있게 해 준다는 사실이다.
request로 query parameter를 전송하면 request 객체에서 getParameter를 사용하면 해당 query 파라미터를 읽어낼 수 있다.
응답 메시지를 보여줄려면 response에 넣어줘야 한다.
setContentType를 text/plain으로 준다면 단순 문자열을 전송해 주는 것이다.
setCharacterEncoding은 utf-8로 둔다.
그럼 여기까지 정보는 Header에 들어가게 된다.
나머지는 response.getWriter().write() 를 통해서 보내준다.
모든 HTTP 요청 응답 정보들을 보고 싶으면 아래와 같이 설정하면 된다. (application.properties)
logging.level.org.apache.coyote.http11=debug
지금까지의 동작 과정
- 스프링부트로 프로젝트를 생성 후 실행 → 스프링부트 안에 있는 내장 톰캣 서버를 띄워준다.
- 톰캣 서버는 내부에 서블릿 컨테이너를 가지고 있다.
- 서블릿 컨테이너를 통해서 내부에 서블릿을 다 생성해준다. → HelloServlet 생성
- GET /hello?username=kim HTTP/1.1 Host: localhost:8080 웹브라우저가 이런 형식으로 HTTP 메시지를 만들어서 서버에 요청을 한다.
- 서버는 Request, Response 객체를 만들어서 HelloServlet의 Service 메서드를 호출하면서 response 객체를 만들어서 웹 브라우저에게 전달해 준다.
- HTTP 응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해 준다. (WAS가 해줌)
HttpServletRequest의 역할
위에서 말했듯이 HTTP 요청 메시지를 개발자가 직접 파싱 하기 어려우니까, 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱 한다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.
추가적인 역할
- 임시 저장소 기능 : HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
- 저장 : request.setAttribute(name, value)
- 조회 : request.getAttribute(name)
- 세션 관리 기능 : request.getSession(create: true)
클라이언트에서 HTTP를 통해 서버로 전달하는 방법
- GET - 쿼리 파라미터
- 검색, 필터, 페이징 등에서 많이 사용하는 방식
- 서버는 HttpServletRequest를 가지고 정보를 쉽게 가져올 수 있다
- POST - HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달 → 똑같이 request.getParameter로 꺼낼 수 있다!
- 클라이언트 입장에서는 차이가 있는데, 서버 입장에서는 둘의 형식이 동일한 것이다
- 회원가입, 상품 주문에 사용
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON형식으로 사용
- POST, PUT, PATCH
- String으로 주고 받는 방식(잘 쓰이지 않음)
@WebServlet(name = "requestBodyStringServlet",urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
response.getWriter().write("OK");
}
}
- getInputStream을 하면 메시지 body의 내용을 byte코드로 바로 얻을 수 있다
- byte 코드를 String으로 바꿔야 하는데 Spring에 StreamUtils라는 유틸리티 클래스를 사용하면 된다.
- 인코딩 정보를 알려줘야 한다
- 지금 이건 String 형식으로 주고 받는 것이다.
- JSON으로 주고 받는 방식이 보통 사용되는데 코드는 아래와 같다.
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// request
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());
}
}
// HelloData
@Getter @Setter
public class HelloData {
private String username;
private int age;
}
- 스프링은 JSON 라이브러리를 기본적으로 jackson이라는 것을 사용한다.
- ObjectMapper를 선언하고 readValue를 통해서 class타입으로 변환할 수 있다. 여기서는 HelloData로 변환을 하였다.
- 나중에는 MVC가 제공하는 기능을 통해서 더 쉽게 JSON을 파싱할 수 있다
서블릿과 자바로 동적으로 변경해야 하는 부분을 보여주고 싶을 때 템플릿 엔진을 사용할 수 있다. 템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있다.
템플릿 엔진에는 JSP, Thymleaf, Freemarker, Velocity 등이 있다. 이전에는 JSP가 많이 사용되었었지만, 스프링은 현재 Thymeleaf를 사용하는 걸 추천한다.
단순히 서블릿만을 사용하기에는 HTML을 다루기가 너무 힘들기 때문에 JSP나 타임리프 같은 것들을 사용하게 된다.
JSP(JavaServer Pages)
- 서블릿을 기반으로 하는 웹 페이지 개발을 위한 기술
- HTML 코드 내에 자바 코드를 삽입하여 동적인 웹 콘텐츠를 생성할 수 있다
- JSP 파일은 서블릿으로 변환되어 서블릿 컨테이너에서 실행
서블릿과 JSP의 한계
분리되어 있지 않다. 이게 제일 핵심 단점이다. 서블릿과 JSP를 사용하다 보면 비즈니스 로직이랑 화면(View)랑 분리되지 않아서 지저분하고 나중에 코드를 볼 때도 복잡하여 유지보수가 어렵다.
그래서 등장하게 된 것이 MVC 패턴이다.
MVC 패턴
MVC 패턴은 컨트롤러와 뷰라는 영역으로 서로 역할을 나눈다. MVC 용어에서 알 수 있듯이 여기서 V는 view이고, C는 Controller이다.
M은 모델인데, 모델은 뷰에 출력할 데이터를 담아두고 뷰가 필요한 데이터를 모두 모델에 담아서 전달한다. 덕분에 뷰는 비즈니스 로직이나 데이터 접근 같은 걸 몰라도 되고, 화면만 렌더링 하면 되는 것이다.
그리고, Service로도 이후에 나눈다. Controller에 비즈니스 로직을 둘 수도 있지만 이렇게 되면 또 컨트롤러가 부담을 많이 지는 문제가 발생하기 때문에 Service 계층을 따로 나누어서 처리한다. Controller는 서비스 계층을 호출하는 역할만 하면 된다.
MVC 패턴과 관련된 프레임워크도 많았었는데, 어노테이션 기반의 스프링 MVC가 나오면서 거의 정리되었다.
MVC 패턴의 한계
MVC 패턴의 한계라기보다는 MVC 패턴을 개선하는 방안이다. MVC 패턴은 요청하는 부분과 응답하는 부분, 즉 request response 코드가 controller마다 따로 구성되어 있다. 중복된 코드는 항상 개선점에 해당한다. 그래서 FrontController라는 것이 고안되었다.
FrontController 패턴
- Front Controller 패턴은 소프트웨어 디자인 패턴 중 하나로, 웹 애플리케이션에서 중앙 집중화된 컨트롤러 역할을 수행하는 컴포넌트
- 이 패턴은 클라이언트의 모든 요청을 하나의 진입점으로 보내고, 해당 요청을 처리하기 위해 적절한 핸들러(컨트롤러)로 라우팅 하는 구조를 가지고 있다
- Front Controller를 제외한 나머지 Controller는 서블릿을 사용하지 않아도 된다.
스프링 웹 MVC에서 보면 FrontController를 사용한다. 우리가 DispatcherServlet으로 알고 있는 것이 FrontController 패턴으로 구현되어 있다. 프런트 컨트롤러 서블릿이 클라이언트 요청을 다 받고 프런트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출하는 즉, 입구를 하나로 만드는 전략이다.
개선점
위의 서블릿 코드에서 override 된 service코드를 보면 HttpServletRequest랑 HttpServletResponse를 계속하여 쓰게 되는데, 해당 코드가 컨트롤러 입장에서는 필요 없다. Controller는 View로 객체를 전달하기만 하면 되는데, 해당 객체를 Model 객체라고 한다. 그럼 이제 우리가 아는 구조가 나오게 된다.
전형적인 Front Controller 패턴의 구조는 다음과 같다:
- 클라이언트는 요청을 Front Controller에게 전송
- Front Controller는 요청을 받아 해당 요청을 처리할 적절한 핸들러(컨트롤러)로 라우팅
- 핸들러는 요청을 처리하고 필요한 작업을 수행한 후 응답을 생성
- Front Controller는 핸들러로부터 받은 응답을 클라이언트에게 전송
뷰 리졸버 (View Resolver)
뷰 리졸버(View Resolver)는 컨트롤러가 처리한 결과를 어떤 뷰로 보여줄지를 결정하는 역할을 한다.
뷰 리졸버는 뷰의 논리적인 이름을 실제 뷰 객체로 변환하여 반환하는 기능을 담당한다.
웹 애플리케이션은 MVC (Model-View-Controller) 아키텍처를 따르는데, 이 구조에서 컨트롤러는 요청을 처리하고 필요한 작업을 수행한 후 결과를 뷰에 전달한다. 뷰 리졸버는 컨트롤러가 반환한 뷰의 논리적인 이름을 실제 뷰 객체로 매핑하여 클라이언트에게 보여줄 뷰를 결정하게 되는 것이다.
Spring MVC
- Spring Framework에서 제공하는 웹 애플리케이션 개발을 위한 모듈
- Model-View-Controller(MVC) 아키텍처 패턴을 기반으로 한다
- 요청을 처리하는 컨트롤러(Controller), 데이터를 처리하는 모델(Model), 화면을 표현하는 뷰(View)로 구성
- 애노테이션 기반의 설정과 느슨한 결합을 통해 유연하고 효율적인 웹 애플리케이션 개발을 지원
- 요약하면, 서블릿은 자바 기반의 웹 애플리케이션 개발을 위한 기술이고, JSP는 서블릿을 기반으로 동적인 웹 페이지를 개발하기 위한 기술
- Spring MVC는 Spring Framework에서 제공하는 모듈로, 웹 애플리케이션 개발을 위한 MVC 패턴을 구현하는 방식
- Spring MVC는 서블릿과 JSP를 기반으로 하며, 개발자에게 편의성과 유연성을 제공하여 웹 애플리케이션 개발을 보다 쉽게 할 수 있도록 도와준다
참고 : 인프런 김영한 님 스프링 MVC 편
'백엔드 > SpringBoot' 카테고리의 다른 글
[Spring] DataBase 설정 준비가 되지 않았을 때 (1) | 2024.01.08 |
---|---|
[Spring] 데이터 전달과 HTTP 메시지 컨버터 (1) | 2024.01.08 |
[Spring] 스프링 시큐리티 + JWT (0) | 2023.05.31 |
[Spring] Spring Security 의존성을 사용하는 로그인 (0) | 2023.05.31 |
Spring Security를 사용하는 이유와 동작 방식 (1) | 2023.05.25 |