kiwi

서블릿부터 스프링 mvc까지의 변화 과정

by 키위먹고싶다

1. 일반 서블릿을 사용할 때 

@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);
    }
}

일반 서블릿을 사용하면 HttpServlet을 상속받고 service메서드를 오버라이딩함.

 

@WebServlet 어노테이션의 

name속성은 서블릿 이름이며

urlPatterns속성은 url매핑

 

HTTP요청을 통해 매핑된 url이 호출되면 서블릿 컨테이너는

service메서드 실행. 

 

HttpServletRequest는 HTTP메세지 정보를 쉽게 얻기 위해 사용하는것임.

이 객체가 HTTP요청 메시지 정보를 파싱함. 

 

Get

Get으로 요청데이터 전달할때는 쿼리 파라미터를 사용함

/url?username=babo&age=23 => 추가 파리미터는 '&'로 구분

메시지 바디가 없음. 그래서 content-type사용 안함. 검색이나 페이징이나 필터에서 사용함

request.getParameter("username")하면 get으로 넘어온 username정보인 babo라는 값의 파라미터 정보를 String으로 얻을 수 있음 

 

POST

html의 form을 사용해서 주로 회원가입이나 상품주문등에 사용

<form action="/request-param" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>
</form>

여기서 알아야 하는건 contentType이 application/x-www-form-urlencoded면

get과 마찬가지로 쿼리 파라미터형식으로 

username=babo&age=23 메시지 바디에 만들어진다.(get과 다르게 메시지 바디에 생김)

 

HTTP 메시지 바디에 직접 데이터 담아서 요청

  • text : 콘텐츠 타입 text/plain
  • json : 콘텐츠 타입 application/json

 

 

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("MemberSaveServlet.service");
        String username = req.getParameter("username");
        int age = Integer.parseInt(req.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");
        PrintWriter w = resp.getWriter();

        w.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" +
                "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                " <li>id="+member.getId()+"</li>\n" +
                " <li>username="+member.getUsername()+"</li>\n" +
                " <li>age="+member.getAge()+"</li>\n" +
                "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" +
                "</body>\n" +
                "</html>");
    }
}

서블릿을 사용해서 동적으로 서비스를 제공하지만 자바 코드로 html을 만들려면 보는것처럼 힘들다. 코드가 복잡해지고 관리도 힘들다. html문서에 동적으로 변경해야하는 부분만 자바 코드로 넣는게 편함. 그래서 나온것이 템플릿 엔진임. 


2. jsp사용

- jstl라이브러리 이용

<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.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>

 

JSP는 서버 내부에서 서블릿으로 전환된다. 실행시에 .jsp까지 적어줘야 함. 

<% ~~ %>사이에 자바 코드 사용 가능.

<ul>
    <li>id=<%=member.getId()%></li>
    <li>id=<%=member.getUsername()%></li>
    <li>id=<%=member.getAge()%></li>
</ul>

<%= ~~~ %>사이에 출력 가능

 

서블릿으로만 하는 자바 코드와 비교하면 Html을 중심으로 하고 중간에 동적으로 변경되는 부분만 자바 코드를 사용한다. 

 

서블릿만 이용할때보다 편리하지만 비지니스로직과 html이 뒤섞여 있고 너무 많은 일을 jsp혼자 다 한다. 

그리고 회원 레포지토리등 너무 노출되어있다. 

ui나 비지니스 로직의 변경라이프 사이클이 너무 다른데 둘중 하나를 수정해야 할때 수천줄의 jsp나 서블릿 코드를 봐야한다. 그리고 jsp같은 뷰 템플릿은 화면을 랜더링하는 것에 최적화 되어 있어서 이 부분만 처리하는게 효과적이다. 

 


3. MVC패턴

Model View Controller

지금까지 하나의 서블릿이나 jsp로 처리하던 것을 Controller와 View라는 영역을 나눈것.

 

Controller : Http요청을 받아서 파라미터를 검증하고 비지니스 로직 실행. 뷰의 전달할 결과데이터를 model에 담음.

Model : 뷰에 출력할 데이터를 담아 둠. 모델덕분에 뷰는 비지니스로직이나 데이터 접근을 몰라도 되고 화면에 집중함.

