스프링으로 서블릿(Servlet)을 다룬다는 것
스프링으로 서블릿(Servlet)을 다룬다는 것
[10분 테코톡] 🐶 코기의 Servlet vs Spring - YouTube
글을 시작하기 앞서 해당 영상을 꼭 들어보자.
목차
개요
서블릿의 등장
서블릿 컨테이너
JSP
초기 MVC
스프링으로 서블릿을 다룬다는 것
프론트 컨트롤러
스프링의 MVC
개요
웹 서버는 다양한 Http 요청(Request)과 응답(Response) 외에도 네트워크 연결 등 많은 규칙과 처리해야하는 로직들이 존재한다.
서버가 처리해야하는 업무
#network
• 서버 TCP/IP 연결 대기, 소켓 연결
↓
#Request Http
• HTTP 요청 메시지를 파싱해서 읽기
• POST 방식, /save URL 인지
• Content-Type 확인
• HTTP 메시지 바디 내용 피싱
• 메시지 바디에 담긴 데이터를 사용할 수 있게 파싱
↓
#비즈니스 로직 수행
• 저장 프로세스 실행
• 비즈니스 로직 실행
↓
#Resposne Http
• 데이터베이스에 저장 요청
• HTTP 응답 메시지 생성 시작
• HTTP 시작 라인 생성
• Header 생성
• 메시지 바디에 HTML 생성에서 입력
↓
#network
• TCP/IP에 응답 전달, 소켓 종료
네트워크 연결 설정부터 HTTP 데이터 파싱, 로직 수행후 응답 HTTP 생성 등 많은 처리가 필요하다.
개발자가 이런 웹 서버를 매번 하나하나 만들기는 매우 힘들고 번거로운 일이다.
때문에 서블릿이라는 기술이 등장했다.
서블릿은 자바로 웹 서버를 더 쉽게 만들고자 등장한 자바로 구현된 CGI이다.
CGI : 공통 게이트웨이 인터페이스(Common Gateway Interface)의 약어로, 웹서버와 외부 프로그램 사이에서 정보를 주고받는 방법이나 규약들을 말한다.
서블릿의 등장
서블릿
이란 자바를 사용하여 웹을 만들기 위해 필요한 기술로써 서블릿 메서드 호출
만으로 웹 요청과 응답을 체계적으로 다룰 수 있게 해주는 자바 기반의 웹 애플리케이션 프로그래밍 기술
이다.
서버가 클라이언트에 대한 요청에 대한 응답을 하는 과정을 더 쉽게 구현하기 위한 기술이라고 생각하자
주로 WAS 즉,동적 웹 서버를 만들기 위한 기술이다.
#network
...
↓
#Request Http
...
↓
# 비즈니스 로직 수행
• 저장 프로세스 실행
• 비즈니스 로직 실행
↓
#Resposne Http
...
↓
#network
...
서블릿이 없다면 우리는 서버를 만들때 소켓을 만들고 listen, accept HTTP 파싱 등을 해야하지만,
서블릿 컨테이너는 이러한 기능을 API로 제공하여 복잡한 과정을 생략할 수 있게 해준다.
그래서 개발자가 서블릿에 구현해야 할 비지니스 로직에 대해서만 집중할수있다.
개발환경
서블릿 만으로 서버를 개발
할때는 톰캣같은 WAS를 설치하고 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린 다음, 톰캣 서버를 실행해야한다.
하지만 스프링 부트를 이용
하면 스프링 부트는 톰캣을 내장하고있기 때문에 스프링 부트가 지원하는 에너테이션을 이용해서 서블릿 코드를 등록해서 사용하면 된다.
서블릿이 어떻게 생겼는지 알아보자
서블릿은 HttpServlet 상속함으로써 쉽게 만들수있다.
필요시 HttpServlet method들을 재정의 할수있다.
//해당 URL로 요청이 들어오면 이 서블릿을 생성한다.
@WebServlet(name = "myServlet", urlPatterns = "/my")
public class MyServlet extends HttpServlet { //HttpServlet을 상속한다.
@Override //서블릿 객체 생성시 실행
public void init(ServletConfig config) throws ServletException{
super.init();
}
@Override //서블릿 객체 소멸시 실행
public void destroy(){
super.destroy();
}
//HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행한다
@Override
protected void service(HttpServletRequest request, HttpServletResponse response){
super.service(request,response);
}
}
핵심인 service method를 살펴보면 아래와 같다.
아래 코드는 흐름이 잘 보이도록 수정된 코드이다.
실제는 이보다 복잡함
요청에따라 doGet(),doPost() 등 메서드를 실행해 로직을 수행하고 응답을 만들어낸다.
필요시 해당 메서드들을 재정의 함으로써 요청에따라 일련의 로직을 정의 할수있다.
public class MyServlet extens HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOEception {
PrintWriter writer = request.getWriter();
writer.println("Hello, do it,if Method type is Get"); //GET으로 요청이 들어왔을때 다음 문구를 출력하도록 설정
}
}
서블릿 동작 과정
- 해당 URL로 요청이 들어오면 WAS는 HTTP Request를 서블릿 컨테이너로 전송한다
- HTTP Request를 전달받은 서블릿 컨테이너는 HttpServletRequest, HttpServletResponse 두 객체를 생성한다.
- web.xml은 사용자가 요청한 URL을 분석하여 어느 서블릿에 대해 요청을 한 것인지 찾는다
- 해당 서블릿에서 service 메소드를 호출한 후 클라이언트의 요청종류 (GET, POST)에 따라 doGet 혹은 doPost를 호출한다.
- doGet, doPost 메소드는 동적 페이지를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.
- 응답이 끝나면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킨다
서블릿을 등록하고 메서드를 수정함으로써 웹 요청 응답 흐름을 체계적으로 관리할수있다는걸 알수있다.
서블릿 특징
서블릿으로 요청을 처리할때 개발자는 아래와 같은 기본 제공 서블릿으로 HTTP 스펙을 매우 편리하게 사용
할수있다.
HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest
+ getCookies() , getHeader() 등의 HTTP 파싱 메서드들을 제공
HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse
+ HTTP 응답 정보를 생성하기위한 메서드 제공
요청 HTTP 파싱
이나 응답 HTTP 만들기
등을 서블릿이 제공하는 처리를 메서드를 통해 쉽게 다룸으로써 개발자들은 좀더 비즈니스 로직 개발에만 집중할수있다.
request.getMethod() = GET
request.getProtocal() = HTTP/1.1
request.getScheme() = http
request.getRequestURL() = http://localhost:8080/request-header
request.getRequestURI() = /request-header
request.getQueryString() = username=hello
request.isSecure() = false
//http 정보 이외에도 네트워크 포트라던지 하는 기타 정보도 알수있음
[Remote 정보]
request.getRemoteHost() = 0:0:0:0:0:0:0:1
request.getRemoteAddr() = 0:0:0:0:0:0:0:1
request.getRemotePort() = 54305
[Local 정보]
request.getLocalName() = localhost
request.getLocalAddr() = 0:0:0:0:0:0:0:1
request.getLocalPort() = 8080
• JAVA의 스레드를 이용하여 동작한다.
• MVC 패턴에서의 컨트롤러로 이용된다.
웹 전반에 걸쳐서 요청응답 흐름을 자바 메서드로 편하게 다루도록 하는게 서블릿의 대표적인 특징이다.
서블릿 컨테이너
서버에 서블릿을 만들었다고 해서 스스로 작동하는 것이 아니고 서블릿을 관리해주는 것
이 필요한데, 그러한 역할을 하는 것이 바로 서블릿 컨테이너이다.
- 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.
- 서블릿을 담고 관리하는 컨테이너
- 서블릿의 생명주기를 관리한다고 생각하면 이해하기 쉽다.
생성 소멸 관리
• 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리 #!
• 서블릿 객체는 싱글톤으로 관리 #!
고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율
최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용
모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근
• 공유 변수 사용 주의
• 서블릿 컨테이너 종료시 함께 종료 #!
• JSP도 서블릿으로 변환 되어서 사용 #!
• 동시 요청을 위한 멀티 쓰레드 처리 지원 #!
• 웹서버와의 통신 지원 #!
서블릿 컨테이너는 서블릿과 웹서버가 손쉽게 통신할 수 있게 해줍니다. 일반적으로 우리는 소켓을 만들고 listen,
accept 등을 해야하지만 서블릿 컨테이너는 이러한 기능을 API로 제공하여 복잡한 과정을 생략할 수 있게 해줍니다.
그래서 개발자가 서블릿에 구현해야 할 비지니스 로직에 대해서만 초점을 두게끔 도와줍니다.
JSP(Java Server Page)
서블릿
은 자바 소스코드 속에 HTML코드가 들어가는 형태인데,JSP
는 이와 반대로 HTML 소스코드 속에 자바 소스코드가 들어가는 구조를 갖는 웹어플리케이션 프로그래밍 기술
서블릿 코드를 먼저 보자
@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//Content-Type: text/html;charset=utf-8
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>안녕?</div>");
writer.println("</body>");
writer.println("</html>");
//String result = objectMapper.writeValueAsString(data);
//response.getWriter().write(result); //json
}
}
동적 페이지를 응답하는경우 응답할때 이렇게 서블릿은 html 코드를 그대로 자바 코드 중간에 넣어서 동적 페이지를 돌려줘야함
이번엔 JSP를 보자
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// request, response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("save.jsp");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
%>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>성공
<ul>
<li>id=<%=member.getId()%></li>
<li>username=<%=member.getUsername()%></li>
<li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
HTML속에서 자바코드는 <% 소스코드 %> 또는 <%= 소스코드 =%>형태로 들어간다. 이 부분은 서버에서 동작한다.
서블릿 규칙은 꽤나 복잡하기 때문에 JSP가 나오게 되었는데 JSP는 WAS(Web Application Server)에 의하여 서블릿 클래스로 변환하여 사용된다
서블릿과 JSP
서블릿
으로 개발할 때는 뷰(View)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여서 지저분하고 복잡했다.
JSP
를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 가져가고, 중간중간 동적으로 변경이 필요한 부분에만 자바 코드를 적용했다.
문제점
하지만 둘다 뷰와 핵심로직들이 뒤죽박죽
섞여서 유지보수가 아주 불편하다.
또한 뷰는 사용자가 보는화면인데 JSP는 핵심로직이 뷰에 노출
되어있다.
MVC 패턴의 등장
비즈니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일에 집중하도록 하자.
서블릿을 컨트롤러
로 사용하고, JSP를 뷰
로 사용해서 MVC 패턴을 구현해 보자
//MemberForm 컨트롤러
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
//..로직수행
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
//View.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
뷰와 컨트롤러가 분리되고 동적 페이지를 잘 생성하지만 문제가 있다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
뷰를 불러오거나 하는 로직이 모든 컨트롤러마다 중복된다.
요청이 오면
서블릿 컨테이너는 각 요청마다 알맞는 서블릿을 찾아 실행
할텐데 각 요청마다 모두 저런 코드들이 중복
된다는 것이다.
즉, 공통 처리가 어렵다. 기능이 복잡해질 수 록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가할 것이다. 단순히 공통 기능을 메서드로 뽑으면 될 것 같지만,
결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 것이다. 그리고 호출하는 것 자체도 중복이다.
스프링
은 이 문제를 프론트 컨트롤러(Front Controller) 패턴을 도입해서 해결했다.
스프링으로 서블릿을 다룬다는것
서블릿을 생성하고 필요한 순간에 호출하고 적절한 시점에 소멸시키는것을 서블릿 컨테이너에게 위임했다.
요청이 들어오면 서블릿 컨테이너는 각 url 요청에 맞는 서블릿을 찾아 실행한다.
그런데 여러 요청을 동시처리해야해서 멀티 쓰레드를 사용하는 경우를 살펴보자
서블릿은 쓰레드로 동작하며 컨트롤러라고 서블릿 특징에서 설명했다.
각 요청마다 쓰레드를 하나씩 생성하기엔 서버의 부담이 크다.
context switching 비용 및 하드웨어의 한계 등
또한 요청마다 처리되는 공통로직들이 매번 중복된다.
따라서 요청당 서블릿을 하나하나 지정해주는것은 매우 비효율적이다.
스프링은 이런 문제를 프론트 컨트롤러 패턴을 도입해서 해결했다.
Dispatcher Servlet의 등장
왼: 기존 방식 우: 프론트 컨트롤러 적용
기존의 방식이 요청마다 쓰래드를 생성하고 각각 서블릿을 정의하여 사용했다면 스프링은 디스패쳐 서블릿이라는 서블릿 하나만을 정의
해서 모든 로직을 처리
할수있게 한다.
프론트 컨트롤러 패턴
디스패쳐 서블릿은 일종의 프론트 컨트롤러이다.
각 클라이언트들은 Front Controller에 요청을 보내고, Front Controller은 각 요청에 맞는 컨트롤러를 찾아서 호출시킨다.
공통 코드에 대해서는 Front Controller에서 처리하고, 서로 다른 코드들만 각 Controller에서 처리할 수 있도록 한다.
디스패쳐 서블릿이 web 요청을 처리하는 과정
디스패쳐 서블릿은 프론트 컨트롤러로써 모든 요청을 받고 다양한 처리 인터페이스들과 상호작용하며 응답을 도출한다.
스프링은 이런 프론트 컨트롤러 패턴을 도입하여 설계되었다.
동작 순서
- 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
- 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
- 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
- ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
- viewResolver 호출: 뷰 리졸버를 찾고 실행한다. (view 이름으로부터 사용될 view 객체를 맵핑하는 역할) JSP의 경우: InternalResourceViewResolver 가 자동 등록되고, 사용된다.
- View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다. JSP의 경우 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있다.
- 뷰 렌더링: 반환된 뷰를 통해서 뷰를 렌더링 한다.
자칫보면 오히려 다뤄야하는 인터페이스들이 많아진것처럼 보이지만 핸들러(컨트롤러)를 제외한 나머지 뷰 리졸버, 핸들러 매핑 등은 스프링 컨테이너가 알아서 관리해주기때문에 개발자는 핸들러(컨트롤러)로직 개발에만 집중할수있다.
스프링 컨테이너
프로그램이 동작하는동안 사용되는 객체들을 프레임워크가 대신 관리하고 보관하기 위해 사용되는 컨테이너
@Commponent 즉 서비스,레파지토리,도메인 등의 객체 관리 및 다양한 스프링 빈 관리
개발에 필요한 부분을 주입
설정 파일만 잘 관리하면 스프링 컨테이너가 알아서 나머지 부분을 관리해준다.
Reference
https://www.youtube.com/watch?v=calGCwG_B4Y
https://coding-factory.tistory.com/742