SpringMVC 정리

꿀이·2022년 3월 1일
0

SpringMVC 정리

인프런 springmvc 강의 들으면서 공부한 내용 메모 해볼까?


서블릿이란?

서버에서 어떠한 작업을 처리해야 한다고 할때 아래 그림처럼 수많은 과정을 거쳐야한다. 근데 비즈니스 로직 부분을 제외하고는 공통적으로 사용하는 부분이다. 매번 개발자들이 같은 코드를 작성하면 효율이 안나오니까 서블릿이 나머지 부분은 대신 처리해주는듯..!

  • HTTP 요청시 흐름

    • WAS는 request, response 객체를 새로 만들어서 서블릿 객체 호출
    • 개발자는 request 객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용
    • 개발자는 response 객체에 HTTP 응답 정보를 편리하게 입력
    • WAS는 response 객체에 담겨있는 내용으로 HTTP 응답 정보 생성

동시 요청 - 멀티 쓰레드

was 내에 쓰레드 풀이 있어서 요청이 들어올 때마다 해당 쓰레드를 이용해서 서블릿을 이용해서 로직을 처리하고 다 사용하면 쓰레드 풀에 다시 반납한다. 개발자가 직접 멀티 쓰레드 관련 코드를 작성하려고 하면 머리가 깨질 수 있는데 이런걸 WAS 가 지원을 해준다.

적정 쓰레드 숫자를 찾는게 중요! 쓰레드를 너무 많이 생성해버리면 cpu가 죽어버릴 수도 있다.
성능 테스트 툴 : 아파치ab, 제이미터, nGrinder


서블릿 실습

클라이언트가 /hello 로 들어오면 was 는 해당 서블릿을 호출한다. (요청받은 서블릿은 애초에 프로그램 실행될 때 서블릿 컨테이너에 등록된다.) 서블릿을 사용하려면 HttpServlet 을 상속받아서 사용해야 하고, service (이거 protected로 되어 있는거다) 메서드를 오버라이드 해서 사용해야 한다.

매겨변수로 있는 HttpServletRequest 및 HttpServletResponse 를 타고 들어가 보면 서블릿 패키지의 인터페이스로 정의되어져 있는데 아마 WAS 가 구현체로 생성을 해서 servlet 호출할 때 넘겨주는듯? 객체를 출력해보면 아파치 어쩌고 뜨는거 보면 그런거 같은데...

요런 request & response 객체를 통해서 쉽게 쿼리 파라미터들을 가져올 수 있고 http 메세지 바디에 데이터를 넣어서 응답을 보낼 수도 있다. 마지막에 response 객체를 was가 다시 웹 브라우저로 결과를 출력 또는 전달해준다.

@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);//write() 하면 http 메세지 바디에 들어간다.

    }
}

참고
HTTP 요청 메세지를 개발자가 항상 맨날 직접 파싱하면 너무 번거롭다. 서블릿은 이러한 작업을 편리하게 해주는 다음과 같은 HttpServletRequest 를 제공한다. 얘가 개발자 대싱 메세지 파싱해주는 역할을 한다!!

  • request.setAttribute , getAttribute : HttpServletRequest 객체는 내부에는 해당 http 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소가 있는데 이때 사용한다.
  • request.getSession() : 세션기능도 제공한다.

HTTP 요청 데이터

  • GET - 쿼리파라미터
    /url?username=hello&age=20 이런식으로 url에 쿼리파라미터를 포함해서 보내는거 ex) 검색,필터,페이징에서 많이 사용

  • POST - HTML Form
    메세지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20 (이런식으로 뭔가 get과 방식이 비슷하자나? 이래서 나중에 @modelattribute 이런거 하면 get,post 상관없이 되는건가보네)
    ex) 회원 가입, 상품 주문 등 HTML Form 사용해서 전송

  • HTTP message body 에 데이터 직접 담아서 전송
    HTTP API 에서 주로 사용하고, json 형식으로 많이 사용한다.


GET 쿼리 파라미터

기본적으로 key-value 형식이다. 그래서 value 를 가져오려면 우선 개발자는 username & age 같은 key 값을 알고 있어야 한다. 다음과 같이 우선 이름들을 뽑아온 후에 해당 이름으로 다시 getParam(이름) 을 넘겨주면 해당 값이 나오게 된다.