View : 모델에 담겨있는 데이터를 사용해서 화면을 생성하는 것에 집중. HTML을 만듬. 

 

 

/WEB-INF경로 안에 있으면 직접 JSP를 호출할 수 없음 . => 컨트롤러를 통해 호출.

 

redirect vs forward

리다이렉트는 실제 클라이언트가 응답을 받고 redirect경로를 다시 요청해서 클라이언트가 인지할 수 있으며 실제 url도 변경된다. 포워드는 서버 내부에서 일어나는 호출이라서 클라이언트가 인지하지 못한다. 

 

 

@WebServlet(name = "mvcMemberSaveServlet" , urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {

    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String username = req.getParameter("username");
        int age = Integer.parseInt(req.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        //Model에 데이터를 보관한다.
        req.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save.jsp";

        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }

}

request가 제공하는 setAttribute를 사용하면 request를 뷰에 전달할 수 있음. request가 model이 되는것임. 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</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") %>로 꺼낼 수 있음. 

 

<%@ 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>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
    </c:forEach>
    </tbody>
</table>
</body>
</html>

members에는 멤버리스트가 들어있는데 Jsp가 제공하는 taglib기능을 이용해서 출력할 수 있다. 

 

 

MVC컨트롤러 단점

 

1. 포워드가 계속 중복됨

2. viewPath 중복 부분 발생. 

3. response잘 사용 안함.

4. 컨트롤러의 공통 처리가 어려움. 

 

 

프론트 컨트롤러의 역할

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")

public class FrontControllerServletV4 extends HttpServlet {
    private Map<String, ControllerV4> controllerV4Map = new HashMap<>();

    public FrontControllerServletV4() {
        controllerV4Map.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerV4Map.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerV4Map.put("/front-controller/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV4.service");

        String requestURI = request.getRequestURI();

        ControllerV4 controller = controllerV4Map.get(requestURI);
        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        HashMap<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();    //추가

        String viewName = controller.process(paramMap, model);

        MyView view = viewResolver(viewName);
        view.render(model, request, response);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private HashMap<String, String> createParamMap(HttpServletRequest request) {
        HashMap<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

컨트롤러

public class MemberSaveControllerV4 implements ControllerV4 {

    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";
    }
}

로직을 실행하고 뷰의 논리 이름만 반환. 

 

뷰 랜더 역할

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));
    }
}

 

컨트롤러가 넘겨준 뷰path로 실제 물리 뷰 찾기. 

 

 

1. 생성자에 모든 컨트롤러 map에 넣기

2. request정보를 map으로 만들어서 컨트롤러 실행. 

3. 컨트롤러의 리턴타입이 뷰의 이름(String)반환.

4. view를 render할때 모델을 set하는과정과 포워드하는과정. 

 

 

프론트 컨트롤러 특징

프론트 컨트롤러 서블릿 하나로 클라이언트 요청을 받고 요청에 맞는 컨트롤러를 호출, 

스프링 웹mvc의 핵심이 프론트 컨트롤러이고

DispatcherServlet이 프론트 컨트롤러 패턴으로 구현되어있음.

 

다른 종류의 컨트롤러를 실행하기 어렵다. 그러므로

다른 인터페이스를 구현한 컨트롤러를 사용하기 위해서 어뎁터를 사용한다. 

 

핸들러 : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경

핸들러 어댑터 : 중간에 어댑터 역할을 하는 애인데 어뎁터 역할 덕분에 다른 종류의 컨트롤러를 호출할 수 있다. 

 

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handler(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;

        HashMap<String, String> paramMap = createParamMap(request);
        HashMap<String, Object> model = new HashMap<>();

        String viewName = controller.process(paramMap, model);

        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

    private HashMap<String, String> createParamMap(HttpServletRequest request) {
        HashMap<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

먼저 어떤 controller용 어댑터인지 (정확히 말하면 이 어댑터가 처리할 수 있는 컨트롤러인지) 확인하고 

이 어댑터에서 컨트롤러를 실행한다. ModelView를 반환한다. 

 

프론트 컨트롤러 역할 

@WebServlet(name = "frontControllerV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    public FrontControllerV5() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }

    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    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());
    }

    @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 mv = adapter.handler(request, response, handler);

        String viewName = mv.getViewName();//논리 이름 new-form
        MyView view = viewResolver(viewName);

        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");
    }
}

