스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

장원령·2021년 4월 17일

Backend(Java Spring)

목록 보기
4/6

인프런 김영한님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 요약정리한 내용입니다

1. 웹 애플리케이션 이해

1. 웹 서버 웹 애플리케이션 서버

1 - 1) 웹서버

: 웹 서버는 HTTP를 기반으로 동작한다.
: 정적 리소스? : 웹 브라우저나 클라이언트에서 요청이 들어왔을 때, 그것에 대한 리소스가 이미 만들어져있고, 만들어져 있는 리소스를 그냥 보내주면 되는 것이다.

1 - 2) 웹 애플리케이션 서버

: HTTP를 기반으로 동작한다.
: 웹 서버와 동일하고, 프로그램 코드를 실행해서 애플리케이션 로직을 수행하기 때문에 다른 사람들에게도 보여줄 수 있음

1 - 3)웹 시스템 구성

: 웹 서버 + 웹어플리케이션 서버 + DB로 구성함

2. 서블릿

: 개발자들이 TCP IP연결하고 멀티쓰레드 고민하기 어려움
: 서블릿이 비즈니스 로직을 제외한 모든 기능들을 WAS에서 지원해준다.

3. 동시 요촐 - 멀티 쓰레드

쓰레드

: 애플리케이션 코드를 하나하나 순차적으로 실행하는 것

요청마다 쓰레드를 생성함

: 쓰레드 생성에 제한이 없어 속도 늦어지고 서버가 죽을 수 있음

이를 보완하기 위해 쓰레드 풀을 사용

: 필요한 쓰레드를 쓰레드 풀에 보관하고 관리하는 방법
: 주로 쓰레드 수를 조정하는데, 쓰레드 풀을 너무 낮게 설정하면 클라이언트의 응답 지연이, 너무 높게 설정하면 CPU, 메모리 리소스 임계점 초과로 서버가 다운된다.
: 싱글톤 객체만 주의하면 된다.

4. HTML HTTP API CSR SSR

1. 정적 리소스

: 고정된 HTML파일, CSS, JS, 이미지, 영상 등을 제공
: 주로 웹 브라우저

2. HTML 페이지

: 동적으로 HTML을 생성하는 것

3. HTTP API

: HTML이 아니라 데이터를 전달, 주로 JSON 사용

4. SSR

: HTML을 서버에서 다 만들어서 클라이언트에 전달
: 정적인 화면에 사용되고, JSP,타임리프 등

5. CSR

: HTML 결과를 자바스크립트를 사용해서 웹 브라우저에서 동적으로 생성해서 적용
: React, Vue.js

5. 자바 웹 기술 역사

: 현재는 애노테이션 긱반의 스프링 MVC를 사용한다.
: 통합에 대한 고민이 없고, 굉장히 유연하고 편리하게 사용 가능
: 빌드 결과에 WAS 서버 포함
: 자바 뷰 템플렛은 주로 타임리프를 사용한다.

2. 서블릿

1. 프로젝트 생성

: package를 war로.

2. Hello 서블릿

@WebServlet

: name은 서블릿 이름, urlPatterns를 통해 URL을 매핑한다.

@ServletComponentScan

: 서블릿 자동 등록 하도록 해주는 스프링 부트의 어노테이션.

service protocol

: 열쇠같이 생긴게 service protocol

Ctrl +O : Override 가능한 메서드 목록을 확인하여 구현하기 위한 코드를 자동 생성해 줍니다.

soutm : 메소드의 이름을 출력해줌

: was 서버들이 서블릿 표준 스펙을 구현해서 위의 request에 찍히는 것

.getParameter()

: 를 통해 쿼리 파라미터를 조회할 수 있다.

결론적으로, 서블릿을 통하여 http메시지를 편리하게 사용할 수 있도록 대신 parsing 해준다.

3. HTTPServletRequest

: 개발자 대신에 HTTP요청 메시지를 파싱하는 역할이다.
: start line, header에 대한 정보 조회 방법이다.

4. HTTP 요청 데이터

: HTTP 요청 메시지를 통하여 클라이언트에서 서버로 데이터를 전달하는 방법에는 크게 세 가지가 있다.