//key값을 구한 후에 접근
request.getParameterNames().asIterator()
             .forEachRemaining(paramName -> System.out.println("paramName = " + paramName + "=" +
                                                request.getParameter(paramName)));

//직접 접근
String username = request.getParameter("username");

같은 username 에 여러개의 값이 쿼리 파라미터로 넘어온다면 아래처럼 배열로 넘어오게 된다. (근데 이렇게 넘기는 일이 있을지는 모르겠다;; 중복으로 넘겨야 하는 이유가 언제 있을까..?)

String[] usernames = request.getParameterValues("username");
        for (String name : usernames) {
            System.out.println("name = " + name);
        }

POST HTML Form

Form 전송의 경우 웹이 자체적으로 Content-Type 을 "application/x-www-form-urlencoded" 으로 설정하고 서버로 전송함. (get의 경우는 메세지 바디를 사용하지 않기 때문에 content-type 설정을 하지 않는다.)

이게 형식이 get요청의 쿼리 파라미터 형식과 같기 때문에 위에 get 예제에서 만든걸 그대로 쓸수 있다.

즉, request.getParameter 는 get 요청이든 post 요청이든 어디든 사용할 수 있다..!!

API 메시지 바디

ServletInputStream 을 통해서 메세지 바디의 내용을 바이트 코드로 가져올 수 있다. 그럼 이제 이 바이트 코드를 string 으로 변환해주면 StreamUtils.copyToString 을 사용하면 된다. 이렇게 하면 메세지 바디의 내용을 받아올 수 있다.

예제에서는 json 형식으로 보냈는데, 이거 json으로 파싱하는 뭔가 있을거 같은데... JsonParser 이런걸로 하는건가?

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        //이렇게 하면 메시지 바디의 내용을 바이트 코드로 얻을 수 있다
        ServletInputStream inputStream = request.getInputStream();
        System.out.println("inputStream = " + inputStream);
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);//이건 스프링이 제공
        System.out.println("messageBody = " + messageBody);
        response.getWriter().write("ok");

    }


역시나... json 파싱하는게 있었구나...

ObjectMapper 라는게 있네 Jackson 라이브러리에 있는건데 json 형식으로 파싱할 때 사용하나보다. 이런식으로 하면 HelloData 객체가 잘 생성되는걸 확인할 수 있다.

나중에 스프링을 사용하면 이렇게 복잡한 과정들을 다 알아서 해준다..!!

public class RequestBodyJsonServlet extends HttpServlet {

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        System.out.println("messageBody = " + messageBody);

        //아니잇..!
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        System.out.println("helloData.getUsername() = " + helloData.getUsername());
        System.out.println("helloData.getAge() = " + helloData.getAge());

        response.getWriter().write("ok");
    }
}

HttpServletResponse - 기본 사용법

이건 예제 코드만 붙여 놓고 나중에 필요할일이 있으면 써보자. 근데 이걸 직접 쓸일이 언제 있을까낭... 아직은 모르겠다.

@Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("ResponseHeaderServlet.service");
        //[status-line]
        response.setStatus(HttpServletResponse.SC_OK);// => 200

        //[response-headers]
        response.setHeader("Content-Type", "text/plain;charset=utf-8");
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("my-header", "hello");

        //[Header 편의 메서드]
        content(response );

        //[쿠키 편의 메서드]
        cookie(response);

        //[리다이렉트 편의 메서드]
        redirect(response);

        //[message body]
        PrintWriter writer = response.getWriter();
        writer.println("안녕하세요!!");

    }

    private void content(HttpServletResponse response) {
        //Content-Type: text/plain;charset=utf-8
        //Content-Length: 2
        //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) {
        //Set-Cookie: myCookie=good; Max-Age=600;
        // response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");

        //이렇게 하면 간단하게 할 수 있네 위에처럼 안하고
        Cookie cookie = new Cookie("myCookie", "good");
        cookie.setMaxAge(600); //600초
        response.addCookie(cookie);
    }

    private void redirect(HttpServletResponse response) throws IOException {
        //Status Code 302
        //Location: /basic/hello-form.html
        //response.setStatus(HttpServletResponse.SC_FOUND); //302
        //response.setHeader("Location", "/basic/hello-form.html");

        //그냥 이렇게 편리하게 리다이렉트를 쓸 수 있네
        response.sendRedirect("/basic/hello-form.html");
    }

HTTP 응답데이터 - 단순 텍스트,HTML

