Search

스프링 MVC 1편

단축키

CONTROL + O : 재정의/구현할 메서드 선택

어노테이션

@ServletComponentScan : 서블릿을 자동으로 등록해서 사용할 수 있도록 해주는 기능
@WebServlet : 서블릿이라고 알려주는 어노테이션
name : 서블릿 이름 지정
urlPatterns : URL매핑

자바 웹 기술 역사

Servlet (1997)
TCP/IP 연결, 멀티스레드 고민 등으로 인한 HTML 생성의 어려움을 해결하기 위해 생긴 기술
JSP (1999)
HTML 생성은 편리하지만 비즈니스 로직까지 너무 많은 역할을 담당한다.
Servlet + JSP = MVC
Model, View, Controller 역할을 나누어 개발 (비즈니스 로직과 화면 렌더링의 역할을 나눔)
MVC Framework 춘추전국 시대
MVC 패턴 자동화, 복잡한 웹 기술을 편리하게 사용할 수 있는 다양한 기능 지원
스트럿츠, 웹워크, 스프링MVC
Spring MVC based @Annotation
@어노테이션으로 유연하고 편리하고 깔끔하게 MVC 코드를 작성할 수 있게 해줌
Spring Boot
스프링 부트는 서버를 내장한다.
과거엔 서버에 WAS를 직접 설치하고 WAR 파일을 만들어 WAS에 배포해야 했지만
스프링 부트는 JAR에 WAS 서버 포함하고 JAR를 실행하면 서버 실행 (빌드 배포 단순화)
Spring 웹 기술의 분화
Web Servlet - Spring MVC (기존)
Web Reactive - Spring WebFlux
비동기 논블로킹 처리
최소 스레드로 최대 성능 (스레드를 CPU 코어 개수에 맞춰 컨텍스트 스위칭 비용 효율화)
함수형 스타일 개발 (동시처리 코드 효율화)
서블릿 기술 사용 X
기술적 난이도가 높음, RDB 지원 부족, 아직은 일반MVC도 빠르다, 실무에서 잘 사용하지 않음

자바 뷰 템플릿 역사

JSP
속도 느림, 기능 부족
프리마커(Freemarker), 벨로시티(Velocity)
속도 문제 해결, 다양한 기능
하지만 발전이 더딤
Thymeleaf
네추럴 템플릿 : HTML의 모양을 유지하면서 뷰 템플릿 적용 가능
스프링MVC와 강력한 기능 통합
성능은 프리마커, 벨로시티가 더 빠르지만 최선의 선택이다.

HTTP

HTML, TEXT, IMAGE, VIDEO, API(JSON, XML)
거의 모든 형태의 데이터를 전송 가능 (서버 간 데이터를 주고 받을 때도 HTTP 사용)

정적 리소스

고정된 HTML 파일, CSS, JS, IMAGE, VIDEO 등
주로 웹 브라우저에서 요청

HTML 페이지

동적으로 필요한 HTML 파일을 생성해서 전달
웹 브라우저는 전달받은 HTML 파일을 해석한다.

HTTP API

HTML이 아니라 주로 JSON형식의 데이터를 전달한다.
다양한 시스템에서 호출 (앱 클라이언트, 웹 클라이언트, 서버 → 서버)
데이터만 주고 받고 UI 화면이 필요하면 클라이언트가 별도 처리한다.
클라이언트 → 서버
앱 : 아이폰, 안드로이드, PC 앱
웹 : 웹 브라우저에서 자바스크립트를 통한 HTTP API 호출 (React, Vue.js 같은 웹 클라이언트)
서버 → 서버
주문 서버 → 결제 서버
기업 간 데이터 통신

웹 서버 (WS, Web Server)

HTTP 기반으로 동작
정적 리소스(HTML, CSS, JS, IMAGE, VIDEO) 제공, 기타 부가 기능
ex) NGINX, APACHE

웹 애플리케이션 서버 (WAS, Web Application Server)

HTTP 기반으로 동작
웹 서버 기능 포함 (정적 리소스 제공 가능)
프로그램 코드를 실행해서 애플리케이션 로직 수행
동적 HTML, HTTP API(JSON)
Servlet, JSP, Spring MVC
ex) Tomcat, Jetty, Undertow

WS vs WAS 차이

WS는 정적 리소스(파일), WAS는 애플리케이션 로직
사실은 애매하다
WS도 프로그램을 실행하는 기능을 포함하기도 함
WAS도 WS의 기능을 제공함
자바는 서블릿 컨테이너를 제공하면 WAS에 포함된다
WAS는 애플리케이션 코드를 실행하는데 더 특화돼있다.

웹 시스템 구성

WAS, DB만으로 시스템을 구성할 수 있다 (정적 리소스, 애플리케이션 로직 모두 제공)
하지만 WAS가 너무 많은 역할을 담당하면 서버가 과부하 될 우려가 있다.
가장 비싼 애플리케이션 로직이 정적 리소스 때문에 수행이 어려울 수 있음
WAS 장애 시 에러 페이지 노출 불가능
이상적인 시스템 구성
정적 리소스는 WS가 처리, 동적인 처리가 필요하면 WAS에 요청을 위임
WAS는 애플리케이션 로직 처리 전담
이렇게 구성하게 되면 리소스를 효율적으로 관리할 수 있게 된다.
정적 리소스가 많이 사용된다면 WS 증설, 애플리케이션 리소스가 많이 사용된다면 WAS 증설
정적 리소스만 제공하는 WS는 잘 죽지 않지만 WAS는 잘 죽는다.
WAS, DB 장애 시 WS가 에러 페이지를 제공

서블릿(Servlet)

HTTP 요청 시 비즈니스 로직을 제외한 나머지 과정들은 서블릿이 자동으로 해준다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello") public class HelloServlet extends HttpServlet { @Override protected void service(HttpServletReqeust request, HttpServletResponse response) { //애플리케이션 로직 } }
Java
복사
urlPatterns(/hello)의 URL이 호출되면 서블릿 코드가 실행된다.
HttpServletRequest : HTTP 요청 정보를 편리하게 사용할 수 있게 해준다.
HttpServletResponse : HTTP 응답 정보를 편리하게 제공할 수 있게 해준다.
이걸 통해 개발자는 HTTP Spec을 편리하게 사용한다.
Request, Response 객체는 요청마다 만들어진다.
1.
WAS가 HTTP 요청 메시지를 기반으로 Request, Response 객체를 새로 만들어서 서블릿 객체 호출
2.
개발자는 파라미터로 전달된 Request 객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용
3.
개발자는 파라미터로 전달된 Response 객체에 HTTP 응답 정보를 편리하게 입력
4.
WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성해서 웹 브라우저에 전송
5.
웹 브라우저는 HTTP 응답 정보를 이용해 렌더링하여 웹 페이지 표시
톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다
서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리한다
서블릿 객체는 싱글톤으로 관리한다
JSP도 서블릿으로 변환 되어서 사용
동시 요청을 위한 멀티스레드 처리 지원

스레드

스레드는 애플리케이션 코드를 한 번에 하나의 코드 라인씩 순차적으로 실행한다.
자바는 처음 실행 시 main이라는 스레드가 실행되고 스레드가 없을 시 애플리케이션 실행이 불가능하다.
동시 처리가 필요하면 스레드를 추가로 생성해야 한다.
장점
1.
동시 요청을 리소스(CPU, 메모리)가 허용할 때까지 처리할 수 있다.
2.
하나의 스레드가 지연되어도 나머지 스레드는 정상 작동한다.
단점
1.
스레드 생성 비용이 매우 비싸고 컨텍스트 스위치 비용이 발생한다.
2.
스레드 생성에 제한이 없어 고객 요청이 올 때마다 생성하면 응답 속도가 늦어지고 CPU, 메모리 임계점을 넘어서 서버가 죽을 수 있다.

스레드 풀

스레드 풀은 max thread만큼 스레드를 미리 생성해 보관하고 관리한다.
max thread가 너무 낮으면 동시 요청이 많을 때 리소스는 여유롭지만 응답이 지연된다.
max thread가 너무 높으면 동시 요청이 많을 때 CPU, 메모리 리소스 임계점 초과로 서버가 죽는다.
장애 발생 시 클라우드라면 일단 서버를 늘린 후 튜닝하고 클라우드가 아니라면 열심히 튜닝한다.
스레드 풀에서 꺼내 사용하고 반환하므로 비용이 절약되고 응답 시간이 빠르다.
max thread가 정해져 있어 요청이 많아도 기존 요청은 안전하게 처리할 수 있다.
WAS가 대부분 처리하므로 개발자는 멀티 스레드 코드를 신경쓰지 않아도 된다.
개발자는 싱글 스레드 프로그래밍을 하듯이 편리하게 소스 코드를 개발해도 된다.
싱글톤 객체, 공유 변수는 주의해서 사용한다.

SSR (Server Side Rendering)