1. GET - 쿼리 파라미터

: 메시지 바디 없이 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 시 반복문 생성 가능

2. POST - HTML Form

: HTML form을 사용해서 클라이언트에서 서버로 데이터를 전송한다.
: 메시지 바디에 쿼리 파라미터 형식으로 전달
: 위의 코드를 중복해서 사용할 수 있다.(쿼리 파라미터 조회 메소드)

3. HTTP message body에 직접 담아 요청

: HTTP API에 주로 사용 데이터를 주로 JSON 사용함.

  1. API 메시지 바디 - 단순 텍스트
    : HTTP 메시지 바디의 데이터를 InputStream을 사용해서 바이트코드로 얻을 수 있고, Streamutils를 활용하여 인코딩 정보를 제공하면 string으로 바꿀 수 있다.
ServletInputStream inputStream = request.getInputStream();
 String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
  1. API 메시지 바디 - Json
    : lombok을 통해 자동으로 getter와 setter 생성
    : Json을 파싱해서 사용하려면 jackson 라이브러리의 ObjectMapper를 사용하면 된다.

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

: 단순 텍스트 응답, HTML 응답, HTTP API - MessageBody JSON 응답등의 내용을 담아 전달한다.

: Content-Type,쿠키, Redirect등의 기능을 제공한다.

  1. 간단한 텍스트 메시지를 담아 전송
    : HTTP 응답으로 HTML을 반환할 때는
response.setContentType("text/html");
  1. Json 방식
 response.setHeader("content-type", "application/json");

3. 서블릿, JSP, MVC 패턴

4. MVC 프레임워크 만들기

: MVC 프레임워크는 궁극적으로 스프링 MVC와 유사한 구조이기 때문에, 이해하기에 도움이 됨

1. 프론트 컨트롤러 패턴(문지기)

: 예전에는 클라이언트가 공통로직이 필요할 경우 각각 다 만들어야 함
: 프런트 컨트롤러를 도입하면, 서블릿처럼 A,B,C를 각각 처리하도록 프론트 컨트롤러가 해결해줌.

  • 프론트 컨트롤러의 특징
    : 서블릿하나로 클라이언트의 요청을 받음
    : 요청에 맞는 컨트롤러를 찾아서 호출해줌
    : 공통처리가 가능해진다.
    : 나머지 컨트롤러는 서블릿 사용 필요가 없어짐.

2. 프론트 컨트롤러 도입

: 구조를 맞추는 단계
: 클라이언트가 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가 호출될 컨트롤러이다. 

3. View 분리

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객체를 생성 후 반환하고, 프론트 컨트롤러가 이를 일관적으로 처리한다.

4. Model 추가

: 컨트롤러 입장에서, 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

A. frontControllerServlet 인터페이스

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

B. 마이뷰에 다음과 같이 render함수를 추가한다.

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로 지정한다.
    }
}

: 실제 구현하는 컨트롤러들의 코드가 굉장히 간편해짐.

5. 단순하고 실용적인 컨트롤러

: 항상 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";

6. 유연한 컨트롤러

어댑터 패턴 이란?
: 완전히 다른 두 가지의 인터페이스를 호환을 가능하게 하는 것.

1. FrontController코드를 작성하며 v3를 추가하는 과정은 아래와 같다.

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

2. 기존의 코드에 V4를 추가하고, V4코드를 실행했을때의 결과를 코드와 같이 보면 다음과 같다.

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

: 확장성이 용이해진 모습.

V4를 추가하는 부분에서 어댑터의 역할

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를 만들어서 반환해야 한다. 그러한 과정의 위의 세 문장.

5. 스프링 MVC 구조 이해

1. 스프링 MVC 전체 구조

: 위에서 만들었던 스프링 MVC 프레임워크와 유사한 구조를 가지고 있다.
: DispatcherServlet이 Frontcontroller를 구성했던 일을 그대로 한다.

  • 동작 순서
  1. 핸들러 조회
  2. 핸들러 어댑터 조회
  3. 핸들러 어댑터 실행
  4. 핸들러 실행
  5. ModelAndView 반환
  6. viewResolver 호출
  7. view 반환
  8. 뷰 렌더링

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