이거 html로 보낼때는 setContentType 을 "text/html" 로 설정해줘야 한다. 아니 근데 이거 자바 코드로 html 을 작성해서 보낼 수가 있네?

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //Content-Type: text/html;charset=utf-8
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<body>");
        writer.println("<div> 안녕 </div>");
        writer.println("</html>");
        writer.println("</body>");
    }

HTTP 응답데이터 - API JSON

이거도 뭐 별내용 없넹 ObjectMapper 를 이용해서 객체를 json 형식으로 바꾸서 보낼 수 있다.

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //Content-Type : application/json
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        HelloData helloData = new HelloData();
        helloData.setUsername("lee");
        helloData.setAge(30);

        //위에 객체를 json 으로 변환해주자 이떄도 objectmapper 를 사용한다.
        String result = objectMapper.writeValueAsString(helloData);
        response.getWriter().write(result);
    }

서블릿을 이용한 웹 애플리케이션

서블릿을 이용해서 회원 관리 웹 애플리케이션을 만들어 보자..!

  • /servlet/members/new-form 에서 username 과 age를 입력 및 전송
  • /servlet/members/save 에서 MemberRepository 에 save
  • /servlet/members 에서 모든 회원 조회

아래 코드와 같이 구현을 했는데 html을 직접 작성하는게 너무 번거롭고 오타 발생 확률이 너무 높다!!! 그리고 뭔가 중복되는 코드들이 보임 setContentType 이라든가 setCharacterEncoding 이라든가 getWriter 라든가?

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        PrintWriter w = response.getWriter();

        //와우 자바코드로 html을 작성을 하는데 이게 엄청 불편!!!
        w.write("<!DOCTYPE html>\n" +
                "<html>\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>Title</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "<form action=\"/servlet/members/save\" method=\"post\">\n" +
                "    username: <input type=\"text\" name=\"username\" />\n" +
                "    age:      <input type=\"text\" name=\"age\" />\n" +
                " <button type=\"submit\">전송</button>\n" + "</form>\n" +
                "</body>\n" +
                "</html>\n");
    }
}


@WebServlet(name = "memberSaveServlet" , urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();

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

        memberRepository.save(member);

        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter w = response.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>");

    }
}

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

    }
}

JSP를 이용한 회원 관리 웹 애플리케이션

JSP 템플릿 엔진을 통해서 HTML 내에서 자바코드를 넣어서 동적으로 변경이 필요한 부분들을 출력할 수 있게 해주자.
<% java code %> 여기에 비즈니스 로직을 넣어주고 여기서 나오는 동적 결과물을 아랫쪽 html 에서 사용한다.

아니 근데 이거도 만만치가 않네... 너무 복잡... html 한줄 고쳐야 할거를 몇백줄의 자바 코드까지 봐야하는 상황이 나올 수도 있다고 한다ㅠㅠ

//회원 저장 로직
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    // request, response 그냥 사용 가능 jsp도 결국은 서블릿으로 변환이 된다.
     MemberRepository memberRepository = MemberRepository.getInstance();
     System.out.println("save.jsp");
     String username = request.getParameter("username");
     int age = Integer.parseInt(request.getParameter("age"));
     Member member = new Member(username, age);
     System.out.println("member = " + member);
     memberRepository.save(member);
%>

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

MVC 패턴 적용 (서블릿 + JSP)

HttpServletRequest 객체 내부에 데이터를 담을 수 있는 공간이 있는데 setAttribute, getAttribute를 이용하면 저장 및 조회를 할 수 있다. 요걸 Model 로 이용할거다.

save 부분만 코드로 한번 보면 Controller 를 통해서 비즈니스 로직을 실행한다. 그리고나서 setAttribute에 결과물을 담은 후에 dispatcher.forward() 를 호출했는데 forward()를 호출하면 해당 view경로에 있는 jsp 파일로 제어권을 넘겨주게 된다.

이렇게 코드를 작성하니까 jsp 만 사용했을 때와는 다르게 java code 와 view 가 완전히 분리가 되었다. 하지만 조금더 개선할 점들이 보인다. controller 부분에서 중복되는 부분들이 많이 보인다. 경로 설정이라던가 (현재 상황에서 폴더 경로가 바뀌거나 한다면 일일이 controller 마다 string부분을 수정해야 한다.)

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

    private static final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        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);

    }
}
-------------------------------------------------------------------------------
//save view
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
 <meta charset="UTF-8">