1.생성자에 핸들러 매핑과 핸들러 어뎁터를 등록한다.

2. getHandler메서드에서 핸드러매핑을 통해 핸들러를 찾는다.

3. getHandlerAdapter메서드에서 핸드러를 처리할 수 있는 어뎁터를 찾는다. 

4. 어뎁터의 handler에서 컨트롤러를 실행한다. 

5. 컨트롤러는 뷰의 이름만 반환하지만 어댑터에서 ModelView형식으로 맞추어 변환한다. 


4. spring MVC

DispatcherServlet도 부모클래스에서 HttpServlet을 상속받으므로 서블릿으로 등록가능.

 

동작순서

1. 핸들러 조회: 핸들러 매핑을 통해 요청된 url에 매핑된 핸들러를 조회한다.

2. 핸들러를 실행 할 수 있는 어댑터를 조회한다.

3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다.

4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.

5. ModelAndView반환 : 핸들러 어댑터는 실제 핸들러가 반환하는 정보를 ModelAndview로 변환해서 반환한다.

6. ViewResolver호출 : 뷰 리졸버를 찾고 호출한다. jsp의 경우 InternalResourceViewResolver가 자동으로 등록되고 사용.

7. View반환 : 뷰 리졸버는 뷰의 논이 이름을 물리 이름으로 바꾸고 랜더링 하는 뷰 객체를 반환한다.

8. 뷰 랜더링 : 뷰를 통해서 뷰를 랜더링한다. 

 

핸들러 매핑과 핸들러 어댑터

 

Controller인터페이스 (어노테이션 말고 과거에 사용함)

@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;
    }
}

컴포넌트로 빈을 등록하고 빈의 이름으로 url을 매핑함.

1. 핸들러 매핑[BeanNameUrlHandlerMapping] -> 이 과정에서 이 컨트롤러를 찾을수 있어야함.(스프링 빈의 이름으로 핸들러 매핑을 통해 핸들러 찾기)

2. 핸들러 어뎁터[SimpleControllerHandlerAdapter] -> 핸들러 매핑을 통해 찾은 핸들러(컨트롤러 인터페이스)를 실행할 수 있는 핸들러 어뎁터를 찾고 실행해야함.

 

/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;

/**
 * Adapter to use the plain {@link Controller} workflow interface with
 * the generic {@link org.springframework.web.servlet.DispatcherServlet}.
 * Supports handlers that implement the {@link LastModified} interface.
 *
 * <p>This is an SPI class, not used directly by application code.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @see org.springframework.web.servlet.DispatcherServlet
 * @see Controller
 * @see HttpRequestHandlerAdapter
 */
public class SimpleControllerHandlerAdapter implements HandlerAdapter {

   @Override
   public boolean supports(Object handler) {
      return (handler instanceof Controller);
   }

   @Override
   @Nullable
   public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
         throws Exception {

      return ((Controller) handler).handleRequest(request, response);
   }

   @Override
   @SuppressWarnings("deprecation")
   public long getLastModified(HttpServletRequest request, Object handler) {
      if (handler instanceof LastModified) {
         return ((LastModified) handler).getLastModified(request);
      }
      return -1L;
   }

}

supports메서드로 넘어온 핸들러를 Controller가 지원함.

 

 

HttpRequestHandler사용(얘도 과거 사용)

@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");
    }
}

얘도 빈으로 등록된 애를 매핑하기 때문에 BeanNameUrlHandlerMapping로 매핑함

다음에 핸들러 어뎁터를 찾아야 하는데 이때는 SimpleControllerHandlerAdpater가 아니라 HttpRequestHandlerAdapter를 사용함

 

public class HttpRequestHandlerAdapter implements HandlerAdapter {

   @Override
   public boolean supports(Object handler) {
      return (handler instanceof HttpRequestHandler);
   }

   @Override
   @Nullable
   public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
         throws Exception {

      ((HttpRequestHandler) handler).handleRequest(request, response);
      return null;
   }

   @Override
   @SuppressWarnings("deprecation")
   public long getLastModified(HttpServletRequest request, Object handler) {
      if (handler instanceof LastModified) {
         return ((LastModified) handler).getLastModified(request);
      }
      return -1L;
   }

}

