Contents

스프링 mvc 1편 정리

스프링 mvc 1편을 듣고 정리한 내용입니다.

1 Servlet이해하기

서블릿을 자동 등록하는 경우, @ServletComponentScan어노테이션을 사용한다.

예제를 보면서 이해해보자

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        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);
    }
}

서블릿을 보면 이렇게 request, response로 구성되어 있으며,
파라미터를 추출해낼 수 있고,
response의 contentType, CharacterEncoding을 설정 가능하며
write로 원하는 내용을 뿌려줄 수 있다

urlPatterns로 원하는 uri를 설정 가능한 걸 볼 수 있다

또, 아래처럼 이렇게 원하는 정보를 뽑아낼 수 있다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
System.out.println("request.getMethod() = " + request.getMethod()); //GET
System.out.println("request.getProtocal() = " + request.getProtocol()); //HTTP/1.1
System.out.println("request.getScheme() = " + request.getScheme()); //http
// http://localhost:8080/request-header
System.out.println("request.getRequestURL() = " + request.getRequestURL());
// /request-test
System.out.println("request.getRequestURI() = " + request.getRequestURI());
//username=hi
System.out.println("request.getQueryString() = " + request.getQueryString());
System.out.println("request.isSecure() = " + request.isSecure()); //https사용 유무

header정보를 아래처럼 iterator로 뽑아내는 등 다양한 정보를 확인할 수 있다!!

1
request.getHeaderNames().asIterator().forEachRemaining(headerName -> System.out.println("headerName = " + headerName));

요청 body의 json 뽑아서 객체로 만들기!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@WebServlet(name = "requestBodyJsonServelt", 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.username = " + helloData.getUsername());
        resp.getWriter().write("ok");
    }
}

이렇게 ServletInputStream을 뽑아낸 후,
copyToString으로 body를 뽑아낸다.
ObjectMapper를 사용해서 json 내용을 객체로 변환해서 볼 수 있다

강의 중, 동시성 문제 고려 필요할 때에는 ConcurrentHashMap, AtomicLong 사용 고려 필요

+result에 2개의 정보가 포함되어있는지 테스트할 때

1
assertThat(result).contains(member1, member2);

JSP?

jsp를 보면서 이해해보자

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    MemberRepository memberRepository = MemberRepository.getInstance();
    List<Member> members = memberRepository.findAll();
%>
<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>

이렇게 위에 자바에서 사용되는 코드가 있고,
아래에 html 관련 코드가 있다
+html, head 이런 태그는 생략!

딱 보기만 해도 jsp를 사용하는 것에 대해 고민할 것이란 걸 알 수 있다!

MVC

강의 들으면서 하나 더 알게 된 건, “/WEB-INF/~“에 있는 부분은, 컨트롤러에서 뷰로 이동할 때 사용한다는 것이었다

1
2
3
4
5
6
//서블릿 관련 코드
request.setAttribute("member", member);

String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

requestDispatcher에 viewPath를 넣고
dispatcher.forward()로 화면을 뿌려준단 걸 알 수 있다
-> 여기서 중복이 계속 발생할 수 있다(viewPath, forward 부분!) 또, setAttribute로 model에 데이터를 보관할 수 있다

jsp의 경우 아래처럼 forEach로 members에 대해
forEach로 item을 뽑아오는 걸 알 수 있다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<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>

중복 개선을 위한 ServletV1

ServletV1에서 uri에 맞게 Controller를 매핑해준다.
HttpServlet을 구현한 것이다보니까 service로 process를 해준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@WebServlet(name = "ServletV1", urlPatterns = "/controller/v1/*")
public class ServletV1 extends HttpServlet {

    private Map<String, ControllerV1> controllerV1Map = new HashMap<>();

    public ServletV1() {
        controllerV1Map.put("/controller/v1/members/new-form", new MemberFormControllerV1());
        controllerV1Map.put("/controller/v1/members/save", new MemberSaveControllerV1());
        controllerV1Map.put("/controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerV1Map.get(requestURI);
        controller.process(request, response); //아까 계속 중복되었던 부분(requestDispatcher, forward)
    }
}

위의 process는 controller마다 구현해주는 부분으로,
아까 중복되었던 부분을 포함한다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class MemberSaveControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //로직 (생략)
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

그래도 컨트롤러마다 다 위처럼 중복된 부분을 가져야함을 알 수 있다

중복 개선을 위한 ServletV2

그래서 뷰라는 클래스를 만들고,
중복되는 부분을 render에 담는다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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);
    }
}

서블릿을 보면 아까와 달라보이지 않지만,
process 밑에 렌더가 추가됨을 알 수 있다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@WebServlet(name = "ServletV2", urlPatterns = "/controller/v2/*")
public class ServletV2 extends HttpServlet {

    private Map<String, ControllerV2> controllerV2Map = new HashMap<>();

    public ServletV2() {
        controllerV2Map.put("/controller/v2/members/new-form", new MemberFormControllerV2());
        controllerV2Map.put("/controller/v2/members/save", new MemberSaveControllerV2());
        controllerV2Map.put("/controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

        ControllerV2 controller = controllerV2Map.get(requestURI);
        MyView view = controller.process(request, response);
        view.render(request, response);
    }
}

그럼 컨트롤러는 이렇게 해당 경로를 리턴해줌을 알 수 있다

1
2
3
4
5
6
7
public class MemberFormControllerV2 implements ControllerV2 {

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

괜찮은 것 같은데,
서블릿 종속성이 여전히 존재한다는 단점이 있다

서블릿 종속성 개선을 위한 ServletV3

아까 MyView와 달리,
viewName에다가 model이 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Getter @Setter
public class ModelView {

    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

그럼 MyView에도 model 관련 작업을 해준다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
   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(request::setAttribute);
        model.forEach((key, value) -> request.setAttribute(key, value));
    }

그럼 ServletV3은 어떻게 바뀔까?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@WebServlet(name = "ServletV3", urlPatterns = "/controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    private Map<String, ControllerV3> controllerV3Map = new HashMap<>();

    public FrontControllerServletV3() {
        controllerV3Map.put("/controller/v3/members/new-form", new MemberFormControllerV3());
        controllerV3Map.put("/controller/v3/members/save", new MemberSaveControllerV3());
        controllerV3Map.put("/controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV3 controller = controllerV3Map.get(requestURI);

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

아래 부분이 상당히 길어졌다
내용을 보면, paramMap을 가져오고
process한 부분에서 viewName을 가져와서 그 논리 이름에 대한 view를 리턴해줌을 알 수 있다

Controller는 무슨 일을 할까?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 modelView = new ModelView("save-result");
        modelView.getModel().put("member", member);
        return modelView;
    }
}

paramMap에서 원하는 정보를 get하고 로직 처리한 후에,
modelView에 정보를 넣어주고 리턴함을 알 수 있다

이걸 어떻게 개선할 수 있을까?

ServletV4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@WebServlet(name = "ServletV4", urlPatterns = "/controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {

    private Map<String, ControllerV4> controllerV4Map = new HashMap<>();

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

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV4 controller = controllerV4Map.get(requestURI);

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