</head>
<body>
성공
<ul>
 <li>id=${member.id}</li>
 <li>username=${member.username}</li>
 <li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

MVC 프레임워크 구현해보기

프론트 컨트롤러 도입 - v1

기존에는 각각의 클라이언트가 각각의 컨트롤러에 직접 접근을 했다. 이렇게 하다보니 각각의 컨트롤러에 HttpServlet 을 상속받아서 구현을 했는데 이렇게 하지말고 공통부분을 묶어서 처리를 해줄거다.

스프링MVC 의 DispatcherServlet 의 역할을 한번 구현해보자 . v1 버전에서는 view 부분은 그대로 놔두고 frontcontroller 만 추가할거다

참고

System.out.println("requestURL = " + requestURL);
System.out.println("requestURI = " + requestURI);

  1. 우선 ControllerV1 이라는 인터페이스를 만든 후에 각각의 form,save,members 구현체를 만든다.

  2. FrontController 에서 Map 을 하나 만든 후에 key = uri & value = 컨트롤러 구현체 를 넣어준다.

  3. FrontController 에서 urlPatterns = "xxx/xxx/*" 로 해줘서 클라이언트의 모든 요청들이 FrontController 를 거칠 수 있도록 한다.

  4. 클라이언트 요청이 들어오면 해당 URI 를 key값으로 하는 컨트롤러를 Map 에서 찾아서 반환하고 호출한다. 이때 request,response 를 같이 넘겨준다.

여기까지 하면 해당 컨트롤러에서 dispatcher 를 통해서 forward() 를 하게 된다. →현재 버전에서는 view 부분은 구현하지 않음! continue...

// "/*" 로 하면 하위에 어떤 url이 오더라도 일단 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 controllerV1 = controllerMap.get(requestURI);
        if (controllerV1 == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controllerV1.process(request, response);
    }
}
public class MemberSaveControllerV1 implements ControllerV1 {

    private static final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void 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);

        request.setAttribute("member", member);

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

    }
}

View 분리 - v2

v1 버전에 추가로 view를 분리해 보자. 기존에는 컨트롤러 내에서 dispatcher.forward() 를 통해서 직접 jsp 를 뿌려줬었다. 이러다 보니 각각의 컨트롤러에서 중복적으로 코드들이 발생하게 된다. MyView 의 render 메서드를 통해서 jsp를 호출하자.

  1. v1 버전에 추가적으로 MyView 클래스를 만들었다. 여기서는 이제 viewPath 를 가지고 해당 jsp 파일로 제어권을 넘겨주는 역할을 한다.

  2. FrontController 에서는 이제 controller.process 를 호출한 후에 MyView를 반환받을거다.

  3. FrontController 에서 MyView.render() 를 통해서 jsp 를 호출한다.

이렇게 하니 각각의 컨트롤러에서 공통적으로 사용하던 dispatcher.forward() 를 생략할 수 있고 코드가 간결해 졌다. 컨트롤러는 서비스 처리 로직에 좀더 집중할 수 있는듯.

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

@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("FrontControllerServletV1.service");

        String requestURI = request.getRequestURI();

        ControllerV2 controllerV2 = controllerMap.get(requestURI);
        if (controllerV2 == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyView view = controllerV2.process(request, response);
        view.render(request, response);
    }
}

public class MemberSaveControllerV2 implements ControllerV2 {

    private static final 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);
        request.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        return new MyView(viewPath);

    }
}

Model 추가- v3

컨트롤러 입장에서 HttpServletRequest, HttpServletResponse 에 대한 정보를 꼭 알아야 할까? 요청 파라미터 정보를 따로 넘겨주기만 한다면 컨트롤러가 httpServlet 기술에 의존하지 않아도 된다.

추가적으로 viewPath 의 논리적인 주소를 controller 에서 반환해 주도록 개발을 해보자 이렇게하면 view 폴더가 변경되거나 할 때 코드변경을 최소로 할 수 있다.

  1. v2 와는 다르게 controller 에서는 더이상 HttpServlet 기술에 의존하지 않는다. 그렇기 때문에 FrontController 에서 request parameter 들을 모두 담아서 넘겨줘야 한다.

  2. ModelView 클래스를 만들어서 컨트롤러에서 논리적 viewPath 와 뷰(jsp) 에서 사용할 데이터 (예를들면 회원 목록) 를 담아서 리턴할 수 있도록 한다.

  3. FrontController 에서 (2) 에서 받은 ModelView 를 이용해서 실질적인 URI 를 생성해주고, render()를 호출한다. 이때 render()에서는 jsp 에서 뿌려줄 데이터 (예를들면 회원목록)들을 모두 request.setAttribute 를 해준다. 이후에 forward() 를 하면서 출력

