인프런 김영한님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 요약정리한 내용입니다
: 웹 서버는 HTTP를 기반으로 동작한다.
: 정적 리소스? : 웹 브라우저나 클라이언트에서 요청이 들어왔을 때, 그것에 대한 리소스가 이미 만들어져있고, 만들어져 있는 리소스를 그냥 보내주면 되는 것이다.
: HTTP를 기반으로 동작한다.
: 웹 서버와 동일하고, 프로그램 코드를 실행해서 애플리케이션 로직을 수행하기 때문에 다른 사람들에게도 보여줄 수 있음
: 웹 서버 + 웹어플리케이션 서버 + DB로 구성함
: 개발자들이 TCP IP연결하고 멀티쓰레드 고민하기 어려움
: 서블릿이 비즈니스 로직을 제외한 모든 기능들을 WAS에서 지원해준다.
: 애플리케이션 코드를 하나하나 순차적으로 실행하는 것
: 쓰레드 생성에 제한이 없어 속도 늦어지고 서버가 죽을 수 있음
: 필요한 쓰레드를 쓰레드 풀에 보관하고 관리하는 방법
: 주로 쓰레드 수를 조정하는데, 쓰레드 풀을 너무 낮게 설정하면 클라이언트의 응답 지연이, 너무 높게 설정하면 CPU, 메모리 리소스 임계점 초과로 서버가 다운된다.
: 싱글톤 객체만 주의하면 된다.
: 고정된 HTML파일, CSS, JS, 이미지, 영상 등을 제공
: 주로 웹 브라우저
: 동적으로 HTML을 생성하는 것
: HTML이 아니라 데이터를 전달, 주로 JSON 사용
: HTML을 서버에서 다 만들어서 클라이언트에 전달
: 정적인 화면에 사용되고, JSP,타임리프 등
: HTML 결과를 자바스크립트를 사용해서 웹 브라우저에서 동적으로 생성해서 적용
: React, Vue.js
: 현재는 애노테이션 긱반의 스프링 MVC를 사용한다.
: 통합에 대한 고민이 없고, 굉장히 유연하고 편리하게 사용 가능
: 빌드 결과에 WAS 서버 포함
: 자바 뷰 템플렛은 주로 타임리프를 사용한다.
: package를 war로.
: name은 서블릿 이름, urlPatterns를 통해 URL을 매핑한다.
: 서블릿 자동 등록 하도록 해주는 스프링 부트의 어노테이션.
: 열쇠같이 생긴게 service protocol
Ctrl +O : Override 가능한 메서드 목록을 확인하여 구현하기 위한 코드를 자동 생성해 줍니다.
soutm : 메소드의 이름을 출력해줌
: was 서버들이 서블릿 표준 스펙을 구현해서 위의 request에 찍히는 것
: 를 통해 쿼리 파라미터를 조회할 수 있다.
결론적으로, 서블릿을 통하여 http메시지를 편리하게 사용할 수 있도록 대신 parsing 해준다.
: 개발자 대신에 HTTP요청 메시지를 파싱하는 역할이다.
: start line, header에 대한 정보 조회 방법이다.
: HTTP 요청 메시지를 통하여 클라이언트에서 서버로 데이터를 전달하는 방법에는 크게 세 가지가 있다.
: 메시지 바디 없이 url의 쿼리 파라미터를 통하여 전달
: ? 로 시작, 추가 파라미터는 &로 구분
@WebServlet (name = "requestParamServlet", urlPatterns = "/request-param" )
public class RequestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("전체 파라미터 조회");
req.getParameterNames().asIterator().
forEachRemaining(paramName -> System.out.println(paramName + " = "+ req.getParameter(paramName)));
System.out.println("단일 파라미터 조회");
String username = req.getParameter("username");
String age = req.getParameter("age");
System.out.println("복수 파라미터 조회");
String[] usernames = req.getParameterValues("username");
}
}
그외
: soutv를 사용하면 출력을 용이하게 할 수 있음
: iter + enter 시 반복문 생성 가능
: HTML form을 사용해서 클라이언트에서 서버로 데이터를 전송한다.
: 메시지 바디에 쿼리 파라미터 형식으로 전달
: 위의 코드를 중복해서 사용할 수 있다.(쿼리 파라미터 조회 메소드)
: HTTP API에 주로 사용 데이터를 주로 JSON 사용함.
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
: 단순 텍스트 응답, HTML 응답, HTTP API - MessageBody JSON 응답등의 내용을 담아 전달한다.
: Content-Type,쿠키, Redirect등의 기능을 제공한다.
response.setContentType("text/html");
response.setHeader("content-type", "application/json");
: MVC 프레임워크는 궁극적으로 스프링 MVC와 유사한 구조이기 때문에, 이해하기에 도움이 됨
: 예전에는 클라이언트가 공통로직이 필요할 경우 각각 다 만들어야 함
: 프런트 컨트롤러를 도입하면, 서블릿처럼 A,B,C를 각각 처리하도록 프론트 컨트롤러가 해결해줌.
- 프론트 컨트롤러의 특징
: 서블릿하나로 클라이언트의 요청을 받음
: 요청에 맞는 컨트롤러를 찾아서 호출해줌
: 공통처리가 가능해진다.
: 나머지 컨트롤러는 서블릿 사용 필요가 없어짐.
: 구조를 맞추는 단계
: 클라이언트가 HTTP요청을 하면, Front COntroller가 매핑 정보에서 컨트롤러를 조회하고, 컨트롤러를 호출한다. 컨트롤러에서 JSP forward하고 HTML 응답을 해준다.
: 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입하고, 각 컨트롤러들은 이 인터페이스를 구현하면 된다. 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
: 내부 로직은 서블릿과 똑같이 만든다.
private Map<String, ControllerV1> controllerMap = new HashMap<>(); // 이 맵의 경우
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); // 키가 매핑 URL, value가 호출될 컨트롤러이다.
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
: 위의 코드처럼, 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있기 때문에, 뷰를 별도로 처리하는 객체를 만든다.
앞서 봤던 코드를 아래처럼 간단히 줄일 수 있다.
// 1번 MyView myView = new MyView("/WEB-INF/views/new-form.jsp");
// 2번 return myView;
return new MyView("/WEB-INF/views/new-form.jsp"); //1번 + 2번
: 각각의 코드는 이렇게 myView객체를 생성 후 반환하고, 프론트 컨트롤러가 이를 일관적으로 처리한다.
: 컨트롤러 입장에서, HttpServletRequest,HttpServletResponce를 활용한 파라미터는 필요하지만, 그자체는 필요하지 않음.
: 요청 파라미터 정보를 자바의 MAP으로 대신 넘기게 하고 request 객체는 별도의 Model 객체를 만들어서 반환한다.
: 서블릿 기술을 전혀 사용하지 않도록 변경하는 것이 이 장의 내용
: 컨트롤러에서 지정하는 뷰 이름에 중복이 있음.
controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
: 위의 경우 new-form과 save가 다르고 이 부분만 끊어 논리이름, 전체를 물리이름이라 한다.
: 서블릿의 종속성 제거를 위해 Model을 직접 만들고 추가로 view 이름까지 전달하는 객체를 만들어보자
: 스프링 MVC의 modelandview랑 비슷한 역할
- modelandview의 역할
: .setViewName을 통해 뷰의 이름 설정
: .addObject()을 통해 데이터를 보냄
: ModelAndView는 컴포넌트 방식으로 ModelAndView 객체를 생성해서 객체형태로 리턴하며, @controller 지원 후 덜 사용된다.
- 메소드 추출하기 단축기 ctrl + alt + m
@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 {
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); // 1. HTTPServletRequest의 파라미터를 다 뽑아서 파라미터 맵을 만들어 반환
ModelView mv = controller.process(paramMap); //2. 모델뷰로 논리이름만 사용해서 뷰 생성
String viewName = mv.getViewName(); //3. 논리이름을 new - form이라 반환
MyView view = viewResolver(viewName); // 4. new-form으로 view reslover 호출 마이뷰 반환
view.render(mv.getModel(), request, response); //5 render(model)호출
}
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;
}
private MyView viewResolver(String viewName) { // 논리이름을 물리이름으로 바꿔줌
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
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)); // jsp는 setAttribute에 넣어야 편하게 꺼내 쓸 수 있음.
// setAttributegkatnsms 이름이 name인 속성의 값을 value로 지정한다.
}
}
: 실제 구현하는 컨트롤러들의 코드가 굉장히 간편해짐.
: 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 번거롭다.
: controller가 ModelView를 반환하지 않고, ViewName만 반환한다.
: 모델 객체 전달을 프론트 컨트롤러에서 생성한다.
Map<String, Object> model = new HashMap<>(); //추가
컨트롤러의 코드가 이렇게 바뀌는걸 볼 수 있음
// ModelView mv = new ModelView("save-result"); // 모델 뷰 만들고
// mv.getModel().put("member",member); // put 함
// return mv;
model.put("member",member); // 그냥 put 만 하면 됨
return "save-result";
어댑터 패턴 이란?
: 완전히 다른 두 가지의 인터페이스를 호환을 가능하게 하는 것.
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//private Map<String, ControllerV3> controllerMap = new HashMap<>(); // 기존
private final Map<String,Object> handlerMappingMap = new HashMap<>(); // 차이점 : 아무 컨트롤러나 다 넣기 위하여 Object를 넣음
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());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request); // 1. 핸들러를 찾음 (MemberFormControllerV3반환)
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler); // 2. 어댑터를 찾음 (ControllerV3HandlerAdapter 반환)
ModelView mv = adapter.handle(request, response, handler); // 3. handle(handler) + 5. Modelview 반환
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)) { // 2.1 public boolean supports(Object handler)가 true여야 실행이 됨
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//private Map<String, ControllerV3> controllerMap = new HashMap<>(); // 기존
private final Map<String,Object> handlerMappingMap = new HashMap<>(); // 차이점 : 아무 컨트롤러나 다 넣기 위하여 Object를 넣음
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); // 1. 핸들러를 찾음 (MemberFormControllerV4반환)
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler); // 2. 어댑터를 찾음 (ControllerV4HandlerAdapter 반환)
ModelView mv = adapter.handle(request, response, handler); // 3. handle(handler) 5. Modelview 반환
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)) { // 2.1 V3는 false V4는 true를 반환
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
: 확장성이 용이해진 모습.
public ModelView handle(HttpServletRequest request, HttpServletResponse
response, Object handler) {
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;
}
: ControllerV4는 뷰의 이름을 반환하는데, 어댑터는 ModelView를 만들어서 반환해야 한다. 그러한 과정의 위의 세 문장.
: 위에서 만들었던 스프링 MVC 프레임워크와 유사한 구조를 가지고 있다.
: DispatcherServlet이 Frontcontroller를 구성했던 일을 그대로 한다.
- 동작 순서
- 핸들러 조회
- 핸들러 어댑터 조회
- 핸들러 어댑터 실행
- 핸들러 실행
- ModelAndView 반환
- viewResolver 호출
- view 반환
- 뷰 렌더링
: implements Controller(web.servlet.mvc)
: controller V2와 V3의 중간 같은 역할을 한다.
: 이후로는 @RequestMapping을 사용하여 컨트롤러를 만든다.
: ModelAndView를 사용한다.
@RequestMapping : RequestMappingHandlerMapping, RequestMappingHandlerAdapter 를 다룬다.
: @RequestMapping을 활용하면 메소드 레벨과의 조합도 가능하다.
@RequestMapping("/springmvc/v2/members") // 클래스 레벨
@RequestMapping("/new-form") // 메소드 레벨
@RequestMapping("/save") // 메소드 레벨
: thymeleaf로 생성
: packaging을 jar를 선택하는데, 내장 톰캣에 최적화 할 때 사용한다. 반면, war는 톰캣을 별도로 설치하고 빌드된 파일을 넣을 때, jsp를 넣을때 사용
: 스프링 부트에 Jar 를 사용하면 /resources/static/index.hml 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.
: 스프링 부트 - 로깅 - Logback & SLF4J(인터페이스)
: logger 참조는 다음과 같다.
import org.slf4j.Logger;
: @Controller의 경우 반환 값이 String이면 뷰 이름으로 인식되어 뷰를 찾고 뷰가 렌더링 되지만, @RestController의 경우 String이 바로 HTTP 메시지 바디에 바로 입력돼서 반환이 된다.
: system.out.println 대신에 사용한다.
: 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
: 콘솔, 파일, 네트워크 등 로그를 별도의 위치에 남길 수 있다.
: LEVEL은 TRACE>DEBUG>INFO>WARN>ERROR, 조절은 application.properties에서
@Slf4j // 로그 사용 방법 1번
@RestController
public class LogTestController {
//private final Logger log = LoggerFactory.getLogger(getClass()); // 로그 사용 방법 2번
@RequestMapping("/log-test")
public String logTest(){
String name = "Spring";
log.info("info log = {}", name);
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);
// log.debug("String concat log=" + name); 로그를 사용하지 않아도 계산이 되기 때문에 이런식으로는 사용 X
return "ok";
}
}
: 요청이 왔을때 어떤 컨트롤러가 매핑이 되는가
: @RequestMapping 에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.
: 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
: 아래의 경우에는 GET이 아니면 에러
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
: 다양한 파라미터들이 존재하는데, 다음과 같은 역할을 한다.
HttpMethod httpMethod
: HTTP 메소드 조회
Locale locale
: Locale 정보 조회
@RequestHeader MultiValueMap<String, String> headerMap
: 모든 HTTP 헤더를 multivaluemap 형식으로 조회
: MultiValueMap : 하나의 키에 여러 값을 받을 수 있는 것
@RequestHeader("host") String host
: 특정 HTTP 헤더를 조회
@CookieValue(value = "myCookie", required = false) String cookie
: 특정 쿠키를 조회
: HTTP 메시지를 통하여 클라이언트에서 서버로 메시지를 전달할 때는 3가지 방법이 있다.
1. GET - 쿼리 파라미터
2. POST - HTML 폼
3. HTTP message body에 데이터를 직접 담아서 요청
: GET 쿼리 파리미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이조회할 수 있다.
이것을 간단히 요청 파라미터(request parameter) 조회라 한다.
: 단순히 HttpServletRequest가 제공하는 방식으로 요청 파라미터를 조회가 가능하다.
: 리소스는 /resources/static 아래에 두면 스프링 부트가 자동으로 인식한다
@Slf4j
@Controller
public class RequestParamController {
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
int age = Integer.parseInt(req.getParameter("age"));
log.info("username ={}, age ={}", username, age);
resp.getWriter().write("ok");
}
// @RequestParam 1번
@ResponseBody //문자 반환을 위해 restcontroller로 바꾸든지 아니면 이 어노테이션을 쓰면 된다.
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberage){
log.info("username = {}, age ={}", memberName, memberage);
return "pitchu";
}
// @RequestParam 2번
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username, //변수명과 똑같으면 생략이 가능하다.
@RequestParam int age){
log.info("username = {}, age ={}", username, age);
return "pitchu";
}
// @RequestParam 3번
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age){
log.info("username = {}, age ={}", username, age); // string int Integer등의 단순 타입이면 @RequestParam도 생략이 가능하다
return "pitchu";
}
// @RequestParam 4번
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username, // true면 꼭 들어와야됨
@RequestParam(required = false) Integer age){ // int에 null 들어갈 수 없고 integer는 들어갈 수 있다.
log.info("username = {}, age ={}", username, age);
return "pitchu";
}
// @RequestParam 5번
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username, // 파라미터에 값이 없을 경우 defaultValue를 사용하면 기본 값을 적용한다.
@RequestParam(required = false, defaultValue = "-1") int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
// @RequestParam 6번
@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";
}
// @ModelAttribute 1번 hello data 객체를 생성 helloData 객체의 프로퍼티를 찾아서 setter를 호출 후 파라미터의 값을 바인딩
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
// @ModelAttribute 2번
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) { //@ModelAttribute도 생략 가능 ,@RequestParam은 단순 타입,@ModelAttribute는 나머지
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
}
public class RequestBodyStringController {
// HTTP 요청 메시지 - 단순 텍스트 1번
@PostMapping("/request-body-string-v1")
public void requestBodyString(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");
}
// HTTP 요청 메시지 - 단순 텍스트 2번
/**
* InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
* OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
*/
@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");
}
// HTTP 요청 메시지 - 단순 텍스트 3번
/**
* HttpEntity: HTTP header, body 정보를 편라하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
// HTTP 요청 메시지 - 단순 텍스트 4번 : 제일 많이 쓰임
@ResponseBody// 응답 결과를 바디에 담아 직접 전달
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) { // 편하게 HTTP 메시지 바디 정보 조회 가능
log.info("messageBody={}", messageBody);
return "ok";
}
}
private ObjectMapper objectMapper = new ObjectMapper();
@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); // message body를 받아서
log.info("messageBody={}", messageBody);
HelloData data = objectMapper.readValue(messageBody, HelloData.class);// object Mapper를 사용하여 객체로 변환한다.
log.info("username={}, age={}", data.getUsername(), data.getAge());
response.getWriter().write("ok");
}
// @RequestBody를 이용해서 message body를 받고,
// 객체 또한 objectMapper 를 쓰지 않고 변경이 가능하다.
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
JSON의 경우 HTTP 요청시에 content-type이 application/json인지 확인해야 함.
: 응답데이터를 만드는 방법은 아래의 세 가지 이다.
- 정적 리소스
- 뷰 템플릿 : 동적인 HTML
- HTTP 메시지 사용
: 다음 디렉토리에 넣는다.
src/main/resources/static
: 경로는 다음과 같다.
src/main/resources/templates
: JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나(@RequestBody) 쓰는 경우(@ResponseBody) 사용한다.
: 다음과 같은 종류가 있고 위에서 부터 순서대로 조건을 만족하는지 확인한다.
바이트 배열 컨버터
문자열 컨버터
Resource 컨버터
Form 컨버터 (폼 데이터 to/from MultiValueMap)
(JAXB2 컨버터)
(Jackson2 컨버터)
(Jackson 컨버터)
(Gson 컨버터)
(Atom 컨버터)
(RSS 컨버터)
이중 하나로 작동 예시를 들자면, Jackson2 타입의 경우에는 조건이 아래와 같다. 위에서 부터 탐색하면서
클래스 타입 : 객체 또는 HashMap
미디어타입: application/json 관련
요청 ex) @RequestBody HelloData Data
// canread를 통해 조건 충족하는가? read를 통하여 객체 생성하고 반환한다.
응답 ex) @ResponseBody return helloData
쓰기 미디어 타입 : application/json 관련
// canwrite를 통해 조건 충족하는가?
// 만족하면 write이용하여 메시지 바디에 데이터 생성

Argument Resolver 때문에 다양한 파라미터 처리가능 ReturnValueHandler는 응답값을 반환하고 처리한다. 위의 두가지를 처리하는데에 앞서 배운 HTTP메시지 컨버터가 사용된다.
@Data //되도록이면 @Getter @ Setter를 사용해라 @Data의 경우 도메인 모델에 사용하기에는 변수가 많아(포함된 어노테이션이 많아) 위험하다.
public class Item {
private Long id;
private String itemName;
private Integer price; // NULL 값도 가정을 한다.
private Integer quantity; // NULL 값도 가정을 한다.
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Repository
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>();
// static 여러개가 동시에 접근하는 경우 Hashmap 쓰면 안된다.
private static long sequence = 0L;
// 이것또한 automic long 등 사용하는게 나음
// 다만 작은 프로젝트니 그냥 사용하였다.
public Item save(Item item){ // item 저장하기
item.setId(+sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id){
return store.get(id);
}
public List<Item> findAll(){
return new ArrayList<>(store.values());
}
public void update(Long itemId, Item updateParam){
// 아이템과 관련된 파라미터를 넣으면 업데이트가 됨
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());// updateParam은 별도의 객체를 만드는게 맞음
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore(){
store.clear();
}
}
: 테스트 작성시엔 언제나 given when then을 유의할 것
@AfterEach
void afterEach() {// 매번 테스트 실행 된 후에 값을 초기화
itemRepository.clearStore();
}
: /resources/static에 HTML을 넣어두면 실제 서비스에서도 공개된다.
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
//@Autowired // 1. 생성자 하나 있으면 @Autowired는 생략 가능
//public BasicItemController(ItemRepository itemRepository) {
// this.itemRepository = itemRepository; // 2. lombok 의 @RequiredArgsConstructor 사용하면 final 붙은거는 생략가능
//}
}
<html xmlns:th="http://www.thymeleaf.org">
: 그후 반환하는 view를 만드는데, 앞서 넣었던 HTML파일들을 타임리프를 사용해서 동적으로 바꿔야 한다.
: thymeleaf는 그대로 볼때는 href, 뷰 템플릿을 거치면 th:href의 값이 href로 대치되면서 동적으로 변경한다.
: HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.
: 더하기 없이 편리하게 결합하는 기능
: th:each 를 사용한다.
: ${}
: 모델에 포함된 값이나 타임리프 변수로 선언한 값을 조회할 수 있다.
: 내용의 값을 th:text 값으로 변경
: @{}
: 쿼리 파라미터를 이용 가능하다.
th:value="${item.itemName}
: 이 문장의 경우엔, value 속성을 th:value 속성으로 변경한다.
: 상품을 등록할 수 있는 폼을 보여주는 것
// 같은 URL이더라도 HTTP 메소드로 기능을 구분한다. (등록 폼과 등록 처리를 깔끔하게)
@GetMapping("/add")
public String addFrom() {
return "basic/addForm";
}
@PostMapping("/add")
public String save() {
return "basic/addForm";
}
: 메시지 바디에 쿼리 파라미터 형식으로 전달하였다 이를 처리하기 위해 @RequestParam 사용한다.
: 상품 등록 처리를 위해서는 두가지 방법이 있는데
@PostMapping("/add")
public String additemV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
@PostMapping()//"/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
//ModelAttribute 가 자동으로 객체 만들고 set을 호출하기 떄문에 4문장 제거 가능
itemRepository.save(item);
// ModelAttribute의 내용으로 "item" 담김
//model.addAttribute("item", item);
return "basic/item";
}
@ModelAttribute 의 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다. 이때 클래스의 첫글자만 소문자로 변경해서 등록한다.
: 등록할 때는 뷰 템플릿을, 수정할때는 redirect를 사용하였다.
: 새로고침은 서버에 전송된 데이터를 다시 전송하는데, 이 때문에 지금까지 만든 데이터가 계속 쌓이게 되는 결론에 닿는다.
: 이 문제를 해결하기 위해 PRG의 단계를 사용한다. 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라 상품 상세 화면으로 리다이렉트를 호출해주면 된다. 리 다이렉트 때문에 상품 저장 후에 실제 상품 화면으로 다시 이동하고 마지막에 호출한내용이 GET/items/{id}가 된다.

return "redirect:/basic/items/" + item.getId();
: 다만 위의 방식으로 사용 시 숫자가 아닌 문자 등을 사용 시 URL 인코딩 문제가 있음
: RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리해준다
: 다음과 같은 예문이 있다고 가정하자.
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
return "redirect:/basic/items/{itemId}" 이 문장에서
: redirectAttributes.addAttribute로 넣었던 값중에서 return에 사용된 {}안의 값이 있을 경우, 해당 값으로 넣어주고 return에 사용되지 않은 나머지 값들은 리다이렉트 될 때 쿼리파라미터로 넘겨준다.
: "/basic/items/itemid?status=true"가 반환된다.