HTML 최종 결과를 서버에서 만들어서 웹 브라우저에 전달한다.
주로 정적인 화면에 사용한다.
JSP, thymeleaf → 백엔드

CSR (Client Side Rendering)

HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해서 적용한다.
주로 동적인 화면에 사용, 웹 환경을 마치 앱처럼 필요한 부분부분 변경할 수 있음
React, Vue.js → 프론트엔드

Hello Servlet

스프링 부트가 내장 톰캣 서버를 실행
톰캣 서버는 서블릿 컨테이너 기능을 갖고 있으므로 서블릿들을 스캔해 생성
//DemoApplication @ServletComponentScan @SpringBootApplication public class Demo3Application { public static void main(String[] args) { SpringApplication.run(Demo3Application.class, args); } }
Java
복사
// HelloServlet @WebServlet(name = "helloServlet", urlPatterns = "/hello") public class HelloServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException { 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); } }
Java
복사
/hello?username=world 를 통해 HTTP 요청
톰캣이 HTTP 요청에 대한 request, response 객체를 만들어서 helloServlet의 service() 실행
필요한 작업을 처리한다 (response에 setContentType, setCharacterEncoding 등)
톰캣이 response 객체를 HTTP 응답으로 웹 브라우저에게 반환

HttpServletRequest

개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 서블릿이 대신 파싱해서 제공해주는 객체이다.
HTTP 요청 메시지 조회 기능
POST /save HTTP/1.1 Host: localhost:8080 content-Type: application/x-www-form-urlencoded username=kim&age=20
Java
복사
START LINE
Method : POST
URL : http://localhost:8080/save
URI : /save
Scheme : http
Protocal : HTTP/1.1
QueryString : null (? 뒤에 문자열)
Secure : false (https 인지 아닌지)
Header
Host, Content, Accept-Language, Cookie 등
Body
username=kim&age=20
form 파라미터 형식 (request.getParameter…)
message body 데이터 형식
임시 저장소 기능
해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
저장 : request.setAttribute(name, value)
조회 : request.getAttribute(name)
세션 관리 기능
request.getSession(create: true)

HTTP 요청 데이터

HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법
GET - 쿼리 파라미터
/url?username=hello&age=20
메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
검색, 필터, 페이징 등에서 사용하는 방식
POST - HTML Form
content-type: application/x-www-form-urlencoded
메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
회원가입, 상품주문, HTML Form 등에서 사용
HTTP message body
HTTP API에서 주로 사용, JSON, XML, TEXT
데이터 형식은 주로 JSON 사용
POST, PUT, PATCH

HTTP 요청 데이터 - GET 쿼리 파라미터

GET http://localhost:8080/request-param?username=hello&age=20 content-type : null message body : null
HTML
복사
쿼리 파라미터는 URL에 ‘?’ 을 시작으로 보낼 수 있고 추가 파라미터는 ‘&’ 으로 구분한다.
HttpServletRequest가 제공하는 getParameter()를 통해 쿼리 파라미터를 편리하게 조회할 수 있다.
복수 파라미터인 경우 단일 파라미터 조회 시 먼저 조회되는 파라미터의 값이 출력된다.
GET 쿼리 파라미터 방식은 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없다
String username = request.getParameter("username"); //단일 파라미터 조회 Enumeration<String> parameterNames = request.getParameterNames(); //파라미터 이름들 모두 조회 Map<String, String[]> parameterMap = request.getParameterMap(); //파라미터를 Map 으로 조회 String[] usernames = request.getParameterValues("username"); //복수 파라미터 조회
Java
복사

HTTP 요청 데이터 - POST HTML Form

POST http://localhost:8080/request-param content-type : application/x-www-form-urlencoded message body : username=hello&age=20
HTML
복사
HTML 태그 중 <form> 태그를 사용하여 POST HTML Form 형식으로 데이터를 보낼 수 있다.
action을 통해 URL 매핑을 통해 호출할 서블릿을 지정할 수 있다.
method를 통해 HTTP 메서드를 지정할 수 있다.
content-type은 application/x-www-form-urlencoded 로 message body로 데이터를 보낸다
getParameter() 와 InputStream 두 방식으로 모두 읽을 수 있다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Title</title> </head> <body> <form action="/request-param" method="post"> username : <input type="text" name="username"> age : <input type="text" name="age"> <button type="submit">전송</button> </form> </body> </html>
HTML
복사

HTTP 요청 데이터 - API (TEXT)

POST http://localhost:8080/request-body-string content-type : text/plain message body : hello
HTML
복사
message body에 데이터를 직접 담아서 요청한다.
요청 정보는 InputStream을 통해서 읽을 수 있다. (byte코드를 반환하므로 Charset 지정해줘야함)
@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"); } }
Java
복사

HTTP 요청 데이터 - API (JSON)

POST http://localhost:8080/request-body-json content-type : application/json message body : {"username": "hello","age": 20}
HTML
복사
message body를 통해 JSON 형식으로 데이터를 담아서 전달한다.
요청 정보는 InputStream을 통해서 읽을 수 있다. (byte코드를 반환하므로 Charset 지정해줘야함)
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json") public class RequestBodyJsonServlet extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletInputStream inputStream = req.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.getUsername() = " + helloData.getUsername()); System.out.println("helloData.getAge() = " + helloData.getAge()); resp.getWriter().write("ok"); } }
Java
복사
jackson 라이브러리(ObjectMapper)를 사용하면 JSON 형식을 파싱하여 객체를 생성할 수 있다.
(파싱된 데이터를 담을 객체를 만들고 getter, setter를 미리 만들어줘야함)
//HelloData @Getter @Setter public class HelloData { private String username; private int age; } //RequestBodyJsonServlet 중 private ObjectMapper objectMapper = new ObjectMapper(); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); System.out.println("helloData.getUsername() = " + helloData.getUsername()); System.out.println("helloData.getAge() = " + helloData.getAge());
Java
복사

HttpServletResponse

HTTP 응답 메시지 생성 (응답코드, 헤더, 바디)
편의 기능 제공 (Content-Type, Cookie, Redirect)
@WebServlet(name ="responseHeaderServlet", urlPatterns = "/response-header") public class ResponseHeaderServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //[status-line] resp.setStatus(HttpServletResponse.SC_OK); //[response-header] resp.setHeader("Content-Type", "text/plain;charset=utf-8"); resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); resp.setHeader("Pragma", "no-cache"); resp.setHeader("my-header", "hello"); //[message body] PrintWriter writer = resp.getWriter(); writer.println("ok"); //[response 편의 기능] content(resp); cookie(resp); redirect(resp); } private void content(HttpServletResponse response) { // response.setHeader("Content-Type", "text/plain;charset=utf-8"); response.setContentType("text/plain"); response.setCharacterEncoding("utf-8"); response.setContentLength(2); } private void cookie(HttpServletResponse response) { // response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600"); Cookie cookie = new Cookie("myCookie", "good"); cookie.setMaxAge(600); response.addCookie(cookie); } private void redirect(HttpServletResponse response) throws IOException { // response.setStatus(HttpServletResponse.SC_FOUND); // response.setHeader("Location", "/basic/hello-form.html"); response.sendRedirect("/basic/hello-form.html"); } }
Java
복사

HTTP 응답 데이터

HTML을 반환할 때는 Content-Type을 text/html로 지정해야 한다.
response.getWriter()로 PrintWriter를 받아와서 출력한다.
@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html") public class ResponseHtmlServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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>"); } }
Java
복사
JSON을 반환할 때는 Content-Type을 application/json으로 지정해야 한다.
objectMapper.writeValueAsString()으로 HelloData를 받아와서 response.getWriter()로 출력한다.
@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json") public class ResponseJsonServlet extends HttpServlet { ObjectMapper objectMapper = new ObjectMapper(); @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json"); resp.setCharacterEncoding("utf-8"); HelloData helloData = new HelloData(); helloData.setUsername("kim"); helloData.setAge(20); String s = objectMapper.writeValueAsString(helloData); resp.getWriter().write(s); } }
Java
복사

JSP