이렇게 하니까 FrontController 의 코드는 복잡하게 바뀌었다. 하지만 각각의 컨트롤러 부분은 훨~씬 간단해졌다.

@Getter @Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

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

@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("FrontControllerServletV1.service");

        String requestURI = request.getRequestURI();

        ControllerV3 controllerV3 = controllerMap.get(requestURI);
        if (controllerV3 == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //파라미터들을 다 뽑아서 paramMap 에 넣어주고
        Map<String, String> paramMap = createParamMap(request);

        //컨트롤러에서 논리적 viewName 과 필요한 결과 모델들을 반환 받았다.
        ModelView mv = controllerV3.process(paramMap);

        //논리 이름을 가지고 실제 uri 를 만들어 주자
        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) {
        //paramMap : request에 있는 모든 파라미터들을 다 가져온다
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator().
                forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

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

v3 버전의 각각의 컨트롤러 부분을 보면 중복부분이 한가지 보인다. 바로 modelView 객체를 직접 new 로 생성을 해주는거다. 이부분을 FrontController 에서 만들어서 매개변수로 파라미터와 함께 넘겨줄거다. 그리고 각각의 컨트롤러는 논리적인 viewName 만 리턴해주면 된다.

이렇게 하면 각각의 컨트롤러는 다음과 같은 수행만 하면 된다
1. 파라미터에서 값을 가져오고
2. 비즈니스 로직을 실행하고
3. 매개변수로 넘어온 model 에 비즈니스 로직 실행결과를 담고
4. 논리적 viewName 만 리턴해주면 된다.

컨트롤러 부분이 v3 보다 더욱더 간단해진걸 확인할 수 있다. 공통부분을 개발할 때 번거러움이 있지만 나중에 개발자들이 컨트롤러를 개발할 때에는 수고가 줄어들게 된다.

//FrontControllerV4  서비스 부분
    @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;
        }

        //파라미터들을 다 뽑아서 paramMap 에 넣어주고
        Map<String, String> paramMap = createParamMap(request);

        //모델을 미리 만들어서 컨트롤러로 넘겨줄거다
        Map<String, Object> model = new HashMap<>();

        //모델과 파라미터들을 넘겨주고 viewName 을 받아온다.
        String viewName = controller.process(paramMap, model);

        //실질적 viewName 을 만들어준다.
        MyView view = viewResolver(viewName);

        //렌더링
        view.render(model, request, response);
    }

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {

        //1. 파라미터에서 값 가져오고
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

        //2. 비즈니스 로직 실행하고
        Member member = new Member(username, age);
        memberRepository.save(member);

        //3. 모델에 담아서 viewName만 리턴하면 끝!!
        model.put("member", member);
        return "save-result";
    }

유연한 컨트롤러-v5 (어댑터 패턴 적용)

지금까지의 FrontController 를 보면 해당 ControllerV3 또는 ControllerV4 중에서 하나만 호출이 가능했다. 어떤 개발자는 V3 을 써서 개발을 하고 싶고 또 다른 개발자는 V4를 사용해서 개발을 하고 싶을 수가 있다. 그래서 핸들러 어댑터를 통해서 핸들러(컨트롤러) 에 접근할 수 있도록 해보겠다. 동작은 다음과 같다

  1. 요청 URI 들과 해당하는 컨트롤러v3, v4... 등을 handlerMappingMap 에 넣어놓는다.
  2. 핸들러어탭터들을 모아둔 list에 v3, v4... 의 어댑터들을 넣는다.
  3. 요청이 들어오면 key = URI 를 통해서 해당하는 핸들러매핑 정보를 가져온다.
  4. 찾아온 핸들러 매핑 정보를 이용해서 해당 어댑터를 찾아온다.
  5. 어댑터를 통해서 핸들러 (컨트롤러) 를 호출하고 modelView 를 반환받는다.