: implements Controller(web.servlet.mvc)
: controller V2와 V3의 중간 같은 역할을 한다.
: 이후로는 @RequestMapping을 사용하여 컨트롤러를 만든다.

3. 뷰 리졸버

: ModelAndView를 사용한다.

4. 스프링 MVC - 시작하기

@RequestMapping : RequestMappingHandlerMapping, RequestMappingHandlerAdapter 를 다룬다.

5. 스프링 MVC - 컨트롤러 통합

: @RequestMapping을 활용하면 메소드 레벨과의 조합도 가능하다.

@RequestMapping("/springmvc/v2/members") // 클래스 레벨
@RequestMapping("/new-form") // 메소드 레벨
@RequestMapping("/save") // 메소드 레벨

6. 스프링 MVC - 실용적인 방식

6. 스프링 MVC - 기본 기능

1. 생성

: thymeleaf로 생성
: packaging을 jar를 선택하는데, 내장 톰캣에 최적화 할 때 사용한다. 반면, war는 톰캣을 별도로 설치하고 빌드된 파일을 넣을 때, jsp를 넣을때 사용
: 스프링 부트에 Jar 를 사용하면 /resources/static/index.hml 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.

2. 로깅 간단히 알아보기

: 스프링 부트 - 로깅 - 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";

    }

}

3. 요청 매핑

: 요청이 왔을때 어떤 컨트롤러가 매핑이 되는가
: @RequestMapping 에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.
: 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE

1. HTTP 메소드 매핑

: 아래의 경우에는 GET이 아니면 에러

@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
 log.info("mappingGetV1");
 return "ok";
}

2.HTTP 메소드 매핑 축약

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

3. PathVariable(경로 변수) 사용

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
 log.info("mappingPath userId={}", data);
 return "ok";
}

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

5 미디어 타입의 경우

@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
 log.info("mappingProduces");
 return "ok";
}

4. HTTP 요청

: 다양한 파라미터들이 존재하는데, 다음과 같은 역할을 한다.

  1. HttpMethod httpMethod
    : HTTP 메소드 조회

  2. Locale locale
    : Locale 정보 조회

  3. @RequestHeader MultiValueMap<String, String> headerMap
    : 모든 HTTP 헤더를 multivaluemap 형식으로 조회
    : MultiValueMap : 하나의 키에 여러 값을 받을 수 있는 것

  4. @RequestHeader("host") String host
    : 특정 HTTP 헤더를 조회

  5. @CookieValue(value = "myCookie", required = false) String cookie
    : 특정 쿠키를 조회

5. HTTP 요청 파라미터

: HTTP 메시지를 통하여 클라이언트에서 서버로 메시지를 전달할 때는 3가지 방법이 있다.
1. GET - 쿼리 파라미터
2. POST - HTML 폼
3. HTTP message body에 데이터를 직접 담아서 요청

: GET 쿼리 파리미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이조회할 수 있다.
이것을 간단히 요청 파라미터(request parameter) 조회라 한다.

1. request.getParameter()

: 단순히 HttpServletRequest가 제공하는 방식으로 요청 파라미터를 조회가 가능하다.
: 리소스는 /resources/static 아래에 두면 스프링 부트가 자동으로 인식한다

2. @RequestParam

3. @ModelAttribute

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

6. HTTP 요청 메시지

1. 단순 텍스트


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

2. JSON

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인지 확인해야 함.

7. 응답

: 응답데이터를 만드는 방법은 아래의 세 가지 이다.

  1. 정적 리소스
  2. 뷰 템플릿 : 동적인 HTML
  3. HTTP 메시지 사용

1. 정적 리소스

: 다음 디렉토리에 넣는다.

src/main/resources/static

2. 뷰 템플릿

: 경로는 다음과 같다.

src/main/resources/templates 

8. HTTP 메시지 컨버터

: 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이용하여 메시지 바디에 데이터 생성

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


Argument Resolver 때문에 다양한 파라미터 처리가능 ReturnValueHandler는 응답값을 반환하고 처리한다. 위의 두가지를 처리하는데에 앞서 배운 HTTP메시지 컨버터가 사용된다.