Servlet + JAVA
HTML을 직접 JAVA 코드 내에 그대로 작성해야 하므로 굉장히 번거롭다..
response 응답으로 HTML을 보내기 때문에 Content-Type, CharacterEncoding 설정해야함
@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members") public class MemberListServlet extends HttpServlet { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Member> members = memberRepository.findAll(); response.setContentType("text/html"); response.setCharacterEncoding("utf-8"); PrintWriter w = response.getWriter(); w.write("<html>"); w.write("<head>"); w.write(" <meta charset=\"UTF-8\">"); w.write(" <title>Title</title>"); w.write("</head>"); w.write("<body>"); w.write("<a href=\"/index.html\">메인</a>"); w.write("<table>"); w.write(" <thead>"); w.write(" <th>id</th>"); w.write(" <th>username</th>"); w.write(" <th>age</th>"); w.write(" </thead>"); w.write(" <tbody>"); for (Member member : members) { w.write(" <tr>"); w.write(" <td>" + member.getId() + "</td>"); w.write(" <td>" + member.getUsername() + "</td>"); w.write(" <td>" + member.getAge() + "</td>"); w.write(" </tr>"); } w.write(" </tbody>"); w.write("</table>"); w.write("</body>"); w.write("</html>"); } }
Java
복사
템플릿 엔진 (JSP)
HTML 중 필요한 부분만 동적으로 코드를 이용해 변경할 수 있다.
HTML 내에서 메인 로직은 자바 코드를 그대로 사용한다.
<%@ page ~ %> : 선언문
<% ~ %> : 자바 코드(로직)
<%= ~ %> : 자바 코드(로직) 바로 출력
<%@ page contentType="text/html;charset=UTF-8" language="java" %>를 선언해야함
<%@ page import="com.example.demo3.domain.member.MemberRepository" %> <%@ page import="com.example.demo3.domain.member.Member" %> <%@ page import="java.util.List" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% MemberRepository memberRepository = MemberRepository.getInstance(); List<Member> members = memberRepository.findAll(); %> <html> <head> <title>Title</title> </head> <body> <a href="/index.html">메인</a> <table> <thead> <th>id</th> <th>username</th> <th>age</th> </thead> <tbody> <% for (Member member : members) { out.write(" <tr>"); out.write(" <td>" + member.getId() + "</td>"); out.write(" <td>" + member.getUsername() + "</td>"); out.write(" <td>" + member.getAge() + "</td>"); out.write(" </tr>"); } %> </tbody> </table> </body> </html>
Java
복사
MVC 패턴
서블릿과 JSP는 자바 코드와 HTML이 섞여있다는 한계가 있다.
서블릿이 처리하던 역할을 Controller로, JSP가 처리하던 역할을 View로 나눈 MVC 패턴 등장
Controller : HTTP 요청을 받아서 파라미터를 검증하고 비즈니스 로직을 실행한다.
그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
Model : 뷰에 출력할 데이터를 담아둔다.
View : 모델에 담겨있는 데이터를 사용해서 HTML을 생성하는 일에 집중한다.
Controller에 비즈니스 로직을 둘 수 있지만 이렇게 되면 Controller의 역할이 너무 많아진다.
그래서 비즈니스 로직을 Service로 분리하고 Controller는 Service를 호출한다.
Service는 비즈니스 로직을 처리하고 Repository는 DB에 접근해서 데이터를 가져온다.

MVC 패턴

Controller, View, Model 적용
Controller는 서블릿, View는 JSP, Model은 HttpRequest 객체를 사용한다.
request.get/setAttribute()를 사용해 내부 데이터 저장소에 저장, 조회한다.
dispatcher.forward() 는 다른 서블릿이나 JSP로 이동하게 하는 기능이다. (서버 내 재호출)
/WEB-IFN 이곳에 있으면 컨트롤 없이 JSP를 직접 호출할 수 없다.
JSTL의 문법을 사용하려면 taglib부분을 선언해야 한다.
${~} : Request 요청에 담긴 데이터를 편리하게 조회할 수 있다.
<c: forEach var=”” items=”${””}”>
//MvcMemberListServlet.java @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 { 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); } }
Java
복사
//members.jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <head> <title>Title</title> </head> <body> <a href="/index.html">메인</a> <table> <thead> <th>id</th> <th>username</th> <th>age</th> </thead> <tbody> <c:forEach var="item" items="${members}"> <tr> <td>id = ${itme.id}</td> <td>username = ${itme.username}</td> <td>age = ${item.age}</td> </tr> </c:forEach> </tbody> </table> </body> </html>
HTML
복사
Controller, View, Model의 한계
Controller의 단점 → 공통 처리가 어렵다!
dispatcher.forward()의 중복
viewPath의 중복 (prefix : /WEB-INF/views , suffix : .jsp)
잘 사용하지 않는 HttpServletRequest, HttpServletResponse
→ Front Controller 패턴을 도입하면 해결할 수 있다.

Front Controller

Front Controller가 없을 땐 각 Controller마다 공통된 로직이 존재했다.
viewPath, dispatcher.forward()의 중복 등
FrontControllerServlet 를 거치게 함으로써 공통 로직을 처리하고 알맞은 Controller를 호출한다.
FrontControllerServlet을 제외한 나머지 Controller는 Servlet으로 만들 필요가 없다.
Spring MVC의 DispatcherServlet도 FrontController 패턴으로 구현되어 있다.

V1 - FrontController 도입