스프링mvc 에서 개발할때 어디에는 Model 을 추가한다거나 어디에는 HttpServletRequest 를 매개변수로 넣던가 했던 기억이 있다. 이런걸 다 스프링MVC 프레임워크에서 처리를 해줬나 보네.
아.. 근데 이게 또 아니네... 엄밀히 말하면 Argument Resolver 가 이런 역할을 해주는거였구나 어떤 거는 매개변수로 HttpServlet 을 넘겨주거나 @RequestParam 을 넘겨주거나... 이런걸 아규먼트리졸버가 처리를 해주는거였구나... 누군가 요청정보를 처리를 해줘서 매개변수로 넘겨주는거니까 / 마찬가지로 리턴 할때 어떤거는 void고 어떤건 ModelAndView고 이런거를 ReturnValueHandler 가 처리를 해주는거였네

와.. 근데 이거 이렇게 구조 짜는거 이해하는게 쉽지가 않네... 이렇게 구조짜는걸 어떻게 생각할까나...

//FrontControllerServlet 의 서비스 부분
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

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

    public FrontControllerServletV5() {
        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 {
        //1. 요청이 오면 handlerMappingMap 에서 uri 에 맞는 핸들러를 가져온다.
        Object handler = getHandler(request);

        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //2. 핸들러 어댑터 찾아온다.
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        //3. 핸들러 어댑터를 통해서 핸들러(컨트롤러) 호출
        ModelView mv = adapter.handle(request, response, handler);

        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        view.render(request, response);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {

        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter 를 찾을 수 없습니다. hanlder="+handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

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

참고
v3,v4 ... 를 공통으로 처리하기 위해서 어댑터.handle() 메서드는 항상 mv 를 반환하도록 해준다. 그래서 기존v4 와는 다르게 modelView 객체에다가 viewName 과 model 을 넣어줘서 이걸 반환한다.

//v4 핸들러어댑터
    @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;
    }

스프링MVC

스프링mvc도 위에서 만든 구조와 거의 비슷한 모습인거를 알 수 있다.
1. 스프링MVC 의 경우 @RequestMapping 을 통해서 핸들러 매핑 & 핸들러 어댑터 조회를 처리한다.
2. return "viewName" 을 하면 뷰 리졸버를 통해서 뷰처리 & Model.addAttribute 를 통해서 ModelAndView 처리 가능

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

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

    @RequestMapping(method = RequestMethod.GET)
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();

        model.addAttribute("members", members);
        return "members";
    }



    @RequestMapping(value = "/save",method = RequestMethod.POST)
    public String saveForm(@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";
    }
}

스프링MVC 기능 정리

@PathVariable

    //path변수 이름이 같으면 밑에 메서드처럼 사용할 수도 있다
    //@PathVariable("userId") String data -> 이런식으로 쓸수도 있는데 넘어오는 값의 이름이랑 같으면 생략 가능
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable String userId) {
        log.info("mappingPath userId={}",userId);
        return "ok";
    }

    /**
     * PathVariable 사용 다중
     * http://localhost:8080/mapping/users/userA/orders/100
     * 이런식으로 uri 가 들어오면
     */
    @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId,
                               @PathVariable Long orderId) {
        log.info("mappingPath userId={}, orderId={}", userId, orderId);
        return "ok";
    }

@RequestParam & @ModelAttribute

@RequestParam("파라미터 이름") 을 통해서 값을 넘겨받을 수 있다. 이때 만약 매개변수 이름이 쿼리파라미터 이름과 동일하면 ("파라미터 이름") 을 생략할 수 있다.

@ModelAttribute 를 통해서 객체에 바로 담아서 쓸 수도 있다. 이 경우에는 쿼리 파라미터 이름으로 객체의 setter 를 호출해서 값을 넣어준다. -> 객체의 맴버변수와 쿼리파라미터 이름을 맞춰줘야 한다.

    @PostMapping("/login")
    @ResponseBody
    private String login2(@RequestParam(defaultValue = "guest") String loginId,
                          @RequestParam(defaultValue = "123") String password) {
        log.info("userId={} password={}", loginId, password);
        return "ok";
    }

    //@PostMapping("/login")
    //@ResponseBody
    private String login(@ModelAttribute LoginForm loginForm) {

		log.info("loginId={} password={}",loginForm.getLoginId(),loginForm.getPassword());
        return "ok";
    }
profile
내게 맞는 옷을 찾는중🔎

0개의 댓글