supports메서드로 넘어온 핸들러를 HttpRequestHandler가 지원함.

 

정리

Controller인터페이스

1. 핸들러 매핑 : BeanNameUrlHandlerMapping

2. 핸들러 어뎁터 : SimpleControllerHandlerAdapter

 

HttpRequestHandler

1. 핸들러 매핑 : BeanNameUrlHandlerMapping

2. 핸들러 어뎁터 : HttpRequestHandlerAdapter

 

 

 

뷰 리졸버

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

추가(물리 주소)

 

스프링부터는 InternelResourceViewResolver라는 뷰 리졸버를 자동등록하는데 이때 저 설정정보를 사용함

실제 주소가 [prefix + 'jsp이름' + suffix]가 되는것. 

 

 

스프링이 제공하는 컨트롤러는 어노테이션 기반으로 동작해서 매우 유연하고 실용적.

@RequestMapping

  @RequestMappingHandlerMapping

  @RequestMappingHandlerAdapter

 

우선순위가 가장 높음. 

 

 

@Controller의 역할 2가지

1. 컴포넌트 대상이 돼서 자동으로 빈 등록. 

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process(){
        return new ModelAndView("new-form");
    }
}

@Component가 있으면 컴포넌트 대상이 돼서 자동으로 스프링 빈으로 등록됐던거 기억.

@Controller안에 @Componet있음

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

   /**
    * The value may indicate a suggestion for a logical component name,
    * to be turned into a Spring bean in case of an autodetected component.
    * @return the suggested component name, if any (or empty String otherwise)
    */
   @AliasFor(annotation = Component.class)
   String value() default "";

}

2. RequetMappingHandlerMapping으로 매핑 시작

 

@RequestMapping : 요청 정보를 매핑. 해당url이 호출되면 이 메서드 호출. 메서드 이름은 맘대로. -> 클래스 레벨에 있어도 됨. 대신 @Component도 써야함. 

ModelAndView : 모델과 뷰 정보를 담아서 반환. 

 

 

비슷한 컨트롤러 합치기

@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm(){
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save");
        mv.addObject("member", member);
        return mv;
    }

    @RequestMapping
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }
}

공통 주소를 RequestMapping을 클래스 레벨에 붙혀서 사용할 수 있음.

 

 

 

자 이제 드디어 내가 알던 형식이 나옴.. 반가워서 눈물이 난다 

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public String newForm(){
        return "new-form";
    }

    @RequestMapping("/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";
    }

    @RequestMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        return "members";
    }
}

ModelAndView를 리턴하는게 아니라 String으로 view이름을 리턴한다. (논리이름. 프로퍼티스에 설정해놔서 앞주소 뒷주소 다 붙어서 이동)

또 save메서드를 보면 @RequestParam을 사용하는데 request를 파싱해서 정보를 얻지 않아도 된다. 

@RequestParam("username") == request.getParameter("username")과 같음 (Get쿼리 파라미터, Post form 둘다 지원. )

또 추가된것이 Model을 사용할 수 있다. model에 setAttribute에 정보를 넣어서 save.jsp로 이동. 

 

 

@RequestMapping(value = "/new-form", method = RequestMethod.GET)
public ModelAndView newForm(){
    return new ModelAndView("new-form");
}

 

@GetMapping("/new-form")
public ModelAndView newForm(){
    return new ModelAndView("new-form");
}

@RequestMapping의 속성인 method가 Get이면 아래 처럼 @GetMapping으로 해도 된다. Post면 @GetPost

 

 

War말고 Jar로 사용

War는 주로 외부에 배포할때 사용

 

@RestController와 Logger

@RestController //HTTP메시지 body에 return 문자를 넣어버림
public class LogTestController {

    private final Logger log = LoggerFactory.getLogger(getClass()); //내 클래스 지정

    @RequestMapping("/log-test")
    public String logTest(){
        String name = "Spring";

        System.out.println("name = " + name);
        log.info(" info log={}", name);

        return "ok";
    }
}

@RestController는 "OK"를 메시지 바디에 넣어버려서 html화면에 OK라고 찍힘. 