//ControllerV1 인터페이스 public interface ControllerV1 { void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; }
Java
복사
각 Controller들로 다형성을 활용하기 위해 인터페이스로 로직의 일관성을 준다.
//ControllerV1 인터페이스를 구현한 MemberListControllerV1 public class MemberListControllerV1 implements ControllerV1 { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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); } }
Java
복사
ControllerV1 인터페이스의 process() 추상메서드를 기존 Controller의 service()로 그대로 구현한다.
이 때 HttpServlet을 상속받지 않았으므로 Servlet이 아니다.
//FrontControllerServletV1 @WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") public class FrontControllerServletV1 extends HttpServlet { private Map<String, ControllerV1> controllerMap = new HashMap<>(); public FrontControllerServletV1() { controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1()); controllerMap.put("/front-controller/v1/members", new MemberListControllerV1()); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("FrontControllerServletV1.service"); String requestURI = request.getRequestURI(); ControllerV1 controller = controllerMap.get(requestURI); if(controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } else { controller.process(request, response); } } }
Java
복사
HttpServlet을 상속받는 유일한 Servlet이다.
urlPatterns = “/front-controller/v1/*” 을 통해 하위 모든 요청을 받아들인다.
Map을 통해 호출할 Controller들을 매핑한다.
service()를 실행하면 URI를 통해 호출할 Controller를 찾아 해당 Controller의 process()를 호출
→ 모든 Controller에서 View로 이동하는 부분에 중복이 있다.
String viewPath = "/WEB-INF/views/members.jsp"; RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response);
Java
복사

V2 - View 분리

//MyView public class MyView { private String viewPath; public MyView(String viewPath) { this.viewPath = viewPath; } public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } }
Java
복사
Controller의 forward 코드 중복을 MyView 하나로 관리한다.
//MemberSaveControllerV2 public class MemberSaveControllerV2 implements ControllerV2 { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override public MyView process(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); 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); return new MyView("/WEB-INF/views/save-result.jsp"); } }
Java
복사
이제 Controller는 반환할 JSP 주소를 통해 MyView를 생성해 반환하기만 하면 된다.
이로서 중복 코드를 줄이고 비즈니스 로직에만 집중할 수 있게 된다.
//FrontControllerServletV2 @WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*") public class FrontControllerServletV2 extends HttpServlet { private Map<String, ControllerV2> controllerMap = new HashMap<>(); public FrontControllerServletV2() { controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2()); controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2()); controllerMap.put("/front-controller/v2/members", new MemberListControllerV2()); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("FrontControllerServletV2.service"); String requestURI = request.getRequestURI(); ControllerV2 controller = controllerMap.get(requestURI); if(controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } else { //controller.process(request, response) MyView view = controller.process(request, response); view.render(request, response); } } }
Java
복사
기존 Controller에서 처리하던 forward를 받아왔기 때문에
반환 받은 MyView 객체를 통해 render()를 호출함으로써 forward를 할 수 있게 된다.
→ FrontController, Controller, MyView 모든 곳에 HttpServletRequest/Response 가 붙어있다..
이러한 서블릿 종속성을 제거하기 위해 request 객체를 Model로 대신 사용할 수 있다.
→ 뷰의 이름 중 prefix, suffix 가 중복된다..
공통된 경로를 제거한 논리 이름만을 반환해 FrontController에서 처리하도록 단순화할 수 있다.

V3 - Model 추가

//FrontControllerServletV3 @WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*") public class FrontControllerServletV3 extends HttpServlet { private Map<String, ControllerV3> controllerMap = new HashMap<>(); public FrontControllerServletV3() { controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3()); controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3()); controllerMap.put("/front-controller/v3/members", new MemberListControllerV3()); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("FrontControllerServletV3.service"); String requestURI = request.getRequestURI(); ControllerV3 controller = controllerMap.get(requestURI); if(controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } Map<String, String> paramMap = createParamMap(request); ModelView mv = controller.process(paramMap); String viewName = mv.getViewName(); MyView view = viewResolver(viewName); view.render(mv.getModel(), request, response); } private MyView viewResolver(String viewName) { return new MyView("/WEB-INF/views/" + viewName + ".jsp"); } private Map<String, String> createParamMap(HttpServletRequest request) { Map<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
HttpServletRequest/Response의 중복을 제거하기 위해 ParamMap을 만들었다.
request에 담겨온 parameter 정보를 Map 형태로 뽑아낸 것이다.
이제 Controller에게 ParamMap만 전달하고 Controller는 뽑아서 쓰기만 하면 된다.
절대적 경로를 논리적 경로로 변환하기 위해 ViewResolver를 추가했다.
단순히 전달 받은 논리적 경로에 prefix, suffix를 붙여서 반환하기만 한다.
//MemberSaveControllerV3 public class MemberSaveControllerV3 implements ControllerV3 { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override public ModelView process(Map<String, String> paramMap) { String username = paramMap.get("username"); int age = Integer.parseInt(paramMap.get("age")); Member member = new Member(username, age); memberRepository.save(member); ModelView mv = new ModelView("save-result"); mv.getModel().put("member", member); return mv; } }
Java
복사
FrontController에게 전달받은 paramMap에서 username, age를 뽑아 사용하는걸 확인할 수 있다.
또한 기존에 request.setAttribute와 MyView를 사용해서 데이터와 절대적 경로를 전달하던 방식을
ModelView를 이용해 데이터객체 자체와 논리적 경로를 담아 전달하는 방식으로 바꿨다
//ModelView @Getter @Setter public class ModelView { private String viewName; private Map<String, Object> model = new HashMap<>(); public ModelView(String viewName) { this.viewName = viewName; } }
Java
복사
viewName(논리적 경로)로 객체를 생성한 후 getter, setter를 이용해 model(데이터)를 담는다
//MyView public class MyView { private String viewPath; public MyView(String viewPath) { this.viewPath = viewPath; } public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { modelToRequestAttribute(model, request); RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) { model.forEach((key, value) -> request.setAttribute(key, value)); } }
Java
복사
최종적으로 render()를 통해 Forward하게 되는데 이 때 model의 데이터를 request 객체로 다 옮겨준다
(JSP는 request 객체의 attribute의 데이터를 조회하기 때문)
→ Controller에서 항상 ModelView를 생성하고 반환하는 부분이 번거롭다!!
FrontController에서 Model을 만들어 갖고 있다가 Controller 호출할 때 파라미터로 전달해주고
Controller는 전달 받은 model에 필요한 데이터를 담기만 한 후 viewName만 반환해주면 간단해진다.

V4 - 단순하고 실용적인 컨트롤러

//MemberSaveControllerV4 public class MemberSaveControllerV4 implements ControllerV4 { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override public String process(Map<String, String> paramMap, Map<String, Object> model) { String username = paramMap.get("username"); int age = Integer.parseInt(paramMap.get("age")); Member member = new Member(username, age); memberRepository.save(member); model.put("member", member); return "save-result"; } }
Java
복사
기존엔 ModelView 객체를 직접 생성해 model에 데이터를 추가한 후 ModelView를 반환하는 방식을
FrontController에서 생성한 model을 파라미터로 전달받아 데이터를 추가한 후 viewName만을 반환한다.
//FrontControllerServletV4 @WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*") public class FrontControllerServletV4 extends HttpServlet { private Map<String, ControllerV4> controllerMap = new HashMap<>(); public FrontControllerServletV4() { controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4()); controllerMap.put("/front-controller/v4/members", new MemberListControllerV4()); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String requestURI = request.getRequestURI(); ControllerV4 controller = controllerMap.get(requestURI); if(controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } Map<String, String> paramMap = createParamMap(request); //model을 직접 생성한 후 controller를 호출한다. Map<String, Object> model = new HashMap<>(); String viewName = controller.process(paramMap, model); MyView view = viewResolver(viewName); //물론 render()를 호출할 때도 mv.getModel()을 전달하는게 아니라 model을 직접 전달한다. view.render(model, request, response); } private MyView viewResolver(String viewName) { return new MyView("/WEB-INF/views/" + viewName + ".jsp"); } private Map<String, String> createParamMap(HttpServletRequest request) { Map<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
→ Controller의 방식을 V1~V4의 여러 가지 방식으로 개발하고 싶다!
하지만 현재까지는 이러한 유연한 방식의 개발은 불가능하다

V5 - 유연한 컨트롤러

컨트롤러를 더 넓은 범위의 이름은 핸들러로 변경한다.
이러한 핸들러의 종류가 다양하더라도 핸들러 어댑터를 통해 호출할 수 있게 해준다.
핸들러 어댑터는 어댑터의 역할만 하기 때문에 FrontController는 핸들러 어댑터 목록을 통해 원하는 핸들러를 처리할 수 있는 핸들러 어댑터를 찾는다.
//FrontControllerServletV5 @WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*") public class FrontControllerServletV5 extends HttpServlet { //handlerMappingMap에 컨트롤러 리스트, handlerAdapters에 어댑터 리스트를 추가한다. private final Map<String, Object> handlerMappingMap = new HashMap<>(); private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>(); public FrontControllerServletV5() { initHandlerMappingMap(); initHandlerAdapters(); } private void initHandlerMappingMap() { handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4()); handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4()); handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4()); } private void initHandlerAdapters() { handlerAdapters.add(new ControllerV3HandlerAdapter()); handlerAdapters.add(new ControllerV4HandlerAdapter()); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //핸들러매핑을 조회해서 우리가 원하는 핸들러(컨트롤러)가 뭔지 찾는다. Object handler = getHandler(request); if(handler == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } //핸들러어댑터를 조회해서 해당 핸들러를 어댑팅 할 수 있는 어댑터를 찾는다. MyHandlerAdapter adapter = getHandlerAdapter(handler); //각 어댑터는 해당 핸들러를 받아 캐스팅한 후 어댑터 처리를 한 후 ModelView를 반환한다. MyHandlerAdapter adapter = getHandlerAdapter(handler); ModelView mv = adapter.handle(request, response, handler); MyView view = viewResolver(mv.getViewName()); view.render(mv.getModel(), request, response); } private Object getHandler(HttpServletRequest request) { String requestURI = request.getRequestURI(); return handlerMappingMap.get(requestURI); } private MyHandlerAdapter getHandlerAdapter(Object handler) { for (MyHandlerAdapter adapter : handlerAdapters) { if(adapter.supports(handler)) { return adapter; } } throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler =" + handler); } private MyView viewResolver(String viewName) { return new MyView("/WEB-INF/views/" + viewName + ".jsp"); } }
Java
복사
우리가 다룰 수 있는 핸들러(컨트롤러)의 리스트를 HandlerMappingMap에 넣는다.
다양한 핸들러(컨트롤러)를 정해진 규격으로 컨버터해주는 어댑터의 리스트를 HandlerAdapters에 넣는다.
이 HandlerMappingMap과 HandlerAdapters는 FrontController가 생성될 때 만들어준다.
요청이 들어오면 해당 요청 URI에 따라 HandlerMappingMap에서 처리 해줄 핸들러를 찾는다.
URI를 key로 해서 value를 찾아온다.
찾은 핸들러를 컨버터해줄 어댑터를 HandlerAdapters에서 찾는다.
for문으로 Adapter의 supports()를 호출해가며 확인한 후 instanceof에 맞는걸 찾아온다.
찾은 어댑터의 handle()를 호출해 ModelView를 반환하도록 컨버트한다.
V3의 Controller는 ModelView를 반환하지만 V4의 Controller는 viewName을 반환한다.
어댑터의 handle()은 해당 Controller(핸들러)를 호출한 후 반환값을 ModelView로 통일해준다.
그 후엔 똑같은 방법으로 ModelView를 이용해 렌더해준다.
//ControllerV4HandlerAdapter public class ControllerV4HandlerAdapter implements MyHandlerAdapter { //instanceof를 통해 ControllerV4의 구현 객체인지 확인한다. @Override public boolean supports(Object handler) { return (handler instanceof ControllerV4); } //Object인 핸들러를 받아 캐스팅한 후 ModelView로 반환하기 위한 컨버터를 해준다. @Override public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException { ControllerV4 controller = (ControllerV4) handler; Map<String, String> paramMap = createParamMap(request); Map<String, Object> model = new HashMap<>(); String viewName = controller.process(paramMap, model); ModelView mv = new ModelView(viewName); mv.setModel(model); return mv; } private Map<String, String> createParamMap(HttpServletRequest request) { Map<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
어댑터는 FrontController와 Handler(Controller) 사이의 처리를 해주는 역할이다.

정리

v1 : 프론트 컨트롤러 도입
기존에 Controller마다 요청을 받던걸
controllerMapping을 통해 FrontController가 모든 요청을 받고
적합한 Controller를 호출해주는 구조로 변경함으로써 공통 처리를 가능하게 함
v2 : View 분리
반복되는 forward 코드를 MyView의 render()를 통해 분리
v3 : Model 추가
기존에 모든 Controller가 HttpServletRequest/Response의 attribute를 사용하고
절대 경로를 통해 View를 생성해서 반환하는 방식을
ModelView를 통해 viewName과 model을 만들어 view 이름과 데이터를 담아 전달한다.
또한 viewResolver를 통해 논리적 경로를 절대 경로로 변환해준다.
v4 : 단순하고 실용적인 컨트롤러
Controller들이 ModelView를 통해 model을 직접 만들어서 데이터를 담아 전달해야 하는 번거로움을
FrontController에서 model을 만들어 각 Controller에게 전달해서 데이터를 담는 방식으로 바꾼다.
v5 : 유연한 컨트롤러
기존의 방식으로 v1~v5의 다양한 컨트롤러를 함께 사용하지 못하는 문제점을 개선하기 위해
controller를 handler로 확장하고 FrontController와 Handler를 이어줄 Adapter를 추가
핸들러 매핑 정보와 어댑터 목록을 갖고 있는 FrontController가 요청 받은 핸들러에 적합한 어댑터를 호출 → 어댑터는 핸들러를 호출한 후 FrontController와 이어주기 위한 처리를 한다.
→ 여태까지 만든 구조는 Spring MVC의 핵심 구조와 거의 같다.
→ 어노테이션을 활용하여 어노테이션을 지원하는 어댑터를 추가하면 컨트롤러를 더 발전 시킬 수 있다.

Spring MVC

v1~v5를 통해 만든 구조
Spring MVC
뭐가 달라졌나?
FrontController → DispatcherServlet
handlerMappingMap → HandlerMapping
MyHandlerAdapter → HandlerAdapter
ModelView → ModelAndView
viewResolver → ViewResolver
MyView → View
DispatcherServlet
부모 클래스에서 HttpServlet을 상속 받아서 사용하고 서블릿으로 동작한다.
(DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet)
DispatcherServlet은 모든 경로(urlPatterns=”/”)에 대해서 매핑한다.
하지만 더 자세한 경로가 우선 순위가 높다.
요청 흐름
서블릿이 호출되면 HttpServlet.service()가 호출되므로 FrameworkServlet은 service()를 오버라이드한다.
결국 DispatcherServlet.doDispatch()가 호출된다. 아래는 doDispatch()의 흐름이다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 1. 핸들러 조회 mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // 2. 핸들러 어댑터 조회 (찾은 핸들러를 처리할 수 있는 어댑터) HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 3. 핸들러 어댑터 실행 // 4. 핸들러 어댑터를 통해 핸들러 실행 // 5. ModelAndView 반환 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
Java
복사
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // 뷰 렌더링 호출 if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isTraceEnabled()) { logger.trace("No view rendering, null ModelAndView returned."); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { // Exception (if any) is already handled.. mappedHandler.triggerAfterCompletion(request, response, null); } }
Java
복사
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; String viewName = mv.getViewName(); if (viewName != null) { // 6. ViewResolver를 통해 뷰 찾고 반환 view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else { // No need to lookup: the ModelAndView object contains the actual View object. view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } } // 7. View 렌더링 if (logger.isTraceEnabled()) { logger.trace("Rendering view [" + view + "] "); } try { if (mv.getStatus() != null) { request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus()); response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "]", ex); } throw ex; } }
Java
복사
동작 순서
핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑한 핸들러(컨트롤러)를 조회한다.
핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다.
핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.
ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
ViewResolver 호출 : 뷰 리졸버를 찾고 실행한다.
JSP의 경우 InternalResourceViewResolver가 사용된다.
View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고 렌더링 역할을 담당하는 뷰 객체를 반환
JSP의 경우 InternalResourceView(JstlView)를 반환하고 내부에 forward()로직이 있다.
뷰 렌더링 : 뷰를 통해서 뷰를 렌더링 한다.
인터페이스
MVC 구조를 이루는 대부분의 역할은 확장 가능할 수 있도록 인터페이스로 제공된다.
핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
뷰 리졸버 : org.springframework.web.servlet.ViewResolver
뷰 : org.springframework.web.servlet.View

HandlerMapping, HandlerAdapter

Controller를 호출하기 위해 거쳐야 하는 2가지 인터페이스
스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해놨다!
HandlerMapping
0순위 : RequestMappingHandlerMapping (@RequestMapping에 사용)
1순위 : BeanNameUrlHandlerMapping (스프링 빈의 이름으로 핸들러를 찾는다)
@Component("/springmvc/old-controller") public class OldController implements Controller { @Override public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { System.out.println("OldController.handleRequest"); return null; } }
Java
복사
HandlerAdapter
0순위 : RequestMappingHandlerAdapter (@RequestMapping에 사용)
1순위 : HttpRequestHandlerAdapter (HttpRequestHandler 인터페이스를 구현한 구현 객체 처리)
2순위 : SimpleControllerHandlerAdapter (Controller 인터페이스를 구현한 구현 객체 처리)
@Component("/springmvc/request-handler") public class MyHttpRequestHandler implements HttpRequestHandler { @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("MyHttpRequestHandler.handleRequest"); } }
Java
복사
@RequestMapping
어노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터로 가장 많이 사용되므로 우선 순위가 높다

ViewResolver

핸들러 어댑터에게 논리 이름을 받아 알맞은 View 객체를 가져오는 역할이다.
BeanNameViewResolver (빈 이름으로 뷰를 찾아서 반환한다.)
InternalResourceViewResolver (JSP를 처리할 수 있는 뷰를 반환한다. Jstl 사용 시 JstlView 반환)
ThymeleafViewResolver (타임리프를 처리할 수 있는 뷰를 반환한다.)
스프링 부트는 InternalResourceViewResolver를 자동으로 등록하는데 application.properties에 prefix, suffix를 설정해줘야 한다.
spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp
Java
복사
InternalResourceView는 forward()를 통해 렌더링하고 다른 View는 자체적으로 렌더링하기도 한다.

@RequestMapping

기존에 HandlerMapping 조회와 HandlerAdapter 조회를 하는 역할이다.
클래스 레벨과 메서드 레벨에 붙일 수 있으며 요청URL에 대한 Controller를 매핑해준다.
(클래스에 기본 경로를 지정하고 메서드마다 추가 경로를 지정할 수 있다.)
@Controller가 붙은 클래스에서만 사용 가능하다.
@Controller @RequestMapping("/springmvc/v3/members") public class SpringMemberControllerV3 { private MemberRepository memberRepository = MemberRepository.getInstance(); @GetMapping("/new-form") public String newForm() { return "new-form"; } @GetMapping public String members(Model model) { List<Member> members = memberRepository.findAll(); model.addAttribute("members", members); return "members"; } @PostMapping("/save") public String save(@RequestParam("username") String username, @RequestParam("age") int age, Model model) { Member member = new Member(username, age); memberRepository.save(member); model.addAttribute("member", member); return "save-result"; } }
Java
복사
@RequestMapping(value = “URL”, method = RequestMethod.GET) 로 HTTP 메서드를 지정해줄 수 있다.
@GetMapping(”URL”), @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 로 간단하게 사용 가능
반환 방법
ModelAndView 객체로 반환하는 방법
ViewName으로 직접 반환하는 방법
Model
파라미터로 Model을 받아서 Model 객체를 따로 생성할 필요가 없다.
@RequestParam
request.getParameter(”username”) 대신 @RequestParam(”username”) String username 를 파라미터에 입력해 사용할 수 있다.
GET, POST 모두 지원한다.

@RestController

@Controller는 반환 값이 String이면 뷰 이름으로 인식하기 때문에 뷰를 찾고 렌더링한다.
@RestController는 반환 값이 HTTP 메시지 바디에 바로 입력된다.

로깅

스프링 부트 라이브러리를 사용하면 spring-boot-starter-logging 라이브러리가 함께 포함된다.
SLF4J (Logback, Log4J, Log4J2 등 수 많은 라이브러리를 통합한 인터페이스 같은 것)
Logback (SLF4J 인터페이스를 구현한 구현체 같은 라이브러리)
로그 선언
1. private final Logger log = LoggerFactory.getLogger(getClass()); 2. private static final Logger log = LoggerFactory.getLogger(LogTestController.class); 3. @Slf4j
Java
복사
@Slf4j를 붙이면 2번의 코드를 자동으로 생성해준다.
로그 호출
log.trace("trace log={}", name); log.debug("debug log={}", name); log.info("info log={}", name); log.warn("warn log={}", name); log.error("error log={}", name);
Java
복사
로그 출력 포멧
2022-06-25 21:02:54.889 INFO 5217 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 3 ms
Java
복사
[시간] [로그 레벨] [프로세스 ID] - - - [쓰레드 명] [클래스명] : [로그 메시지]
로그 레벨
TRACE > DEBUG > INFO > WARN > ERROR
보통 개발 서버는 debug , 운영 서버는 info 출력
로그 레벨 설정
//application.properties #전체 로그 레벨 설정(기본 info) logging.level.root=info #demo 패키지와 그 하위 로그 레벨 설정 logging.level.com.example.demo=debug
Java
복사
올바른 로그 사용법
안 좋은 예시 : log.debug(”data=” + data)
JAVA 언어 특성상 문자 더하기 연산이 실행되므로 불 필요한 연산이 수행된다.
좋은 예시 : log.debug(”data={}”, data)
불 필요한 연산이 실행되지 않는다.
장점
1.
스레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고 출력 모양을 조정할 수 있다.
2.
로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영 서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
3.
시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다. 파일로 남기는 경우 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
4.
성능도 일반 System.out보다 좋다
→ 실무에서 무조건 사용해야 한다.

HTTP 요청 매핑

@RequestMapping
@RequestMapping(value = “URL”, method = RequestMethod.GET) 로 HTTP 메서드를 지정해줄 수 있다.
@GetMapping(”URL”), @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 로 간단하게 사용 가능
매핑된 경로 뒤에 ‘/’가 붙는 것도 같은 요청으로 매핑한다. /hello-basic == /hello-basic/
요청 URL 을 배열 {}로 묶어서 다중 설정이 가능하다. {”/hello-basic”, “/hello-go”}
method를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다. (모두 허용)
@RestController
@Controller는 반환 값이 String이면 뷰 이름으로 인식하기 때문에 뷰를 찾고 렌더링한다.
@RestController는 반환 값이 HTTP 메시지 바디에 바로 입력된다.
@PathVariable
@GetMapping("/mapping/{userId}") public String mappingPath(@PathVariable("userId") String data) { log.info("mappingPath userId={}", data); return "ok"; }
Java
복사
최근 HTTP API는 경로에 식별자를 넣는 스타일을 선호한다. /mapping/userA
@RequestParam과 비슷하다
@RequestMapping은 URL 경로를 템플릿화 할 수 있는데 @PathVariable을 사용하면 매칭되는 부분을 편리하게 조회할 수 있다.
URL 식별자와 이름과 변수명이 같은 경우 @PathVariable만 붙여도 된다.
특정 파라미터 조건 매핑
@GetMapping(value = "/mapping-param", params = "mode=debug") public String mappingParam() { log.info("mappingParam"); return "ok"; }
Java
복사
파라미터에 “mode=debug” 가 존재해야 호출된다.
여러 가지 표현 가능
params=”mode”
params=”!mode”
params=”mode=debug”
params=”mode≠debug”
params={”mode=debug”, “data=good”}
특정 헤더 조건 매핑
@GetMapping(value = "/mapping-header", headers = "mode=debug") public String mappingHeader() { log.info("mappingHeader"); return "ok"; }
Java
복사
파라미터 매핑과 비슷하지만 HTTP 헤더를 사용한다.
Content-Type 조건 매핑 (consumes)
@PostMapping(value = "/mapping-consume", consumes = "application/json") public String mappingConsumes() { log.info("mappingConsumes"); return "ok"; }
Java
복사
파라미터 매핑과 비슷하지만 HTTP 요청에 Content-Type 헤더를 기반으로 매핑한다.
(consume은 들어오는 요청에 대한 필터)
다양한 표현 가능
consumes = “application/json”
consumes = “!application/json”
consumes = “application/*”
consumes = “*\/*”
MediaType.APPLICATION_JSON_VALUE
consumes = {”text/plain”, “application/*”}
Accept 조건 매핑 (produce)
@PostMapping(value = "/mapping/produce", produces = "text/html") public String mappingProduces() { log.info("mappingProduces"); return "ok"; }
Java
복사
파라미터 매핑과 비슷하지만 HTTP 요청에 Accept 헤더를 기반으로 매핑한다.
(produces는 나가는 응답에 대한 필터)

HTTP API 요청 매핑

먼저 API에 대한 구상
회원 목록 조회 : GET /users
회원 등록 : POST /users
회원 조회 : GET /users/{userId}
회원 수정 : PATCH /users/{userId}
회원 삭제 : DELETE /users/{userId}
@RestController @RequestMapping("/mapping/users") public class MappingClassController { @GetMapping public String user() { return "get users"; } @PostMapping public String addUser() { return "post user"; } @GetMapping("/{userId}") public String findUser(@PathVariable String userId) { return "get userId = " + userId; } @PatchMapping("/{userId}") public String updateUser(@PathVariable String userId) { return "update userId = " + userId; } @DeleteMapping("/{userId}") public String deleteUser(@PathVariable String userId) { return "delete userId = " + userId; } }
Java
복사
URL을 계층적으로 매핑

HTTP 요청 - 헤더 조회

@Controller, @RestController를 사용하면 다양한 파라미터를 통해 HTTP 헤더 정보를 조회할 수 있다.
@Slf4j @RestController public class RequestHeaderController { @RequestMapping("/headers") public String headers(HttpServletRequest request, HttpServletResponse response, HttpMethod httpMethod, Locale locale, @RequestHeader MultiValueMap<String, String> headerMap, @RequestHeader("host") String host, @CookieValue(value = "myCookie", required = false) String cookie) { log.info("request={}", request); log.info("response={}", response); log.info("httpMethod={}", httpMethod); log.info("locale={}", locale); log.info("headerMap={}", headerMap); log.info("header host={}", host); log.info("myCookie={}", cookie); return "ok"; } }
Java
복사
HttpServletRequest, HttpServletResponse : request, response 객체를 사용한다.
HttpMethod : HTTP 메서드를 조회한다. (GET, POST, …)
Locale : Locale 정보를 조회한다. (ko_kR, …)
@RequestHeader : HTTP 헤더의 정보를 조회한다.
@RequestHeader MultiValueMap<String, String> headerMap : 모든 헤더 정보를 조회
@RequestHeader(”host”) String host : 특정 헤더 정보를 조회
@CookieValue : 쿠키를 조회한다.
속성들
value : 조회할 데이터의 이름을 지정한다.
required : 필수 값 여부를 지정한다.
defaultValue : 기본 값을 지정한다.

HTTP 요청 - 요청 파라미터(쿼리 파라미터, HTML Form)

클라이언트에서 서버로 요청 데이터를 전달할 때는 주로 3가지 방법을 사용한다.
GET - 쿼리 파라미터
/url?username=hello&age=20
POST - HTML Form
Content-Type : application/x-www-form-urlencoded
message body에 username=hello&age=20 형식으로 전달
HTTP message body
JSON, XML, TEXT 형식으로 전달
POST, PUT, PATCH 주로 사용

HttpServletRequest.getParameter()

HttpServletRequest 의 getParameter()로 쿼리 파라미터와 HTML Form 방법을 조회할 수 있다.
HTML Form 예시
POST /request-param ... content-type: application/x-www-form-urlencoded username=hello&age=20
Java
복사
요청 파라미터 조회
@Slf4j @Controller public class RequestParamController { @RequestMapping("/request-param-v1") public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException { String username = request.getParameter("username"); int age = Integer.parseInt(request.getParameter("age")); log.info("username={}, age={}", username, age); response.getWriter().write("ok"); } }
Java
복사
반환 타입이 void고 response에 직접 값을 넣으면 View를 조회하지 않는다.

더 편리한 요청 파라미터 조회 - @RequestParam

//@RequestParam 사용 @ResponseBody @RequestMapping("/request-param-v2") public String requestParamV2(@RequestParam("username") String memberName, @RequestParam("age") int memberAge) { log.info("username={}, age={}", memberName, memberAge); return "ok"; } //파라미터 이름과 변수 이름이 같다면 (name="") 생략 가능 @ResponseBody @RequestMapping("/request-param-v3") public String requestParamV3(@RequestParam String username, @RequestParam int age) { log.info("username={}, age={}", username, age); return "ok"; } //String, int 등의 단순 타입이라면 @RequestParam도 생략 가능 @ResponseBody @RequestMapping("/request-param-v4") public String requestParamV4(String username, int age) { log.info("username={}, age={}", username, age); return "ok"; } //required 로 파라미터 필수 여부 설정 (기본값은 true) @ResponseBody @RequestMapping("/request-param-required") public String requestParamRequired(@RequestParam(required = true) String username, @RequestParam(required = true) int age) { log.info("username={}, age={}", username, age); return "ok"; } //defaultValue 로 기본 값 설정 @ResponseBody @RequestMapping("/request-param-default") public String requestParamDefault(@RequestParam(defaultValue = "guest") String username, @RequestParam(defaultValue = "-1") int age) { log.info("username={}, age={}", username, age); return "ok"; }
Java
복사
@RequestParam의 value는 파라미터 이름으로 바인딩
변수 이름과 value가 같다면 생략 가능하고 단순 타입이라면 @RequestParam도 생략 가능하다.
파라미터에 이름만 있고 값이 없는 경우 → 빈 문자로 통과
기본형에 null 입력 → 500에러
반환 타입이 String이고 문자를 반환할 때 View를 조회하고 싶지 않다면 @ResponseBody를 사용
required로 필수 여부를 설정할 수 있다 (기본값은 true)
defualtValue로 기본값을 적용할 수 있다.

요청 파라미터를 Map으로 조회 - @RequestParamMap

//Map @ResponseBody @RequestMapping("/request-param-map") public String requestParamMap(@RequestParam Map<String, Object> paramMap) { log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age")); return "ok"; } //MultiValueMap @ResponseBody @RequestMapping("/request-param-multimap") public String requestParamMultiMap(@RequestParam MultiValueMap<String, Object> paramMap) { log.info("username={}, age={}", paramMap.get("username").get(1), paramMap.get("age").get(1)); return "ok"; }
Java
복사
파라미터를 Map, MultiValueMap으로 조회할 수 있다.
@RequestParam Map<String, Object> paramMap
Map(key, value)
@RequestParam MultiValueMap<String, Object> paramMap
MultiValueMap(key, [value1, value2, …])
파라미터의 값이 1개가 확실하면 Map 아니면 MultiValueMap을 사용하자.

요청 파라미터로 Model 객체를 손쉽게 - @ModelAttribute

동작 순서
1.
@ModelAttribute가 붙은 객체를 생성
2.
요청 파라미터의 이름으로 객체의 프로퍼티를 찾아 해당 프로퍼티의 setter를 호출해 값을 바인딩
(username=”hello” 인 경우 setUsername(”hello”) 호출)
3.
Model 추가
파라미터에 Model이 없는 경우에도 자동으로 model.addAttribute(”Model에 포함할 이름”) 적용
바인딩 오류
값을 타입과 일치하지 않게 바인딩하면 BindException 발생
//HelloData (Model 객체) @Data public class HelloData { private String username; private int age; }
Java
복사
@Data
@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용
파라미터에 대한 프로퍼티를 가져야 한다.
@ResponseBody @RequestMapping("/model-attribute-v1") public String modelAttributeV1(@ModelAttribute HelloData helloData) { log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return "ok"; } @ResponseBody @RequestMapping("/model-attribute-v2") public String modelAttributeV2(HelloData helloData) { log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return "ok"; }
Java
복사
@ModelAttribute를 생략할 수 있다.
String, int, Integer 등 단순 타입 = @RequestParam
argument rsolver 지정 타입 외 나머지 = @ModelAttribute
(@ModelAttribute가 붙은 파라미터의 타입 이름 첫 글자를 소문자로 바꿔 Model에 추가한다.)

요청 메시지 - 단순 텍스트

HTTP message Body에 데이터를 직접 담은 요청에서 데이터를 조회할 때 사용한다.
이 경우엔 앞선 경우들처럼 @RequestParam, @ModelAttribute를 사용할 수 없다.
//HttpServletRequest로부터 InputStream을 받아와서 사용 @PostMapping("/request-body-string-v1") public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException { ServletInputStream inputStream = request.getInputStream(); String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); log.info("messageBody={}", messageBody); response.getWriter().write("ok"); }
Java
복사
HttpServletRequest로부터 InputStream을 받아와서 StreamUtils를 사용하는 방식이다.
//InputStream, OutputStream을 파라미터로 받아와서 사용 @PostMapping("/request-body-string-v2") public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException { String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); log.info("messageBody={}", messageBody); responseWriter.write("ok"); }
Java
복사
Spring MVC가 파라미터로 지원해주는 InputStream, OutputStream을 받아와서 사용하는 방식이다.
InputStream(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회
OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력
//HttpEntity를 받아와서 사용 @PostMapping("/request-body-string-v3") public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) { String messageBody = httpEntity.getBody(); log.info("messageBody={}", messageBody); return new HttpEntity<>("ok"); }
Java
복사
Spring MVC가 지원해주는 HttpEntity를 이용해 HTTP header, body를 조회하는 방식이다.
RequestEntity : 요청에 사용한다 (HTTP Method, URL 정보 추가)
ResponseEntity : 응답에 사용한다 (HTTP StatusCode 설정 가능)
응답해도 사용 가능하며 헤더 정보를 포함할 수 있다.
Spring MVC 내부에서 HTTP message body를 읽어 문자나 객체로 변환해서 전달하는데
이 때, HttpMessageConverter라는 기능을 지원한다.
//@RequestBody, @ResponseBody @PostMapping("/request-body-string-v4") public String requestBodyStringV4(@RequestBody String messageBody) { log.info("messageBody={}", messageBody); return "ok"; }
Java
복사
@RequestBody String messagebody를 파라미터로 사용하여 쉽게 데이터를 조회할 수 있다.
(헤더 정보가 필요하다면 HttpEntity 혹은 @RequestHeader를 사용하면 된다.)
@ResponseBody를 이용하면 View는 무시한채 반환값을 message body로 직접 전달한다.
정리
요청 파라미터를 조회 : @RequestParam, @ModelAttribute
HTTP message body 조회 : @RequestBody

요청 메시지 - JSON

HTTP message Body에 JSON을 담은 요청에서 데이터를 조회할 때 사용한다.
JSON을 Map형태로 바꾸기 위해선 jackson 라이브러리에서 지원하는 ObjectMapper를 사용해야 한다.
//HttpServletRequest로부터 InputStream을 받아와서 사용 @PostMapping("/request-body-json-v1") public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException { ServletInputStream inputStream = request.getInputStream(); String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); log.info("messageBody={}", messageBody); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); response.getWriter().write("ok"); }
Java
복사
HttpServletRequest로부터 InputStream을 받아와서 StreamUtils를 사용하는 방식이다.
//@RequestBody를 이용해 message body를 가져와서 사용 @ResponseBody @PostMapping("/request-body-json-v2") public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException { log.info("messageBody={}", messageBody); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return "ok"; }
Java
복사
@RequestBody를 이용해 message body를 받아와 ObjectMapper를 통해 객체로 변환하는 방식이다.
//@RequestBody를 통해 message body를 객체로 변환시켜서 사용 (객체 자체에 바인드) @ResponseBody @PostMapping("/request-body-json-v3") public String requestBodyJsonV3(@RequestBody HelloData data) { log.info("username={}, age={}", data.getUsername(), data.getAge()); return "ok"; } //HttpEntity를 통해 message body를 객체로 받아와서 사용 (httpEntity에서 body를 받아야함) @ResponseBody @PostMapping("/request-body-json-v4") public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) { HelloData data = httpEntity.getBody(); log.info("username={}, age={}", data.getUsername(), data.getAge()); return "ok"; }
Java
복사
@RequestBody를 객체에 붙이게 되면 HTTP 메시지 컨버터가 message body를 객체로 변환시켜서 사용
HttpEntity는 message body를 객체로 변환시켜 가지고 있는다.
//JSON을 객체로 변환시켜 사용하고 객체를 다시 JSON으로 변환시켜 응답 @ResponseBody @PostMapping("/request-body-json-v5") public HelloData requestBodyJsonV5(@RequestBody HelloData data) { log.info("username={}, age={}", data.getUsername(), data.getAge()); return data; }
Java
복사
@RequestBody : JSON 요청 → HTTP 메시지 컨버터 → 객체
@ResponseBody : 객체 → HTTP 메시지 컨버터 → JSON 응답

HTTP 응답 - 정적 리소스, 뷰 템플릿

스프링에서 응답 데이터를 만드는 방법은 3가지이다.
정적 리소스 (정적인 HTML, CSS, JS)
해당 파일을 변경 없이 그대로 서비스하는 것
/static, /public, /resources, /META-INF/resources
뷰 템플릿 사용 (동적인 HTML)
뷰 템플릿을 거쳐 HTML이 생성되고 뷰다 응답을 만들어 전달하는 것
/templates
HTTP message body (HTTP API (JSON, XML, TEXT 등))

뷰 템플릿 사용

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p th:text="${data}">empty</p> </body> </html>
HTML
복사
//viewPath와 데이터를 담은 Model을 저장한 ModelAndView를 생성해 반환하는 방식 @RequestMapping("/response-view-v1") public ModelAndView responseViewV1() { ModelAndView mv = new ModelAndView("response/hello").addObject("data", "hello!"); return mv; }
Java
복사
//파라미터로 전달받은 Model에 데이터를 담고 viewPath를 반환하는 방식 @RequestMapping("/response-view-v2") public String responseViewV2(Model model) { model.addAttribute("data", "hello!"); return "response/hello"; }
Java
복사
ViewResolver가 해당 viewPath에 맞는 View를 찾아서 렌더링한다.
//Spring이 관례적으로 허용해주는 생략 방식 @RequestMapping("/response/hello") public void responseViewV3(Model model) { model.addAttribute("data", "hello!"); }
Java
복사
@RequestMapping에 viewPath를 적으면 viewPath를 따로 반환하지 않아도 된다.
@Controller를 사용하고 HttpServletResponse, OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없는 경우 사용 가능

HTTP message body 에 직접 입력

HTML이나 뷰 템플릿도 다 HTTP 응답 메시지 바디에 데이터를 담아서 전달하는 것이다.
이제부터의 내용은 HTTP 응답 메시지 바디에 직접 데이터를 담아서 전달하는 방식을 말한다.
@GetMapping("/response-body-string-v1") public void responseBodyV1(HttpServletRequest request, HttpServletResponse response) throws IOException { response.getWriter().write("ok"); }
Java
복사
HttpServletResponse의 writer를 이용하여 문자를 담는 방식
@GetMapping("/response-body-string-v2") public ResponseEntity<String> responseBodyV2() { return new ResponseEntity<>("ok", HttpStatus.OK); }
Java
복사
ResponseEntity에 문자와 상태코드를 담아 반환하면 HTTP 메시지 컨버터가 변환하는 방식
@ResponseBody @GetMapping("/response-body-string-v3") public String responseBodyV3() { return "ok"; }
Java
복사
@ResponseBody 를 사용해 String을 그대로 출력하는 방식
@GetMapping("/response-body-json-v1") public ResponseEntity<HelloData> responseBodyJsonV1() { HelloData helloData = new HelloData(); helloData.setUsername("userA"); helloData.setAge(20); return new ResponseEntity<>(helloData, HttpStatus.OK); }
Java
복사
데이터를 담은 객체를 ResponseEntity로 반환하면 HTTP 메시지 컨버터가 JSON으로 변환하는 방식
@ResponseStatus(HttpStatus.OK) @ResponseBody @GetMapping("/response-body-json-v2") public HelloData responseBodyJsonV2() { HelloData helloData = new HelloData(); helloData.setUsername("userA"); helloData.setAge(20); return helloData; }
Java
복사
@ResponseBody를 사용해 객체 자체를 반환하는 방식으로 @ResponseStatus를 사용해 정적인 상태코드를 붙일 수 있다.

@RestController

@ResponseBody + @Controller 의 기능을 가진 어노테이션이다.
해당 컨트롤러 내 모든 메서드에 @ResponseBody가 적용되는 효과가 있다.

HTTP 메시지 컨버터

@ResponseBody 사용한 경우
HTTP의 BODY에 문자 내용을 직접 반환
viewResolver 대신 HttpMessageConverter가 동작
기본 문자 처리 : StringHttpMessageConverter
기본 객체 처리 : MappingJackson2HttpMessageConverter
이외에도 byte처리 등 기타 여러 HttpMessageConverter가 스프링에 기본 등록
HttpMessageConverter의 종류는 HTTP Accpet 헤더, Controller 반환 타입 등을 조합해서 선택된다.
HttpMessageConverter는 HTTP 요청, 응답 둘 다 사용된다.
HTTP 요청 : @RequestBody, HttpEntity(RequestEntity)
HTTP 응답 : @ResponseBody, HttpEntity(ResponseEntity)
스프링 부트의 기본 메시지 컨버터
대상 클래스 타입과 미디어 타입 둘을 체크해서 사용 여부를 결정한다.
만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.
canRead(), canWrite()를 통해 MessageConverter가 해당 클래스, 미디어타입을 지원하는지 체크
read(), write()를 통해 MessageConverter가 메시지를 읽고 쓴다.
0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter
Java
복사
ByteArrayHttpMessageConverter
클래스 타입 : byte[], 미디어 타입 : */*
요청 : @RequestBody byte[] data
응답 : @ResponseBody return byte[] , application/octet-stream
StringHttpMessageConverter
클래스 타입 : String, 미디어 타입 */*
요청 : @RequestBody String data
응답 : @ResponseBody return “ok” , text/plain
MappingJackson2HttpMessageConverter
클래스 타입 : 객체 또는 HashMap, 미디어 타입 : application/json
요청 : @RequestBody Hellodata data
응답 : @ResponseBody return hellodata, 미디어 타입 : application/json
요청 동작 순서
1.
HTTP 요청이 오고 컨트롤러에서 @RequestBody 나 HttpEntity로 받는다.
2.
MessageConverter가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출한다.
a.
대상 클래스 타입을 지원하는가
b.
HTTP 요청의 Content-Type 미디어 타입을 지원하는가
3.
canRead()를 만족하면 read()를 호출해서 객체를 생성하고 반환한다.
응답 동작 순서
1.
컨트롤러에서 @ResponseBody 나 HttpEntity로 값을 반환한다.
2.
MessageConverter가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
a.
대상 클래스 타입을 지원하는가
b.
HTTP 요청의 Accept 미디어 타입을 지원하는가 (@RequestMapping의 produces)
3.
canWrite()를 만족하면 write()를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.

요청 매핑 핸들러 어댑터 구조

HTTP 메시지 컨버터는 어디서 사용되는걸까?
→ 모든 비밀은 @RequestMapping을 처리하는 RequestMappingHandlerAdapter 에 있다.

ArgumentResolver

ArgumentResolver의 역할
HttpServletRequest, Model 같은 파라미터 전달
@RequestParam, @ModelAttribute 같은 어노테이션 처리
@RequestBody, HttpEntity 같은 HTTP 메시지 처리
스프링은 30개가 넘는 ArgumentResolver를 제공한다.
//HandlerMethodArgumentResolver public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
Java
복사
동작 방식
1.
HandlerAdapter는 Handler를 호출하기 전 Handler의 파라미터를 확인
2.
ArgumentResolver의 supportsParameter()를 호출해 해당 파라미터를 지원하는지 체크
3.
지원하면 resolveArgument()를 호출해서 실제 객체를 생성
4.
Handler 호출 시 생성된 객체를 파라미터로 넘겨준다.

ReturnValueHandler

Handler의 반환 값을 변환하고 처리하는 역할
스프링은 10개가 넘는 ReturnValueHandler를 제공한다.
//HandlerMethodReturnValueHandler public interface HandlerMethodReturnValueHandler { boolean supportsReturnType(MethodParameter returnType); void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; }
Java
복사

HTTP 메시지 컨버터

요청의 경우
@RequestBody를 처리하는 ArgumentResolver가 있고 HttpEntity를 처리하는 ArgumentResolver가 있다.
이 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
응답의 경우
@ResponseBody를 처리하는 ReturnValueHandler가 있고 HttpEntity를 처리하는 ReturnValueHandler가 있다.
이 ReturnValueHandler들이 HTTP 메시지 컨버터를 사용해서 응답 결과를 만든다.
Spring MVC의 경우
@RequestBody, @ResponseBody는 RequestResponseBodyMethodProcessor를 사용
HttpEntity는 HttpEntityMethodProcessor를 사용
정리
스프링은 아래를 모두 인터페이스로 제공하며 필요하면 언제든지 확장할 수 있다.
HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter
기능 확장은 WebMvcConfigurer를 상속 받아서 스프링 빈으로 등록하면 된다.

redirect

return "redirect:/..."
Java
복사
String으로 viewPath를 반환하는 경우 redirect를 통해 3xx번대 상태코드를 반환할 수 있다.
{} 를 통해 컨트롤러에 매핑된 @PathVariable 값을 사용할 수 있다.

PRG (Post/Redirect/Get)

상품 저장 후 새로고침을 하게 되면 POST가 다시 요청되는 경우
상품 ID는 계속 올라가지만 내용은 그대로인 요청이 계속 반복된다.
위 문제를 해결하기 위해 POST 후 Redirect 하고 새로고침하면 GET이 되게 해야한다.
//이전 코드 @PostMapping("/add") public String addItem(@ModelAttribute("item") Item item) { itemRepository.save(item); return "basic/item"; } //이후 코드 @PostMapping("/add") public String addItem(@ModelAttribute("item") Item item) { itemRepository.save(item); return "redirect:/basic/items/" + item.getId(); }
Java
복사

RedirectAttributes

위 방식은 새로고침하면 POST가 재요청되는 문제는 해결하였다.
하지만 URL에 변수를 더하는 방식은 URL 인코딩이 되지 않기 때문에 위험하다.
또한 고객 입장에서 저장이 잘 된건지 아닌지 확인할 수 없는 문제점이 있다.
@PostMapping("/add") public String addItem(@ModelAttribute("item") Item item, RedirectAttributes redirectAttributes) { Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/basic/items/{itemId}"; }
Java
복사
RedirectAttributes 파라미터를 통해 itemId와 저장이 잘 됐다는 status=true 를 저장할 수 있게 된다.
itemId는 @PathVariable처럼 {itemId}을 바인딩 → redirect:/basic/items/{itemId} 가능
status=true는 ?status=true 로 URL에 쿼리 파라미터로 전달된다.
//item.html <div class="container"> <div class="py-5 text-center"> <h2>상품 상세</h2> </div> <!-- 추가! --> <h2 th:if="${param.status}" th:text="'저장 완료!'"></h2> ...
HTML
복사
status=true 쿼리 파라미터를 통해 HTML에서 저장이 완료된걸 표시해줄 수 있다.