주제 : 스프링의 MVC 기술들을 한번에 빠르게 훑고 간다.
목표 : 이전에 공부했던 스프링 웹 MVC를 복습하면서 전반적인 감을 찾는다!
웹 서버
- HTTP 기반으로 동작
- 정적 리소스 제공, 기타 부가기능
- 정적 HTML, CSS, JS, 이미지, 영상
- NGINX, APACHE
웹 애플리케이션 서버
HTTP 기반으로 동작
웹 서버 기능 포함 + (정적 리소스 제공 가능)
- 동적 HTML, HTTP API(JSON)
- 서블릿, JSP, 스프링 MVC
예) 톰캣, Jetty, Undertow
정리 : 웹 서버는 정적 리소스, WAS는 애플리케이션 코드를 실행하는데 더 특화
웹 시스템 구성 - WAS, DB
- WAS, DB 만으로 시스템 구성 가능
- WAS는 정적 리소스, 애플리케이션 로직 모두 제공 가능하다. 그러나, WAS가 너무 많은 역할을 담당하면 서버 과부하 우려가 있다. WAS 장애시 오류 화면도 노출 불가능하다.
따라서, 정적 리소스는 웹 서버가 처리하는 것이 좋다.
- 웹 서버는 애플리케이션 로직같은 동적인 처리가 필요하면 WAS에 요청을 위임하는 것이다.
- 이렇게 하면 효율적으로 리소스 관리가 가능하다.
- 정적 리소스가 많이 사용되면 Web 서버 증설하면 된다.
- 애플리케이션 리소스가 많이 사용되면 WAS를 증설한다.
- 정적 리소스만 제공하는 웹 서버는 잘 죽지 않는다.
- 반면, 애플리케이션 로직이 동작하는 WAS 서버는 잘 죽는다.
- WAS, DB 장애시 WEB 서버가 오류 화면을 제공 가능하기 때문에 클라이언트에게 오류화면 HTML을 보여주고 개발자는 빠르게 애플리케이션 로직을 수정하면 된다.
- 웹 애플리케이션 서버를 직접 구현해야 한다면, 엄청나게 많은 코드를 직접 작성해야 한다.
하지만, 서블릿을 지원하는 WAS를 사용한다면 의미있는 비즈니스 로직만을 만들면 된다.
서블릿을 지원하는 WAS는 의미있는 비즈니스 로직 이외의 모든 것을 다 실행시켜준다.
특징
@WebServlet(name = "helloServlet", urlPatterns = "/hello") public class HelloServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response){ //애플리케이션 로직 } }
- urlPatterns(/hello)의 URL이 호출되면 서블릿 코드가 실행된다.
- HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest
- HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse
- 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.
- 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리한다.
- 서블릿 객체는 싱글톤으로 관리해야 한다!
- 고객의 요청이 올 때마다 계속 객체를 생성하는 것은 비효율적이기 때문에 싱글톤으로 관리한다.
- 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용한다.
- 공유 변수를 사용하는 것은 주의해야 한다.
(private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.)- JSP도 서블릿으로 변환되어서 사용한다.
- 동시 요청을 위한 멀티 쓰레드 처리를 지원한다.(이게 가장 큰 특징!)
- 애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 쓰레드이다.
- 자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행된다.
- 쓰레드가 없다면 자바 애플리케이션 실행이 불가능하다.
- 쓰레드는 한번에 하나의 코드 라인만 수행한다.
- 동시 처리가 필요하면 쓰레드를 추가로 생성해야 한다. 하지만, 장단점이 존재한다
요청마다 쓰레드 생성(장단점)
- 장점
- 동시 요청을 처리할 수 있다.
- 리소스가 허용할 때까지 처리가 가능하다.
- 하나의 쓰레드가 지연 되어도, 나머지 쓰레드는 정상 동작한다.
- 단점
- 쓰레드는 생성 비용이 매우 비싸다.
- 쓰레드는 컨텍스트 스위칭 비용이 발생한다.
- 해결책 : 쓰레드 풀(미리 쓰레드를 만들어 놓는다.)
- 쓰레드가 미리 생성되어 있으므로, 쓰레드를 생성하고 종료하는 비용이 절약되고, 응답 시간이 빠르다.
- 생성 가능한 쓰레드의 최대치가 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.
- WAS의 주요 튜닝 포인트는 최대 쓰레드 수이다.
WAS의 멀티 쓰레드 지원(핵심)
- 멀티 쓰레드에 대한 부분은 WAS가 처리
- 개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 된다.
- 개발자는 마치 싱글 쓰레드 프로그래밍을 하듯이 편리하게 소스 코드 개발
- 다만, 멀티 쓰레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)은 주의해서 사용
- HTML 페이지
- 동적으로 필요한 HTML 파일을 생성해서 전달
- 웹 브라우저 : HTML 해석
- HTTP API
- HTML이 아니라 데이터를 전달
- 주로 JSON 형식 사용
- 다양한 시스템에서 호출
- 데이터만 주고 받음, UI 화면이 필요하면, 클라이언트가 별도 처리
- 앱, 웹 클라이언트, 서버 to 서버
- SSR - 서버 사이드 렌더링
- HTML 최종 결과를 서버에서 만들어서 웹 브라우저에 전달
- 주로 정적인 화면에 사용
- 관련기술 : JSP, 타임리프 -> 백엔드 개발자
- 스프링 부트 서블릿 환경 구성(
@ServletComponentScan)
- 스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록
@ServletComponentScan을 지원한다.@ServletComponentScan //서블릿 자동 등록 @SpringBootApplication public class ServletApplication { public static void main(String[] args) { SpringApplication.run(ServletApplication.class, args); } }
- 서블릿 등록하기
- 서블릿은 HttpServlet을 상속받는다.
- 쿼리 파라미터 조회
(localhost:8080/hello?username=kim이라고하면 request.getParameter로 조회가 가능)- response.getWriter().write 사용하면 http 메시지에 바디가 다 들어간다.
@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); }
서블릿 컨테이너 동작 방식 설명
내장 톰캣 서버 생성
웹 애플리케이션 서버의 요청 응답 구조
HttpServletRequest 역할 : HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 매우 불편하다. 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를
HttpServletRequest객체에 담아서 제공한다.HTTP 요청 데이터 - 개요
HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보자.주로 다음 3가지 방법을 사용한다.
- GET - 쿼리 파라미터
- /url?username=hello&age=20
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
- 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
- POST - HTML Form
- 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
- 예) 회원 가입, 상품 주문, HTML Form 사용
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH
HTTP 요청 데이터 - API 메시지 바디 - JSON(3번째 방식)
이번에는 HTTP API에서 주로 사용하는 JSON 형식으로 데이터를 전달해보자.JSON 형식 전송
- POST http://localhost:8080/request-body-json
- content-type: application/json
- message body: {"username": "hello", "age": 20}
- 결과: messageBody = {"username": "hello", "age": 20}
JSON 형식 파싱 추가
- JSON 형식으로 파싱할 수 있게 객체를 하나 생성(롬복이 Getter,Setter 처리해줌)
@Getter @Setter public class HelloData { private String username; private int age; }@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 { 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()); response.getWriter().write("ok"); } }출력 결과
messageBody={"username": "hello", "age": 20}
data.username=hello
data.age=20
HTTP 응답 데이터
HTTP 응답 메시지는 주로 다음 내용을 담아서 전달(요청 데이터의 3가지와 비슷)
- 단순 텍스트 응답
- 앞에서 살펴봄 ( writer.println("ok"); )
- HTML 응답
- HTTP API - MessageBody JSON 응답
- HTTP 응답 데이터 - API JSON 코드(3번째 방식)
- HTTP 응답으로 JSON을 반환할 때는 content-type을 application/json 로 지정해야 한다. Jackson 라이브러리가 제공하는 objectMapper.writeValueAsString() 를 사용하면 객체를 JSON 문자로 변경할 수 있다.
@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json") public class ResponseJsonServlet extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //Content-Type: application/json response.setHeader("content-type", "application/json"); response.setCharacterEncoding("utf-8"); HelloData data = new HelloData(); data.setUsername("kim"); data.setAge(20); //{"username":"kim","age":20} String result = objectMapper.writeValueAsString(data); response.getWriter().write(result); } }
서블릿으로 개발할 때는 뷰화면을 위한 HTML을 만든는 작업이 자바 코드에 섞여서 지저분하고 복잡했다. 하지만, JSP를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 가져가고, 중간중간 동적으로 변경이 필요한 부분에만 자바 코드를 적용했다. 그런데 이렇게 해도 해결되지 않는 몇 가지 고민이 남는다. 회원 저장 JSP는 코드의 상위 절반은 회원을 저장하기 위한 비즈니스 로직이고, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역이다. 회원 목록의 경우에도 마찬가지다.
MVC 패턴의 등장
비즈니스 로직은 서블릿처럼 다른 곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면을 그리는 일에 집중할 수 있도록 MVC패턴이라는 것이 등장하였다.MVC 패턴(Model View Controller) - 개요
MVC 패턴은 지금까지 학습한 것처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
- 컨트롤러 : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달한 결과 데이터를 조회해서 모델에 담는다.
- 모델 : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
- 뷰 : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용해보자. Model은 HttpServletRequest 객체를 사용한다. request 내부에 데이터 저장소를 가지고 있는데, request.setAttribute(), request.getAttribute()를 사용하면 데이터를 보관하고 조회할 수 있다.
회원 등록
- 1) 회원 등록 폼 - 컨트롤러
hello.servlet.web.servletmvc.MvcMemberFormServlet@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); } }
dispathcer.forward(): 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다./WEB-INF: 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 우리가 기대하는 것은 항상 컨트롤러를 통해서 JSP를 호출하는 것이다.
- 2) 회원 등록 폼 - 뷰
<%@ 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>
- http://localhost:8080/servlet-mvc/members/new-form
- HTML Form이 잘 나오는 것을 확인할 수 있다.
회원 저장
- 1) 회원 저장 - 컨트롤러
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") public class MvcMemberSaveServlet extends HttpServlet { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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); //Model 에 데이터를 보관한다. request.setAttribute("member", member); String viewPath = "/WEB-INF/views/save-result.jsp"; RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } }
- HttpServletRequest를 Model로 사용한다.
- request가 제공하는
setAttribute()를 사용하면 request 객체에 데이터를 보관해서 뷰에 전달할 수 있다. 뷰는request.getAttribute()를 사용해서 데이터를 꺼내면 된다.
- 2) 회원 저장 - 뷰
main/webapp/WEB-INF/views/save-result.jsp<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <meta charset="UTF-8"> </head> <body> 성공 <ul> <li>id=${member.id}</li> <li>username=${member.username}</li> <li>age=${member.age}</li> </ul> <a href="/index.html">메인</a> </body> </html>
- <%= request.getAttribute("member")%> 로 모델에 저장한 member 객체를 꺼낼 수 있지만, 너무 복잡해진다.
- JSP는 ${ } 문법을 제공하는데, 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다
회원 목록 조회
1) 회원 목록 조회 - 컨트롤러
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members") public class MvcMemberListServlet extends HttpServlet { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("MvcMemberListServlet.service"); List<Member> members = memberRepository.findAll(); request.setAttribute("members", members); String viewPath = "/WEB-INF/views/members.jsp"; RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } }
- request 객체를 사용해서
List<Member> members를 모델에 보관했다.
2) 회원 목록 조회 - 뷰
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="save" method="post"> username: <input type="text" name="username" /> age: <input type="text" name="age" /> <button type="submit">전송</button> </form> </body> </html>
- 모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해서 반복하면서 출력했다.
- members 리스트에서 member 를 순서대로 꺼내서 item 변수에 담고, 출력하는 과정을 반복한다.
- 실행
- http://localhost:8080/servlet-mvc/members
- 저장된 결과 목록을 확인할 수 있다.
MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다. 특히, 뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다. 그런데 컨트롤러는 딱 봐도 중복이 많고, 필요하지 않는 코드들도 많이 보인다.
- 이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 프론트 컨트롤러 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다. 스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다.