@Controller는 반환값이 String이면 뷰의 이름으로 인식돼서 뷰가 랜더링 된다.

@RestController는 반환 값으로 뷰를 찾는것이 아니라 HTTP메시지에 바로 입력된다. 

 

name = Spring ==> System.out.println으로 찍은거 
2022-01-22 13:32:24.459  INFO 32884 --- [nio-8099-exec1] hello.springmvc.basic.LogTestController  :  info log=Spring ==> log.info로 찍은결과

 

[시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지] 다양한 정보가 찍힘.

 

System.out.println("name = " + 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);

name = Spring
2022-01-22 13:39:50.440 DEBUG 38240 --- [nio-8099-exec-1] hello.springmvc.basic.LogTestController  : trace log=Spring
2022-01-22 13:39:50.442  INFO 38240 --- [nio-8099-exec-1] hello.springmvc.basic.LogTestController  :  info log=Spring
2022-01-22 13:39:50.442  WARN 38240 --- [nio-8099-exec-1] hello.springmvc.basic.LogTestController  :  info log=Spring
2022-01-22 13:39:50.442 ERROR 38240 --- [nio-8099-exec-1] hello.springmvc.basic.LogTestController  :  info log=Spring

#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug

로깅에는 레벨이 있어서 그 debug로 설정하면 그 하위 애들까지만 log찍히고 trace는 찍히지 않음. 기본은 info부터

 

운영시스템에서는 중요한 정보만 log로 남겨야함. System.out.println은 모든 정보가 다 찍혀서 수천명의 사람이 요청했을 때 매우 많은 정보가 찍힐 수 있는데 그러면 안됨. 그래서 log를 사용.

 

개발 서버에서는 debug로 운영서버에서는 info로 각각 용도에 맞춰서 사용. 로컬 피씨에서는 trace로

Trace>debug>info>warn>error순으로 레벨.

 

@Slf4j

사용하면 롬복이 자동으로 log사용할 수 있게 함. 

 

 

요청 매핑

@Slf4j
@RestController
public class MappingController {

    @RequestMapping("/hello-basic")
    public String helloBasic(){
        log.info("helloBasic");
        return "ok";
    }
}

@RequestMapping("hello-basic")

/hello-basic URL이 호출되면 이 메서드가 실행되도록 매핑한다.배열로 여러개의 값을 넣어도 됨. 

만약 method속성에 HTTP메서드를 지정해주지 않으면 메서드와 무관하게 호출된다.  

@Slf4j
@RestController
public class MappingController {

    @RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
    public String helloBasic(){
        log.info("helloBasic");
        return "ok";
    }
}

POST로 호출하면 405에러. 

 

@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
    log.info("mapping-get-v2");
    return "ok";
}

@GetMapping에는

@RequestMapping(method = RequestMethod.GET)

이 붙어 있음.

 

PathVariable

/**
 * PathVariable 사용
 * 변수명이 같으면 생략 가능
 * @PathVariable("userId") String userId -> @PathVariable userId
 */
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){
    log.info("mappignPath userId={}", data);
    return "ok";
}

경로 변수. 만약에 url에 /mapping/userA 라고 오면 userA를 사용할 수 있음. 

최근 HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호.

 

/mapping/{userId} ==> /mapping/userA (경로변수)

/mapping ==> /mapping?userId=userA (쿼리파라미터)

 

/**
 * PathVariable 사용 다중
 */
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
        orderId) {
    log.info("mappingPath userId={}, orderId={}", userId, orderId);
    return "ok";
}

다중 사용 가능.

 

특정 파라미터 조건매핑

/**
 * 파라미터로 추가 매핑
 * params="mode",
 * params="!mode"
 * params="mode=debug"
 * params="mode!=debug" (! = )
 * params = {"mode=debug","data=good"}
 */
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
    log.info("mappingParam");
    return "ok";
}

파라미터 정보로 mode=debug 넘어오지 않으면 메서드 실행 안됨. 

 

특정 해더 조건 매핑

/**
 * 특정 헤더로 추가 매핑
 * headers="mode",
 * headers="!mode"
 * headers="mode=debug"
 * headers="mode!=debug" (! = )
 */
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
    log.info("mappingHeader");
    return "ok";
}