7. 스프링 MVC - 웹 페이지 만들기

1. 상품 도메인 개발

1.1 도메인

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

1.2 저장소


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

}

1.3 이에 대한 간단한 테스트

: 테스트 작성시엔 언제나 given when then을 유의할 것

@AfterEach
    void afterEach() {// 매번 테스트 실행 된 후에 값을 초기화
        itemRepository.clearStore();
    }

2. 상품 서비스 HTML

: /resources/static에 HTML을 넣어두면 실제 서비스에서도 공개된다.

3. 상품 목록 - 타임리프

1. 먼저 컨트롤러를 만든다.

@RequiredArgsConstructor
public class BasicItemController {
    private final ItemRepository itemRepository;

    //@Autowired // 1. 생성자 하나 있으면 @Autowired는 생략 가능
    //public BasicItemController(ItemRepository itemRepository) {
    //    this.itemRepository = itemRepository; // 2. lombok 의 @RequiredArgsConstructor 사용하면 final 붙은거는 생략가능 
    //} 
}

2. thymeleaf 사용

2.1 선언

<html xmlns:th="http://www.thymeleaf.org">

2.2 속성

: 그후 반환하는 view를 만드는데, 앞서 넣었던 HTML파일들을 타임리프를 사용해서 동적으로 바꿔야 한다.

: thymeleaf는 그대로 볼때는 href, 뷰 템플릿을 거치면 th:href의 값이 href로 대치되면서 동적으로 변경한다.

2.3 핵심

: HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.

2.4 리터럴 대체 '|...|'

: 더하기 없이 편리하게 결합하는 기능

2.5 반복

: th:each 를 사용한다.

2.6 변수 표현식

: ${}
: 모델에 포함된 값이나 타임리프 변수로 선언한 값을 조회할 수 있다.

2.7 내용 변경

: 내용의 값을 th:text 값으로 변경

2.8 URL 링크 표현식 2

: @{}
: 쿼리 파라미터를 이용 가능하다.

4. 상품 상세

1. 마찬가지로 BasicitemController에 추가한다.

2. 위와 같이 th 문법에 맞게 개조한다.

th:value="${item.itemName}

: 이 문장의 경우엔, value 속성을 th:value 속성으로 변경한다.

5. 상품 등록 폼

: 상품을 등록할 수 있는 폼을 보여주는 것

1. 컨트롤러 내에서는

// 같은 URL이더라도  HTTP 메소드로 기능을 구분한다. (등록 폼과 등록 처리를 깔끔하게) 
    @GetMapping("/add")
    public String addFrom() {
        return "basic/addForm";
    }

    @PostMapping("/add")
    public String save() {
        return "basic/addForm";
    }

6. 상품 등록 처리 - @ModelAttribute

: 메시지 바디에 쿼리 파라미터 형식으로 전달하였다 이를 처리하기 위해 @RequestParam 사용한다.
: 상품 등록 처리를 위해서는 두가지 방법이 있는데

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

2. @ModelAttribute를 사용하는 방법

@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 의 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다. 이때 클래스의 첫글자만 소문자로 변경해서 등록한다.

7. 상품 수정

: 등록할 때는 뷰 템플릿을, 수정할때는 redirect를 사용하였다.

8. PRG Post/Redirect/Get

: 새로고침은 서버에 전송된 데이터를 다시 전송하는데, 이 때문에 지금까지 만든 데이터가 계속 쌓이게 되는 결론에 닿는다.

: 이 문제를 해결하기 위해 PRG의 단계를 사용한다. 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라 상품 상세 화면으로 리다이렉트를 호출해주면 된다. 리 다이렉트 때문에 상품 저장 후에 실제 상품 화면으로 다시 이동하고 마지막에 호출한내용이 GET/items/{id}가 된다.

return "redirect:/basic/items/" + item.getId(); 

: 다만 위의 방식으로 사용 시 숫자가 아닌 문자 등을 사용 시 URL 인코딩 문제가 있음

9. RedirectAttributes

: 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"가 반환된다.

0개의 댓글