헤더 정보에 mode=debug없으면 실행 안됨. params와 비슷

 

미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

/**
 * Content-Type 헤더 기반 추가 매핑 Media Type
 * consumes="application/json"
 * consumes="!application/json"
 * consumes="application/*"
 * consumes="*\/*"
 * MediaType.APPLICATION_JSON_VALUE
 */
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
    log.info("mappingConsumes");
    return "ok";
}

HTTP요청 content-type헤더를 기반으로 미디어 타입 매핑함. 조건에 맞지 않으면 415상태코드 반환.

 

미디어 타입 조건 매핑 - HTTP 요청 Accept, produce

/**
 * Accept 헤더 기반 Media Type
 * produces = "text/html"
 * produces = "!text/html"
 * produces = "text/*"
 * produces = "*\/*"
 */
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
    log.info("mappingProduces");
    return "ok";
}

헤더 accept가 text/html이 아니면 406에러

 

 

요청 매핑 API 예시

@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;
    }
}

회원 목록 조회: GET /mapping/users
회원 등록: POST /mapping/users
회원 조회: GET /mapping/users/id1
회원 수정: PATCH /mapping/users/id1
회원 삭제: DELETE /mapping/users/id1

 

HTTP요청 파라미터 - 쿼리 파라미터, HTML FORM

@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");

}

쿼리 파라미터 : /request-param-v1?username=tt&age=12

 

HTML FORM

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/request-param-v1" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>
</form>
</body>
</html>

여기서는 단순히 request.getParameter

 

 

HTTP요청 파라미터 - @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";
}

@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
        @RequestParam String username,
        @RequestParam int age){

    log.info("username={}, age={}", username, age);
    return "ok";
}

@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age){
    log.info("username={}, age={}", username, age);
    return "ok";
}

@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
        @RequestParam(required = true) String username,
        @RequestParam(required = false) Integer age){
    log.info("username={}, age={}", username, age);
    return "ok";
}

@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
        @RequestParam(required = true, defaultValue = "guest") String username,
        @RequestParam(required = false, defaultValue = "-1") int age){
    log.info("username={}, age={}", username, age);
    return "ok";
}

@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";
}

 

 

HTTP요청 파라미터 - @ModelAttribute

@Data
public class HelloData {

    private String username;
    private int age;
}
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttribute(@ModelAttribute HelloData helloData) {

    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    log.info("helloData={}", helloData);
    return "ok";
}

@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttribute2(HelloData helloData) {

    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    log.info("helloData={}", helloData);
    return "ok";
}

@ModelAttribute는 요청파라미터를 받아서 그 객체를 생성하고 객체에 값을 넣어주는 과정을 완전히 자동화해주는 기능을 제공한다. 생략도 가능하다. 

 

HTTP요청 메시지 - 단순 텍스트

메시지 바디에 직접 데이터 담아서 요청


@Slf4j
@Controller
public class RequestBodyStringController {

    @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");
    }

    @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");
    }

    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {

        String messageBody = httpEntity.getBody();
        log.info("messageBody={}", messageBody);

        return new HttpEntity<>("ok");
    }

    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {

        log.info("messageBody={}", messageBody);

        return "ok";
    }
}

 

@RequestParam, ModelAttribute는 사용할 수 없다. 

바디의 메시지를 InputStream으로 읽을 수 있다. 

 

HttpEntity : Http 헤더와 바디 정보를 편리하게 조회하고 직접 반환가능.

 

@RequestBody : 요청 메시지바디 정보를 조회할 수 있고 

@ResponseBody :  응답 메시지바디에 직접 데이터를 넣어서 반환한다.

 

요청 파라미터 : @RequestParam, @ModelAttribute

HTTP MESSAGE BODY를 직접 조회 : @RequestBody

 

 

 

'spring' 카테고리의 다른 글

Validation  (0) 2022.02.03
메시지 국제화  (0) 2022.02.01
타임리프 기본 문법  (0) 2022.01.28
스프링 mvc 웹 페이지 만들기  (0) 2022.01.26
tomcat을 이용한 Servlet에 대한 설명과 spring에서의 사용  (0) 2022.01.08

블로그의 정보

kiwi

키위먹고싶다

활동하기