Spring MVC1

Park sang woo·2022년 9월 30일
0

인프런 공부

목록 보기
5/8

𖤐 서블릿

초록색 부분을 제외한 모든 것을 서블릿이 지원한다.
자바를 사용하여 페이지를 동적으로 생성하는 서버측 프로그램으로, HTTP 요청과 응답 처리를 위한 스프링에서 제공하는 핵심 기능.
HTTP로 통신할 때 많은 과정들이 존재하는데, 서블릿은 그 과정을 간소화 시켜주며 개발자가 의미있는 비즈니스 로직에 집중할 수 있도록 도와준다.

TCP/IP 연결, HTTP 요청 파싱, 데이터 내용 읽고 HTTP 응답 메시지 생성까지 다 자동화해준다.






𖤐 동시 요청 - 멀티 쓰레드

⭕🔥 개념 너무 중요. -> 개념 잘 정리 못하면 트래픽 발생 시 해결 어려움.

클라이언트에서 서버 요청하면 WAS가 응답을 한다.
WAS에서는 TCP, IP 커넥션 연결을 해서 servlet을 호출한다.
그런데 이 서블릿을 누가 호출하는지가 중요하다.

☪ 바로 쓰레드가 호출한다.

쓰레드는 애플리케이션 코드를 하나하나 순차적으로 실행하는 것이다.
자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행.
쓰레드가 없으면 자바 애플리케이션 실행이 불가능하다.
쓰레드는 한 번에 하나의 코드 라인만 수행한다. 만약 동시 처리가 필요하면 쓰레드를 추가로 생성을 해주어야 한다.

단일 요청 - 쓰레드 하나 사용
요청이 오면 쓰레드를 할당하고 이 쓰레드를 가지고 servlet을 호출해서 실행한다. 실행하고 나서 응답까지 다 하고 응답 끝나면 쓰레드 휴식.

다중 요청 - 쓰레드 하나 사용
요청하고 연결해서 쓰레드 사용하여 servlet 요청하는데 처리가 잘못된 경우(처리 지연) 가 발생하면 요청 2번이 들어온다. 요청 2번도 쓰레드를 사용해야 하는데 하지 못하고 기다리게 된다. 그래서 결국 요청 1번, 2번 다 죽게 된다.

요청마다 쓰레드 생성
요청 1번 했는데 처리 지연 발생하면 사용하고 있는 쓰레드 냅두고 요청 2번에 대한 쓰레드 생성.

장점
-> 동시 요청을 처리할 수 있다.
-> 리소스(CPU, 메모리)가 허용할 때까지 처리가능
-> 하나의 쓰레드가 지연되어도, 나머지 쓰레드는 정상 동작.

단점
->쓰레드는 생성 비용이 매우 비싸다. (CPU를 많이 쓰기도 함.)
고객의 요청이 올 때 마다 쓰레드르르 생성하면 응답 속도가 늦어짐
->쓰레드는 컨텍스트 스위칭 비용 발생하다.
CPU 코어가 하나라 할 때 쓰레드가 2개면 코어 하나가 쓰레드 2개를 동시에 수행할 수는 없으므로 하나 실행 끝내고 그 다음 걸 실행한다. 여기서 다음 쓰레드로 전환할 때 비용이 발생하는 것을 컨텍스트 스위칭 비용이라 한다.
-> 쓰레드 생성에 제한이 없다.
고객의 요청이 너무 많이 오면 쓰레드가 계속 생성이 되서 CPU, 메모리 임계점을 넘어서 서버가 죽을 수 있다.





𖤐 쓰레드 풀

WAS 내부에 쓰레드 풀이라는 것을 사용한다.
요청이 오면 쓰레드 풀에 놀고 있는 쓰레드를 요청한다. (쓰레드 풀은 미리 쓰레드를 만들어 놓음.)

쓰레드를 사용하고 나서 죽이는 것이 아니라 다시 쓰레드 풀에 반납을 한다.
그래서 쓰레드를 죽이고 생성하는 방식이 아니라 빌려쓰고 다시 반납하는 방식을 사용한다.
만약 쓰레드 200개가 동시에 처리되고 있는데 요청이 200개 이상이 오면 나머지 요청들은 쓰레드 풀에 쓰레드 요구 시 대기하거나 거절을 당한다. 기다리는 요청은 거절하거나 특정 숫자만큼만 대기하도록 설정할 수 있다.
그리고나서 반납된 쓰레드를 통해 대기, 거절 당한 요청이 그 쓰레드를 사용

특징 : 필요한 쓰레드를 쓰레드 풀에 보관하고 관리.
쓰레드 풀에 생성 가능한 쓰레드의 최대치를 관리하고 톰캣은 최대 200개 기본 설정. (변경 가능)

장점 : 쓰레드가 미리 생성되어 있으므로 쓰레드 생성, 종료하는 비용이 절약되고 응답시간이 빠르다.
생성 가능한 쓰레드의 최대치가 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.





𖤐 쓰레드 풀 실무 팁

WAS의 주요 튜닝 포인트는 최대 쓰레드 수이다.
트래픽이 많을 때 잘 조절하는 것이 중요
(최대 쓰레드 수를 튜닝했을 때 극적인 효과를 볼 수 있는 확률이 굉장히 높다.)

이 값을 너무 낮게 설정할 경우
동시 요청이 많으면 서버 리소스는 여유롭지만, 클라이언트는 금방 응답 지연한다.
ex) 최대 쓰레드를 딱 10개만 설정하면 요청이 동시에 100개가 올 때 10개는 실행 중에 있고 90개는 대기상태로 있는다. 그러면 처리되는 것은 10개만이고 나머지는 대기 상태로 남으면서 장애가 발생한다.

이 값을 너무 많이 설정할 경우
동시 요청이 많으면, CPU, 메모리 리소스 임계점 초과로 서버 다운된다.





𖤐 쓰레드 풀의 적정 숫자

애플리케이션 로직은 조회 횟수에 따라서 활동량이 차이가 난다. CPU, 애플리케이션 로직 복잡도, 메모리, IO리소스 상황에 따라 다 다르다. 아무리 잘하는 사람도 최적의 해는 찾지 못함. (대략적으로 감을 잡을 뿐)
중요한 것은 결국 성능 테스트를 해봐야 한다.
가능한한 최대한 실제 서비스와 유사하게 성능 테스트를 시도.
성능 테스트 툴 : 아파치 ab, 제이미터, nGrinder(네이버에서 만들 것으로 좋음.)






𖤐 WAS가 멀티스레드 지원을 해준다. (핵심)

멀티 쓰레드에 대한 부분은 WAS가 처리
개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 된다.
마치 싱글 쓰레드 프로그래밍을 하듯이 편리하게 소스 코드를 개발.
멀티 쓰레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈) 는 주의해서 사용

🪭 싱글톤

객체를 하나만 생성해놓고 공유해서 사용하는 것
현재 나의 java JVM에는 이 객체 딱 하나만 존재하는 것.
그래서 여러 개의 new로 생성하지 않고 하나만 생성해서 모두가 다 공유해서 사용할 수 있도록 해주는 것.
HTTP Request, Response는 고객마다 다 데이터가 다르므로 객체는 HTTP 요청이 올 때마다 계속 새로 만드는 것이 맞다.
하지만 서블릿은 항상 생성할 필요 없고 메서드 안에 비즈니스 로직만 만들어서 다같이 재사용하면 된다.
요청이 올 때마다 계속 new해서 새로 생성하는 것은 비표율적.
모든 고객 HTTP 요청은 동일한 서블릿 객체 인스턴스에 접근하게 된다.

즉 같은 서버라면 누가 요청을 하든 다 같은 서블릿 인스턴스에 접근해서 로직을 실행하는 것.
-> 공유 변수 사용 주의






𖤐 HTTP API

데이터만 주고받음, UI 화면이 필요하면 클라이언트가 별도 처리
앱 클라이언트 to 서버
앱 개발자가 데이터만 요구할 때 WAS는 주문 DB 조회해서 JSON 데이터 가지고 앱 클라이언트에 내려준다.
데이터를 내려주는 것을 HTTP API라 한다.
똑같이 데이터를 올려주면 웹 클라이언트 to 서버.
서버 to 서버 데이터 주고 받음.
결제 서버에 결제해 달라고 하면 결제에 대한 결과를 JSON으로 주문 서버로 내려줌.

백엔드 개발자가 서비스를 제공할 때 고민 3가지
1.정적 리소스 어떻게 제공할지.
2.동적으로 제공하는 HTML 페이지 어떻게 제공할지.
3.HTTP API 어떻게 제공할지.





𖤐 SSR(서버 사이드 랜더링)

웹 브라우저에서 주문 내역 달라고 하면 서버는 주문 DB 조회해서 HTML 동적으로 다 생성한 다음에 최종적으로 HTML 서버에서 다 만든다. HTML 화면을 다 랜더링해서 HTTP 응답에 HTML 코드를 다 실어서 응답을 보내면 웹 브라우저는 받아서 "HTML이 왔네" 하고 HTML 랜더링해서 사용자들에게 보여준다.
HTML을 다 만드는 과정은 서버에서 끝내고 웹 브라우저는 단순하게 완전하게 다 생성된 것을 보여주기만 한다. 그래서 동적으로 만든 최종 결과물이 서버에서 다 생성이 된다고 해서 서버 사이드 랜더링이라 한다.





𖤐 CSR(클라이언트 사이드 랜더링)


웹 브라우저에서 서버측에 요청을 하는 것은 SSR과 같다. 그 후 HTML을 서버에서 응답을 하는데 HTML 안에 내용이 하나도 없다.
텅 빈 HTML을 내려주고 대신에 애플리케이션 구동에 대한 JS 링크를 내려준다.
JS 링크는 HTML 안에 포함된 자바스크립트 다운로드 링크를 말한다.
(HTML 안에는 보통 JS를 다운 받기 위한 링크가 포함되어 있다.)

이 링크를 내려주면 웹 브라우저가 JS를 서버에 요청을 한다. 이 JS 코드 안에는 클라이언트의 로직도 들어있고 HTML을 어떻게 JS로 렌더링 할지에 대한 로직도 들어있다. 서버에서 그것을 응답한다.

그러면 웹 브라우저가 HTTP API를 가지고 서버를 호출한다. 서버에서 주문 정보를 조율해서 JSON 데이터를 내려준다.

클라이언트 로직에서 데이터를 API로 다 조회했기 때문에 HTML 렌더링 코드에 섞어서 HTML을 웹 브라우저에서 만든다.

(웹 브라우저가 서버에 JS를 요청한다. 앞서 말한 JS를 웹 브라우저가 다운 받아야 실행할 수 있다.)

사용 권장

  1. SSR을 사용하는 경우
  • 네트워크가 느릴때 (CSR은 한번에 모든것을 불러오지만 SSR은 각 페이지 마다 불러오기 때문)
  • SEO(검색엔진 최적화)가 필요할 때.
  • 최초 로딩이 빨라야하는 사이트를 개발할 때
  • 메인 Script가 크고 로딩이 매우 느릴 때 CSR은 메인 스크립트가 로딩이 끝나면 API로 데이터 요청을 보낸다. 하지만 SSR은 한번의 요청에 아예 렌더가 가능한 페이지가 돌아온다.
  • 웹 사이트가 상호작용이 별로 없을 때
  1. CSR을 사용하는 경우
  • 네트워크가 빠를 때
  • 서버의 성능이 좋지 않을 때
  • 사용자에게 보여줘야하는 데이터의 양이 많을 때 (로딩창을 띄울 수 있는 장점이 있다.)
  • 메인 Script가 가벼울 때
  • SEO에 상관 없을 때
  • 웹 어플리케이션에 사용자와 상호작용할 것들이 많을 때(아예 렌더링 되지 않아 사용자의 행동을 막는 것이 경험에 오히려 유리함.)

https://velog.io/@lusate/CSR-vs-SSR






𖤐 무엇을 사용해야하는가?

유저랑 상호작용이 많고 고객의 개인 정보를 기준으로 이루어지는 서비스라면 검색엔진노출보다 고객의 데이터를 보호하는 것이 더 중요할 수 있다면 -> CSR

회사 홈페이지처럼 상위 노출이 필요하고 누구에게나 항상 같은 내용을 보여줘야 하며 매주 업데이트 되어야 한다면 -> SSR

SSR처럼 같지만 업데이트를 거의 하지 않아도 된다면 -> SSG

사용자에 따라 페이지 내용이 달라지며 화면 깜빡임 없는 빠른 인터렉션이 필요하고 상위노출이 필요하다면 -> CSR + SSR

데이터 가져오는 SSG
React에서는 useEffect를 통해 데이터를 가져오는데 Next.js에서는 useEffect를 사용하면 SSG로 작동하지 않습니다.
따라서 Next.js에서 SSG를 하려면, Next.js에서 제공하는 getStatPropsgetStaticPaths를 사용해야 합니다.

상위 노출이란
검색 엔진 결과 페이지에서 특정 웹사이트나 페이지가 상위에 나타나는 것을 의미합니다.
Google에서 특정 키워드를 검색하면 해당 키워드와 관련된 웹사이트가 검색 결과의 첫 페이지에 나타나는 것처럼.






𖤐 @ServletComponentScan (서블릿 자동 등록)

스프링 부트에서 서블릿 사용하려면 이 어노테이션 사용.
스프링이 자동으로 현재 패키지 포함해서 하위 패키지까지 뒤져서 서플릿을 다 찾아서 자동으로 등록해준다.





𖤐 GET 방식, POST 방식

GET 방식은 클라이언트의 데이터를 URL 뒤에 붙여서 보낸다.
ex) localhost:8080/request-param/username=""&age=20
키와 값의 쌍으로 들어가야 한다.

POST 방식은 데이터 전송을 기반으로 한 요청 메서드이다.
URL에 붙여서 보내지 않고 BODY에다가 데이터를 넣어서 보낸다.
ex)application/x-www-form-urlencoded
text/plain
multipart/form-data
컨텐트 타입을 꼭 명시해주어야 한다.


@WebServlet(name = "helloServlet", urlPatterns = "/hello")

해당 어노테이션 안에 경로를 입력하면 클라이언트에서 해당 경로를 입력할 때 톰캣서버가 알아서 찾아서 실행한다.
/hello 하면 실행된다.


public class HelloServlet extends HttpServlet { //서블릿은 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);
        //super.service(req, resp); //Ctrl + O로 생성.


        //쿼리 파라미터를 서블릿이 쉽게 읽을 수 있도록 지원해준다.
        String username = request.getParameter("username");
        System.out.println("username = " + username);

        //응답 메시지 보냄. 응답은 HttpServletResponse에 찍어줌.
        //값을 넣으면 웹 브라우저에 응답하는 response 메시지에 데이터가 담겨서 나가게 된다.
        response.setContentType("text/plain"); //단순 문자로 보냄
        response.setCharacterEncoding("utf-8"); //여기 2개는 헤더 정보에 들어감.

        response.getWriter().write("hello" + username); //Http 메시지 Body에 데이터가 들어간다.
    }
}
//HTTP 요청이 오면 서블릿 컨테이너가 요청, 응답 객체를 만들어서 서블릿에 던져준다.




𖤐 httpServletRequest

역할 : HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만 매우 불편하다.
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.

☪ @WebServlet

어노테이션 이용하여 서블릿 매핑
ex) @webServlet(name = "서블릿 이름", value = "/서블릿 매핑 이름")

☪ Servlet

서블릿이란 : 자바를 사용하여 페이지를 동적으로 생성하는 서버측 프로그램으로, HTTP 요청과 응답 처리를 위한 스프링에서 제공하는 핵심 기능.
HTTP로 통신할 때 많은 과정들이 존재하는데, 서블릿은 그 과정을 간소화 시켜주며 개발자가 의미있는 비즈니스 로직에 집중할 수 있도록 도와준다.

업로드중..



서블릿 simple 코드

//해당 어노테이션 안에 경로를 입력하면 클라이언트에서 해당 경로를 입력할 때 톰캣서버가 알아서 찾아서 실행한다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
// /hello 하면 실행됨.
public class HelloServlet extends HttpServlet { //서블릿은 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);
        //super.service(req, resp); //Ctrl + O로 생성.


        //쿼리 파라미터를 서블릿이 쉽게 읽을 수 있도록 지원해준다.
        String username = request.getParameter("username");
        System.out.println("username = " + username);

        //응답 메시지 보냄. 응답은 HttpServletResponse에 찍어줌.
        //값을 넣으면 웹 브라우저에 응답하는 response 메시지에 데이터가 담겨서 나가게 된다.
        response.setContentType("text/plain"); //단순 문자로 보냄
        response.setCharacterEncoding("utf-8"); //여기 2개는 헤더 정보에 들어감.

        response.getWriter().write("hello" + username); //Http 메시지 Body에 데이터가 들어간다.
    }
}
//HTTP 요청이 오면 서블릿 컨테이너가 요청, 응답 객체를 만들어서 서블릿에 던져준다


☪ Request 헤더 정보 조회 (HttpServletRequest 기본 사용법)

@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //호출하는 부분
        printStartLine(request); //start 라인
        printHeaders(request); //헤더 정보
        printHeaderUtils(request); //헤더를 편리하게 조회
        printEtc(request); //기타 부가적 정보 조회
    }

    //http://localhost:8080/request-header?username=hello 창에 입력하면
    //request.getQueryString() = username=hello 정보가 출력된다..
    private static void printStartLine(HttpServletRequest request) {
        System.out.println("--- REQUEST-LINE - start ---");

        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 사용 유무
        System.out.println("--- REQUEST-LINE - end ---");
        System.out.println();
    }

    //Header 모든 정보
    private void printHeaders(HttpServletRequest request) {
        System.out.println("--- Headers - start ---");

        // Header 정보 가져오는 방법 (옛날 방식)
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            //다음 요소가 있으면
            String headerName = headerNames.nextElement(); // 값을 꺼냄.
            System.out.println(headerName + " = " + headerName);
        }

        // Header 정보 가져오는 방법 (요즘 스타일)
        request.getHeaderNames().asIterator()
                .forEachRemaining(headerName -> System.out.println("headerNames = " + headerNames));

        //헤더 하나만 조회
        request.getHeader("host");

        System.out.println("--- Headers - end ---");
        System.out.println();
    }

    //헤더를 편리하게 조회하는 기능
    private void printHeaderUtils(HttpServletRequest request) {
        System.out.println("--- Header 편의 조회 start ---");
        System.out.println("[Host 편의 조회]");

        //Host 헤더 -> localhost 출력
        System.out.println("request.getServerName() = " + request.getServerName());
        //Host 헤더 -> 8080
        System.out.println("request.getServerName() = " + request.getServerPort());
        System.out.println();

        System.out.println("[Accept-Language 편의 조회]");
        request.getLocales().asIterator()
                .forEachRemaining(locale -> System.out.println("locale = " + locale));
        /*
        locale = ko
        locale = en_US
        locale = en
        locale = ko_KR 을 출력
         */
        System.out.println("request.getLocale() = " + request.getLocales());
        //위 4개 중 가장 위에 것을 출력.
        System.out.println();


        //쿠키
        System.out.println("[cookie 편의 조회]");
        if(request.getCookies() != null){
            for (Cookie cookie : request.getCookies()) {
                System.out.println(cookie.getName() + ": " + cookie.getValue());
            }
        }
        System.out.println();

        System.out.println("[Content 편의 조회]");
        System.out.println("request.getContentType() : " + request.getContentType());
        //클라이언트가 요청 정보를 전송할 때 사용한 컨텐트의 타입을 구한다.
        //컨텐트 타입 : 전송되는 내용이 무엇인지 알려줌.
        //get 방식은 컨텐트을 보내지 않음. 그래서 null임.
        System.out.println("request.getContentLength() : " + request.getContentLength());
        //클라이언트가 전송한 요청정보의 길이를 구한다.
        System.out.println("request.getContentType() : " + request.getCharacterEncoding());
        //클라이언트가 요청 정보를 전송할 때 사용한 문자셋의 인코딩을 구한다.
        System.out.println();
    }

    //기타 정보 조회
    private void printEtc(HttpServletRequest request) {
        System.out.println("--- 기타 조회 start ---");
        //요청이 온 것에 대한 정보 -> 메시지가 아닌 네트워크 커넥션이 맺어진 것에 대한 정보
        System.out.println("[Remote 정보]");
        System.out.println("request.getRemoteHost() = " + request.getRemoteHost());
        System.out.println("request.getRemoteAddr() = " + request.getRemoteAddr());
        System.out.println("request.getRemotePort() = " + request.getRemotePort());

        //나의 서버에 대한 정보.
        System.out.println("[Local 정보]");
        System.out.println("request.getLocalName = " + request.getLocalName());
        System.out.println("request.getLocalAddr = " + request.getLocalAddr());
        System.out.println("request.getLocalPort = " + request.getLocalPort());
    }

}


𖤐 HTTP 요청 보내는 방법은 총 3가지

요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달.

☪ Http 요청 데이터 (GET 쿼리 파라미터, POST Html Form, HTTP message body)

@Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //모든 요청 파라미터 꺼냄 -> username=hello&age=20 2개 모두 꺼냄
        System.out.println("[전체 파라미터 조회] - start");
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName ->
                        System.out.println(paramName + " = " + request.getParameter(paramName)));
        //paramName 은 username을 즉 키이고, request.getParameter(paramName) 은 hello로 값을 꺼내는 것이다.
        //http://localhost:8080/request-param?username=hello&age=20 라 해주면 파라미터 생성됨.
        System.out.println("[전체 파라미터 조회] - end");


        // 단일 파라미터 조회
        System.out.println("[단일 파라미터 조회]");
        String username = request.getParameter("username");
        System.out.println("username = " + username);

        String age = request.getParameter("age");
        System.out.println("age = " + age);
        System.out.println();

        //http://localhost:8080/request-param?username=hello&age=20&username=hello2
        //이런 식으로 하나의 파라미터 이름에 여러 값들을 넘길 수 있다.
        System.out.println("[이름이 같은 복수 파라미터 조회]");
        String[] usernames = request.getParameterValues("username");
        for (String name : usernames) {
            System.out.println("username = " + name);
        }
        /*
        하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용해야 한다.
        파라미터 이름은 하나인데 값이 중복될 경우 request.getParameterValues() 를 사용한다.
        여러가지 이름이 있으면 getParameterValues로 꺼낸다.
        근데 중복으로 보내는 경우는 거의 없긴 함.
        출력
        [이름이 같은 복수 파라미터 조회]
        username = hello
        username = hello2
         */

        response.getWriter().write("ok");
        //메시지 body에 ok
    }

POST방식으로 HTML Form 에 데이터 입력해서 요청 데이터 처리하는 방법
주로 회원 가입, 상품 주문 등에서 사용하는 방식.
메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달한다.
ex)username=hello&age=20

html form이 있어야 함. -> hello-form.html을 생성함.

request.getParameter는 2가지를 다 지원한다. GET 쿼리 파라미터 데이터도 꺼낼 수 있고 POST방식의 파라미터 데이터도 꺼낼 수 있다.
파라미터 데이터 -> username=hello 이런 것들.


앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다. 따라서 쿼리 파라미터 조회 메서드를
그대로 사용하면 된다.
클라이언트(웹 브라우저) 입장에서는 두 방식에 차이가 있지만 서버 입장에서는 둘의 형식이 동일하므로
request.getParameter()로 편리하게 구분없이 조회할 수 있다.
정리하면 request.getParameter()는 GET URL 쿼리 파라미터 형식도 지원하고,
POST HTML From 형식도 지원한다.


참고 : content-type은 HTTP 메시지 바디의 데이터 형식을 지정한다.
GET URL 쿼리 파라미터 형식으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를
사용하지 않기 때문에 content-type이 없다.
POST HTML From 형식으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에
바디에 포함된 데이터가 어떤 형식인지 content-type을 꼭 지정해야 한다. 이렇게 폼으로 데이터를
전송하는 형식을 application/x-www-form-urlencoded 라 한다.

간단한 테스트에 HTML Form을 만들기는 힘들다. 그래서 이 때는 Postman 을 사용하면 된다.

Postman 에서 Body -> x-www-form-urlencoded 선택하고 Headers에서 content-type 확인.



𖤐 HTTP 요청 데이터 (단순 텍스트)

메시지 바디에 내가 원하는 데이터를 직접 실어서 서버에 전송.

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream(); //InputStream으로 데이터 읽을 수 있다.
        //메시지 바디의 내용을 바이트 코드로 바로 얻어올 수 있다.

        //바이트 코드를 스트림으로 변환
        //꺼낼 때 인코딩 정보가 무엇인지 알려줘야 한다.
        //(항상 바이트 코드를 문자로 변환하면 어떤 인코딩인지 알려주어야 한다. 반대로가 되도 알려주어야 함.)
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        System.out.println("messageBody = " + messageBody);
        response.getWriter().write("ok");
        //postman에서 확인.
    }

☪ InputStream

바이트 단위 입출력을 위한 최상위 입출력 스트림 클래스

𖤐 HTTP 요청 데이터 (JSON)

메시지 바디에 내가 원하는 데이터를 직접 실어서 서버에 전송.

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream(); //InputStream으로 데이터 읽을 수 있다.
        //메시지 바디의 내용을 바이트 코드로 바로 얻어올 수 있다.

        //바이트 코드를 스트림으로 변환
        //꺼낼 때 인코딩 정보가 무엇인지 알려줘야 한다.
        //(항상 바이트 코드를 문자로 변환하면 어떤 인코딩인지 알려주어야 한다. 반대로가 되도 알려주어야 함.)
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        System.out.println("messageBody = " + messageBody);
        response.getWriter().write("ok");
        //postman에서 확인.
    }

☪ ObjectMapper

JSON 컨텐츠를 Java 객체로 변환하거나 반대로 Java 객체를 JSON으로 serialization 할 때 사용하는 Jackson 라이브러리의 클래스이다.

@WebServlet(name = "RequestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {

    //스프링은 json 라이브러리를 기본으로 jackson을 사용
    private 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);

        //값을 읽을 수 있음. (객체로 변환할 때는 JSON라이브러리가 필요.)
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        System.out.println("helloData.username = " + helloData.getUsername());
        System.out.println("helloData.age = " + helloData.getAge());

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

𖤐 Request 헤더 정보 조회 (HttpServletResponse 기본 사용법)

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //[Status-line]
        response.setStatus(HttpServletResponse.SC_OK); //200

        //[response-header]
        //한글 깨짐 방지 -> charset-utf-8
        response.setHeader("Content-type", "text/plain;charset-utf-8");
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        //"no-cache, no-store, must-revalidate" -> 캐시를 무효화
        response.setHeader("Pragma", "no-cache");
        response.setHeader("my-header", "hello");
        //내가 원하는 임의의 헤더 만들기도 가능. HTTP 응답의 헤더에 실려 나감.

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

        PrintWriter writer = response.getWriter();
        writer.println("ok");
    }

    private void content(HttpServletResponse response) {
        //Content-Type: text/plain;charset=utf-8
        //Content-Length: 2
        //response.setHeader("Content-Type", "text/plain;charset=utf-8");
        //위 처럼 setHeader()했는데 밑에처럼 하면 자동으로 생성할 수 있음.
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        //response.setContentLength(2); 생략 시 자동 생성.
        //writer.println("ok"); ln 해서 3이 자동 생성됨.
    }

    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);
        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");
        //바로 위 2줄 처럼 해도 되지만 한 줄로도 가능
        response.sendRedirect("/basic/hello-form.html");

    }

𖤐 HTTP 응답 데이터 (단순 텍스트, HTML)

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 : 바이트를 문자 형태를 가진 객체로 바꿔준다.
        // getWriter() 메소드를 통해 응답으로 내보낼 출력 스트림을 얻어낸 후
        // out.print(HTML 태그) 형태로 작성하여 스트림에 텍스트를 기록한다

        //서블릿으로 html을 랜더링할 때는 직접 html 코드를 작성해야 한다.
        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<body>");
        writer.println("<div>안녕???</div>");
        writer.println("</body>");
        writer.println("</html>");
    }

𖤐 HTTP 응답 데이터 (JSON)

private ObjectMapper objectMapper = new ObjectMapper();

    @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("Park");
        helloData.setAge(26);

        //json으로 형태를 바꿔야 한다.
        //{"username":"Park", "age":26} 이런 식으로
        //json으로 바꾸거나 json을 자바 객체로 바꿀 때는 ObjectMapper 사용
        String result = objectMapper.writeValueAsString(helloData);
        //객체를 String 타입으로 변환.

        response.getWriter().write(result);

    }

𖤐 템플릿 엔진

서블릿과 자바 코드만으로 HTML을 만드는 것이 아니라 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣어서 편리하게 한다.
이게 템플릿 엔진이 나온 이유고 이것을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있다.
템플릿 엔진에는 Thymeleaf, Freemarker, Velocity 등이 있다.



𖤐 변경 주기가 다르면 분리한다.

UI 변경하는 일과 비즈니스 로직 변경하는 일은 대부분 따로따로 일어나고 서로에게 영향을 주지 않는다.
변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수에 좋지 않다.
그래서 MVC 패턴이 생김.





𖤐 Model View Controller

MVC 패턴은 하나의 서블릿이나 JSP로 처리하던 것을 컨트롤러와 뷰라는 영역으로 서로 역할을 나눈 것이다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.

컨트롤러 : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
모델 : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 랜더링 하는 일에 집중할 수 있다.
뷰 : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.



☪ redirect 와 forward 차이

리다이렉트는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.



𖤐 프론트 컨트롤러 패턴 특징

프론트 컨트롤러도 서블릿이다. 프론트 컨트롤러를 앞에다 두고 이 서블릿 하나로 클라이언트의 요청을 다 받는다. 그리고 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출한다
입구를 하나로 만들 수 있고 공통 처리가 가능하다.
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.

스프링 웹 MVC의 핵심도 프론트 컨트롤러이다.
DispatcherServet이 프론트 컨트롤러 패턴으로 구현되어 있다.



☪ v1, v2, v3, v4 로 mvc 프레임워크를 생성했다.

뷰 리졸버 : 눈리 뷰 이름을 실제 물리 뷰 경로로 변경한다.
ex) "/WEB-INF/views/new-form.jsp" -> new-form




𖤐 v5

v1~v4 까지 프론트 컨트롤러는 오직 Controller 인터페이스 하나만을 가지고 사용했다.
ControllerV3, ControllerV4는 완전히 다른 인터페이스이다. 따라서 호환이 불가능한데 여기서 사용하는 것이 바로 어댑터
어댑터 패턴을 사용하면 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해 줄 수 있다.

핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 이유는 어댑터라는 것이 들어오면 어떠한 종류든지 다 처리할 수 있다. 그래서 컨트롤러의 개념 뿐만 아니라 다른 것들도 다 처리할 수 있어서 넓은 범위로 핸들러 라고 함.
핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가된다. 여기서 어댑터 역할을 하는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
프론트 컨트롤러가 핸들러 어댑터를 통해서 핸들러(컨트롤러)를 호출을 하고 핸들러 어댑터가 결과를 받아서 ModelView를 프론트 컨트롤러에게 반환해준다.
렌더링이란 서버로부터 HTML 파일을 받아 브라우저에 뿌려주는 과정



handler는 컨트롤러이다.
support는 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.

return(handler instanceof ControllerV3);

handler가 ControllerV3의 인스턴스인지 물어봄.
그래서 ControllerV3의 인터페이스를 구현한 무언가가 넘어오게 되면 참 반환.


handle 메서드는 핸들러를 호출해주고 반환은 ModelView로 맞춰서 반환해준다. (V3의 경우)
각 버전에 맞춰서 반환해야 한다.
실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
이전에는 프론트 컨트롤러가 실제 큰트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.





𖤐 Spring MVC

위의 v1~v5 들은 직접 만든 프레임워크이다. 스프링 MVC와 다른 점은
FrontController -> DispatcherServelt
handlerMappingMap -> HandlerMapping
MyHandlerAdapter -> HandlerAdapter
ModelView -> ModelAndView
viewResolver -> ViewResolver
MyView -> View



DispachdrServlet 서블릿 등록

디스패쳐 서블릿이 스프링 MVC의 핵심이다.
DispacherServlet 도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고 서블릿으로 동작한다.
상속관계 : DispatcherServlet -> FrameworkSevlet -> HttpServletBean -> HttpServlet
스프링 부트는 DispacherServlet을 서블릿으로 자동으로 등록하면서 모든 경로 (urlPatterns="/") 에 대해서 매핑한다.
*더 자세한 경로가 우선순위가 높다. 그래서 기존에 등록한 서블릿도 함께 동작한다.
그래서 DispatcherServlet이 우선순위가 더 낮음.


요청 흐름

서블릿이 호출되면 HttpServlet이 제공하는 service() 가 호출된다.
서블릿 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다.
FrameworkServlet.service() 를 시작으로 여러 메서드가 호출되면서 DispacherServlet.doDispatch() 가 호출된다.



*****

𖤐 Spring MVC 구조의 동작 순서

  1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.

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

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

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

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

  6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다.

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

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



*****

𖤐 doDispatch()

  1. 핸들러 조회
  2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터를 찾음
  3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행
  4. 최종적으로 ModelAndView를 반환.


*****

𖤐 개념 잡기

Front Controller
요청 URI에 따라 알맞는 컨트롤러를 선택하여 호출하는 것이다. 이 역할의 핵심이 핸들러 매핑과 핸들러 어댑터이다.

컨트롤러
클라이언트의 요청을 실제로 처리하는 것

핸들러
DispatcherServlet으로 받은 요청을 Controller로 보내지는데 이 요청이 어떤 룰으로 컨트롤러에 보내는지(매핑하는지), 그 방법을 정해주는 클래스이다.

핸들러 매핑
Dispatcher Servlet이 요청 URI가 어떤 핸들러와 매핑되는지 찾는 과정이다.
핸들러 매핑은 결국 요청과 알맞는 핸들러 객체를 Dispatcher Servlet에 리턴한다.

핸들러 어댑터
핸들러 매핑에서 리턴받은 핸들러 객체를 가지고 이에 맞는 어댑터를 찾는 과정이다. 어댑터란 2개 이상의 인터페이스에 스펙이 맞지 않을 때 중간에 이 스펙을 맞도록 변환해주는 역할을 하는 객체이다.

뷰 리졸버
View를 어떤 것을 사용할 지 설정을 할 수 있도록 해주는 역할로 실행할 뷰를 찾는다.

핸들러 어댑터가 필요한 이유

어떤 개발자는 컨트롤러의 리턴 타입을 String으로 하고 싶고 또 어떤 개발자는 리턴 타입을 ModelAndView로 개발하고 싶을 수 있다. 컨트롤러의 매개변수도 마찬가지로, 어떤 개발자는 서블릿의 HttpServletRequest와 HttpServletResponse로 받고 싶고, 또 어떤 개발자는 매개변수를 받지 않거나 Model만 받고 싶어할 수도 있다. 이 모든 요구 사항을 맞추기 위해서 어댑터 패턴이 필요한 것이다.


핸들러 어댑터는 컨트롤러에서 String으로 응답받든, Model로 응답받든, 무조건 Dispatcher Servlet에서 ModelAndView객체로 응답을 해줘야하는 역할이 있는 것이다.



☪ 컨트롤러 호출 방법

컨트롤러 호출되려면 HandlerMapping, HandlerAdapter 2가지가 필요하다.
핸들러 매핑에서 Controller를 찾아와야 한다.
스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
그리고 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다. 개발자가 직접
핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다.



☪ 스프링 부트를 사용하면 자동으로 핸들러 매핑과 어댑터를 여러 개 등록해준다. (자동 등록 순서)

핸들러 매핑 클래스
1. RequestMappingHndlerMapping -> 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
2. BeanNameUrlHandlerMapping -> 스프링 빈의 이름으로 핸들러를 찾는다.

핸들러 어댑터
1. RequestMappingHandlerAdapter -> 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
2. HttpRequestHandlerAdapter -> HttpRequestHandler 처리
3. SimpleControllerHandlerAdapter -> Controller 인터페이스





𖤐 스프링 MVC 시작

스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터이다.
@RequestMapping
-> RequestMappingHandlerMapping
-> ReauestMappingHandlerAdapter


참고 : JSP를 사용하지 않기 때문에 Jar를 사용.
War는 보통 톰캣을 별도로 설치해서 빌드된 파일을 넣을 때 사용.




☪ @Controller

  1. 스프링이 자동으로 스프링 빈으로 등록해준다. @Component를 쓰면 자동으로 ComponentScan의 대상이 되서 스프링 빈으로 등록이 된 것처럼.
  2. 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다. -> @Controller가 있으면 RequestMappingHandlerMapping 에서 핸들러 정보로 인식하여 꺼낼 수 있는 대상이 된다는 의미.

☪ @Controller의 역할은 Model 객체를 만들어 데이터를 담고 View를 찾는 것이지만, @RestController는 단순히 객체만을 반환하고 객체 데이터는 JSON 또는 XML 형식으로 HTTP 응답에 담아서 전송


☪ @RequestMapping

요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지어도 된다.

1. @RequestMapping(value="경로", method=RequestMethod.GET) 처럼 긴 코드가 @GetMapping("경로") @PostMapping("경로") 이런 식으로 짧게 줄일 수 있다.

2. url의 중복 사용으로  url로 방식이 다른 여러 개를 매핑이 가능.

3. @requestMapping에서는 List형태의 데이터를 바로 보낼수 없지만 @Post는 가능하다.

☪ ModelAndView

모델과 뷰 정보를 담아서 반환하면 된다.


RequestMappingHandlerMapping 은 스프링 빈 중에서 @RequestMapping 또는 @Controller 가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.
(꼭 클래스 레벨이 있어야 인식을 할 수 있다.)



스프링 MVC V2

@Controller
@RequestMapping("/springmvc/v2/members") //합칠 수 있다.
public class SpringMemberControllerV2 {
    //@RequestMapping을 메서드 단위로 해서 3개의 클래스를 만들었는데
    //컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.

    private MemberRepository memberRepository = MemberRepository.getInstance();

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

    //V1에 SpringMemberSaveControllerV1의 @RequestMapping
    @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-result");
        //mv.getModel().put("member", member);
        //addObject(String name, Object value)
        mv.addObject("member", member);// 바로 위 코드와 같은 것
        // 스프링이 제공하는 ModelAndView를 통해 Model 데이터를 추가할 때 사용
        //이후 뷰를 랜더링할 때도 사용.

        return mv;
    }


    //V1에 SpringMemberListControllerV1의 @RequestMapping
    @RequestMapping
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");

        //mv.getModel().put("member", member);
        //addObject(String name, Object value)
        mv.addObject("members", members);

        return mv;
    }
}

메서드 마다 ModelAndView 를 계속 만들고 반환하는 반복이 있다. 그래서 스프링 프레임워크 생성할 때의 V3 -> V4 처럼 실용적으로 바꿀 수 있다.

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

    //V1에 SpringMemberFormControllerV1의 @RequestMapping
    @RequestMapping("/new-form")
    public String newForm() {
        //return new ModelAndView("new-form");
        //ModelAndView 가 아닌 String으로 반환 ->
        //애노테이션 기반 컨트롤러는 ModelAndView를 반환해도 되고 문자를 반환해도 된다.
        //그러면 뷰 이름으로 알고 프로세스가 진행이 된다.
        return "new-form";
    }

    //V1에 SpringMemberSaveControllerV1의 @RequestMapping
    @RequestMapping("/save")
    //request, response 받았는데 유연해서 파라미터를 직접 받을 수도 있다. -> @RequestParam 써서
    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-result";
    }


    //V1에 SpringMemberListControllerV1의 @RequestMapping
    @RequestMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);

        return "members";
    }
}


☪ @RequestParam

스프링의 HTTP 요청 파라미터를 @RequstParam으로 받을 수 있다.
@RequestParam("username") 은 request.getParameter("username") 과 거의 같은 코드이다
GET, POST 방식 모두 지원한다.


GET, POST 모두 할 수가 있어서 구분하는 것이 좋은 설계다.

@RequestMapping(value = "/new-form", method = RequestMethod.GET)

이렇게 하면 GET으로만 호출이 가능하다.
너무 길어서 다른 방법을 사용하기도 한다.

@GetMapping("/new-form")
@PostMapping("/save")




𖤐 로깅(logging)

@RestController
그냥 @Controller 일 때는 반환할 때 View 이름이 반환이 되었는데
@RestController 라고 하면 문자 반환할 때 그대로 String으로 반환이 된다.


로그를 사용하면 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
과거에는 System.out.println()으로 출력했었음.

지금은 로그를 쓰면 이렇게 출력함.

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

결과
2022-10-05 16:46:34.004 INFO 9112 --- [nio-8080-exec-3] hello.springmvc.basic.LogTestController : info log = Spring


log.trace("trace log = " + name);
대괄호를 꼭 써줘야 한다. 자바는 메서드(log.trace)를 호출하기 전에 + 연산을 한다.
연산을 사용하면서 메모리도 가져가고 CPU도 사용을 한다. 만약 trace를 사용하지 않는다고 하면 출력을 하지도 않는데 연산을 해버리면서 쓸모없는 리소스를 사용하게 된다.
대괄호 있으면 메서드 호출해서 파라미터로 넘기기만 하므로 아무 연산이 일어나지 않는다.


로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
properties 설정을logging.level.hello.springmvc=info 로 해서 info 이상의 레벨들(warn, error) 만 출력이 된다.
하지만 System.out.println()은 무조건 로그 다 출력하여 운영 시에 로그 다 남기면 로그 폭탄 맞는다.
그래서 핵심은 애플리케이션 코드를 건드리지 않고 properties에서 설정만으로 로그 레벨을 정해줄 수 있다는 것이다.



시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.


@Slf4j 를 써주면 이거 쓰지 않아도 된다.

private final Logger log = LoggerFactory.getLogger(LogTestController.class);

log.trace, debug, info, warn, error 등이 있음





𖤐 요청 매핑

@GetMapping : 조회,
@PostMapping : 등록, 주어진 URI 표현식과 일치하는 HTTP POST 요청을 처리
@PutMapping : 여러 번 호출할 경우, 클라이언트가 받는 응답은 동일하다. (멱등성)
@PatchMapping : 정보 수정
@DeleteMapping : 삭제




𖤐 @PathVariable (경로 변수) (중요)

PathVariable 사용
변수명이 같으면 생략 가능
@PathVariable("userId") String userId -> @PathVariable userId
* ex) 요청 URL이 /mapping/userA 이런 식으로 URL 자체에 어떤 값이 들어가는 것이다.

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

HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적이다.


@PathVariable 의 이름과 파라미터 이름이 같으면 생략할 수 있다.
String data 가 아닌 String userId 라면

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

☪ 특정 헤더로 조건 매핑

Postman에서 Send하면 404에러가 발생한다.
Header는 넣어주면 해결된다.
Postman -> Headers -> headers 정해준 것처럼 mode = debug 설정.

  • 특정 헤더로 추가 매핑
    • headers = "mode"
    • headers = "!mode" -> header 이름이 없어야 함.
    • headers = "mode=debug" -> header key, value가 다 있어야 함.
    • headers = "mode!=debug" -> key, value가 맞으면 안 됨.
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }
    //Postman에서 Send하면 404에러가 발생한다.
    //Header는 넣어주면 해결된다.
    //Postman -> Headers -> headers 정해준 것처럼 mode = debug 설정.

☪ 미디어 타입 조건 매핑 (Content-Type 헤더 기반)

/**
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";
    }
    //header의 Content-Type이 application/json일 때만 호출이 된다.

HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.

☪ 미디어 타입 조건 매핑 (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 헤더가 필요하다.
HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.
그래서 produces = "text/html 이면 Postman 에서 Headers 에서 Accept를 text/html 로 바꿔준다. 이것은 클라이언트가 Content-Type이 text/html을 받아들일 수 있다는 의미이다.

미디어 타입의 조건 매핑에 consume 과 produces 가 있다.
consume 은 요청 헤더의 Content-Type, produces는 요청 헤더의 Accept 기반으로 매핑이 된다.





𖤐 HTTP 요청 기본, 헤더 조회

HttpMethod 가 GET, POST ... 이다.
MultiValueMap : 하나의 키에 여러 값을 받을 수 있다.
같은 헤더에 같은 값들이 들어갈 수 있으므로 MultiValueMap으로 받았다.

@Slf4j // 이거 하면 log 찍을 수 있음
@RestController
//응답 값을 View를 찾지 않고 바로 문자 그대로의 http 응답을 찾음
public class RequestHeaderController {
    @RequestMapping("/headers")
    //스프링의 애노테이션 기반 컨트롤러는 다양한 파라미터를 받아들일 수 있다.
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale, //언어
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          //헤더 정보를 한번에 다 받음
                          @RequestHeader("host") String host,
                          //헤더 정보 하나 받음. ("host" 필수)
                          @CookieValue(value = "myCookie", required = false) String cookie) {
                            //myCookie 가 쿠키 이름

        log.info("request = {}", request);
        log.info("response = {}", response);
        log.info("httpMethod = {}", httpMethod);
        log.info("Locale = {}", locale);
        log.info("headerMap = {}", headerMap);
        log.info("header host = {}", host);
        log.info("myCookie = {}", cookie);
        return "ok";
    }
}




𖤐 요청 파라미터 조회하는 방법

  1. GET - 쿼리 파라미터
  2. POST - HTML From
  3. HTTP message body
    @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");
    }

𖤐 @RequestParam 사용해서 요청 파라미터 편리하게 사용

@Controller 이면서 String을 반환하면 "ok"라는 view를 찾게 된다.
그러면 @RestController로 바꾸던지 메서드를 @ResponseBody로 하던지 하면 된다.
@ResponseBody는 "ok" 라는 문자를 그대로 http 응답 메시지에 넣어서 바로 반환한다. (@RestController와 같은 효과)

@ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge) {

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


☪ 생략 가능

변수명이 요청 파라미터 이름과 같도록 해야 함.
memberName -> 파라미터 이름과 같도록 username으로 수정.
memberAge -> age

단 String, int, Integer 같은 단순 타입이면 @RequestParam도 생략 가능 (그래도 애노테이션 @RequestParam 넣는 것이 나음 나는.)

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


☪ 필수 파라미터 여부 (required = true/false)

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


required = true 일 경우 URL에 그 변수가 없으면 400 예외 발생.
즉 username이 true 인데
localhost:8080/request-param-required?age=24 라면 400 예외 발생.
(URL에 username 변수가 없기 때문.)

int 는 null이 불가능하므로 Integer를 써야한다.
/request-param?username= 으로 username 파라미터 이름만 있고 값이 없는 경우 "" 로 빈문자로 통과하게 된다. 그래서 이 경우에는 400 예외가 발생하지 않는다.



☪ defaultValue

값이 없어도 defaultValue를 통해 기본값을 줄 수 있다.

@ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username,
            //username이 없을 경우 기본값으로 guest를 주겠다는 의미
            @RequestParam(required = false, defaultValue = "-1") int age) {
        log.info("username = {}, age = {}", username, age);
        return "ok";
    }

defaultValue가 있으면 값이 있든 없든 어차피 defaultValue로 들어가기 때문에 required는 의미가 없음.

참고 : 빈 문자의 경우에도 설정한 기본 값이 적용된다.
ex) localhost:8080/request-param-default?username= 으로 빈문자를 줘도 값이 default인 guest로 들어와서 ok가 됨.



☪ RequestParam Map (모든 요청 파라미터 다 받고 싶음)

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

파라미터를 Map, MultiValueMap으로 조회할 수 있다.
ex) MultiValueMap 은 (key=userId, value=[id1, id2])
파라미터 값은 보통 1개를 주로 사용. 그러므로 Map을 사용.
만약 그렇지 않다면 MultiValueMap 사용.





𖤐 요청 파라미터 @ModelAttribute

요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주었다.

스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다.
스프링 MVC @ModelAttribute 과정

  1. HelloData 객체를 생성.
  2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력한다. (프로퍼티는 getter, setter)
  • ex) 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력

@ModelAttribute 역할
@ModelAttribute 가 밑에 set을 자동으로 만들어준다. 그래서 삭제가 가능하다

@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model){
	/**
    * @ModelAttribute 가 밑에 set을 자동으로 만들어준다. 그래서 삭제 가능
    */
	/*Item item = new Item();
    item.setItemName(itemName);
    item.setPrice(price);
    item.setQuantity(quantity);*/
    
    itemRepository.save(item);
    
    // model.addAttribute(item); 자동 추가, 생략 가능
    
    return "basic/item";
}

@ModelAttribute("item") "item" 이라는 네임 속성을 넣어줬는데 이것은 자동으로 model.addAttribute("item")를 해준다.
즉 Model에 넣어주는 역할도 하는 것이다. 그래서 생략 가능하다.



@ModelAttribute의 네임 속성 제거

@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, Model model){
    itemRepository.save(item);
    
    // model.addAttribute(item); 자동 추가, 생략 가능
    
    return "basic/item";
}

"item" 네임 속성을 지워버리면 클래스명인 Item을 첫 글자만 소문자로 바꿔서 Model에 넣어준다.


@PostMapping("/add")
public String addItemV4(Item item, Model model){
    itemRepository.save(item);
    
    return "basic/item";
}

@ModelAttribute 생략 가능 (그냥 없애줘도 된다.)
@RequestParam도 생략 가능하다. String이나 int 같은 단순 타입이라면 RequestParam이 자동 적용되고 Item처럼 의미의 객체라면 ModelAttribute가 적용된다.
(클래스명에서 앞 문자만 소문자로 적용되는 것은 똑같다.)

@RequestParam도 생략 가능하므로 혼란이 올 수 있음. 그래서 String, int, Integer 같은 단순 타입은 @RequestParam 사용, 나머지는 @ModelAttribute 사용. (Item처럼 객체로 있는 것들)


☪ 프로퍼티

객체에 getUsername(), setUsername() 메서드가 있으면, 이 객체는 username이라는 프로퍼티를 가지고 있다.
ex) getXxx -> xxx (getXxx 가 getUsername, xxx 는 username)


@Data

@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 를 자동으로 적용해준다.





𖤐 Http 요청 메시지 (HttpEntity)

스프링 MVC는 HttpEntity를 지원한다.
HttpEntity는 HTTP header, body 정보를 편리하게 조회하게 해준다.
응답에도 사용 가능하고 메시지 바디 정보 직접 반환한다.
헤더 정보 포함 가능하고 view 조회하지 X.

@PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
        //http content에 있는 메시지 바디를 꺼내는 것이다.
        String messageBody = httpEntity.getBody();

        log.info("messageBody={}", messageBody);
        return new HttpEntity<>("ok");
    }

요청 파라미터와 HttpEntity 확실하게 구분
요청 파라미터는 GET에 쿼리 파라미터 오는 것 또는 POST 방식으로 html form 데이터 전송 방식의 경우에만 @RequestParam, @ModelAttribute 사용.
그 외의 경우는 HttpEntity 사용하거나 데이터 직접 꺼냄.
HttpEntity는 요청 파라미터와 전혀 관련이 없다.




𖤐 @RequestBody, @ResponseBody (실무에서 잘 씀)

@RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. HTTP 메시지 바디를 직접 조회하는 기능을 한다.
@ResponseBody가 있으면 "ok"를 HTTP 응답 코드에 넣어서 반환한다.
@RequestBody는 절대 생략이 불가능하다. -> 생략하면 값이 들어가지 않는다.
즉 생략하면 @ModelAttribute가 붙어버린다. 그래서 생략하면 HTTP 메시지 바디가 아닌 요청 파라미터를 처리하게 된다.
참고로 헤더 정보가 필요하면 HttpEntity를 쓰거나 @RequestHeader를 사용하면 된다.
당연히 요청 파라미터와는 전혀 관련이 없다.
@ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다.
물론 이 경우도 view 사용X.

@ResponseBody
    @PostMapping("/request-body-string-v4")
    public HttpEntity<String> requestBodyStringV4(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);
        return new HttpEntity<>("ok");
    }
    //@RequestBody, @ResponseBody를 통해 HttpEntity 역할을 수행.




𖤐 Http 요청 메시지 (JSON)

@ResponseBody
    @PostMapping("/request-body-json-v3")
    //String 이 아닌 HelloData 바로 넣기.
    public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }
    
    
@ResponseBody
    @PostMapping("/request-body-json-v4")
    //String 이 아닌 HelloData 바로 넣기.
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) throws IOException {
        HelloData helloData = httpEntity.getBody();
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }


☪ HTTP 메시지 converter

HTTP 메시지를 읽고 조작해서 처리.
Stream 해서 데이터 읽고 문자로 바꾸는 수동으로 해준 작업들을 스프링에서 converter 가 자동으로 해준다.
HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디에 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
HTTPMessageConverter 를 사용하면 MappingJackson2HttpMessageConverter가 동작을 한다.
이 때 HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); 이 코드를 대신해준다. (json의 경우)
즉 HTTPMessageConverter는 문자 뿐만 아니라 JSON도 객체로 변환해준다.


HttpMessageConverter가 들어올 때도 적용되지만 나갈 때도 적용이 된다.
helloData라는 객체가 Converter로 인해 JSON으로 바뀌고 바뀐 JSON 문자가 HTTP 메시지 응답에 박혀서 고객 응답으로 나간다.

//반환 HelloData
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    //String 이 아닌 HelloData 바로 넣기.
    public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) throws IOException {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return helloData;
    }


@ResponseBody 응답의 경우에도 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다.
물론 이 경우에도 HttpEntity를 사용해도 된다.

정리
@RequestBody 요청
-> JSON 요청 -> HTTP 메시지 컨버터 -> 객체
@ResponseBody
-> 객체 -> HTTP 메시지 컨버터 -> JSON 응답





𖤐 HTTP 응답 - 정적 리소스, 뷰 템플릿

정적 리소스 -> 웹 브라우저에 정적인 HTML, CSS, JS 를 제공할 때 사용
스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공함.
'static', 'public', 'resources', '/META-INF/resources'

뷰 템플릿 -> 웹 브라우저에 동적인 HTML을 제공할 때 사용. 근데 뷰 템플릿들이 랜드링 할 수 있다면 다른 것들도 가능
스프링 부트는 기본적으로 뷰 템플릿 경로를 src/main/resources/templates 으로 설정.


HTTP 메시지 사용 -> HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.



☪ String을 반환하는 경우

@ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고, 랜더링 한다.
@ResponseBody가 있으면 뷰 리졸버를 실행하지 않고 HTTP 메시지 바디에 직접 response/hello라는 문자가 입력된다.

@RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1() {
        ModelAndView mav = new ModelAndView("response/hello")
                .addObject("data", "hello"); //data는 hello
                //html에 {data} 해놓은 부분을 "hello"로 치환해준다.
        return mav;
    }

    //String으로 반환. 그러면 Model이 필요
    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model) {
        model.addAttribute("data", "hello!!");

        //@Controller이면서 String을 반환하게 되면 이게 view의 논리적 이름이 된다.
        return "response/hello";
    }




𖤐 HTTP 응답 - HTTP API, 메시지 바디 직접 입력

HTTP API를 제공하는 경우 HTML이 아니라 데이터를 직접 전달해야 하므로 HTTP 메시지 바디에 JSON같은 형식으로 데이터를 실어 보낸다.



𖤐 ResponseEntity

HttpEntity 를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디
정보를 가지고 있다. ResponseEntity 는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.



𖤐 @ResponseBody

@ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다. ResponseEntity 도 동일한 방식으로 동작한다.


☪ @RestController -> @Controller + @ResponseBody

@Controller 대신에 사용하면 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있다. 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다. 이름 그대로 Rest API를 만들 때 사용하는 컨트롤러이다.


API 만들 때 이런 스타일 주로 사용 (클래스 레벨에 @RestController 있음)

@GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);

        return helloData;
    }




𖤐 HTTP 메시지 converter(자세히)

뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라 HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

HTTP 요청의 경우에는 컨트롤러가 호출되기 전에 컨버터가 적용이 되어 @RequestBody가 있으면 HTTP메시지 바디의 데이터를 꺼내서 변환한 다음에 넘겨준다.
@RequestBody, HttpEntity(RequestEntity) 가 있으면 HTTP메시지 컨버터를 적용.
@ResponseBody, HttpEntity(ResponseEntity) 가 있으면 HTTP메시지 컨버터를 적용.

HTTP 메시지 컨버터는 HTTP 요청, 응답 둘 다 사용한다.




☪ 스프링 부트 기본 메시지 컨버터

순서대로 확인
0 = BtyeArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

대상 클래스 타입과 미디어 타입을 체크해서 사용여부를 결졍한다. 만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.
BtyeArrayHttpMessageConverter : byte[] 데이터를 처리
-> 클래스 타입 - byte[], 미디어타입 -
/*
StringHttpMessageConverter : String 문자로 데이터를 처리
-> 클래스 타입 - String, 미디어타입 -
/*
*MappingJackson2HttpMessageConverter : application/json 으로 처리
-> 클래스 타입 - 객체 또는 HashMap, 미디어타입 - application/json
그 외 타입이 맞지 않으면 탈락.

HTTP 메시지 컨버터는 스프링 MVC에서 사용된다.



𖤐 ArgumentResolver

파라미터를 유연하게 처리할 수 있게 해주는 인터페이스.
애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다. HttpServletRequest, Model, @RequestParam, @ModelAttribute 같은 애노테이션 그리고 @RequestBody, HttpEntity 같은 Http 메시지를 처리하는 부분까지 매우 유연하게 해주는게 ArgumentResolver이다.

애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor는 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터의 값을 생성한다. 그리고 파라미터 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.




핸들러 매핑 어댑터가 ArgumentResolver에게 request, response 객체 가져다 줄 수 있는지 물어봐서 가져다 줄 수 있는 객체가 선택이 되고 ArgumentResolver를 통해서 객체가 다 나온다. (requeset, response 말고 다른 객체가 될 수 있음.)

핸들러 어댑터는 파라미터를 넘겨줄 데이터가 다 준비가 됐다고 하면 핸들러에서 호출을 하는데 핸들러의 파라미터를 보고 어떤 파라미터가 필요한지 판단한 다음 ArgumentResolver에게 물어본다.
그러면 ArgumentResolver가 파라미터를 생성해준다.
생성해주고 나서 핸들러 어댑터가 컨트롤러를 호출하면서 ArgumentResolver를 통해 생성된 파라미터를 핸들러에 넣어준다.

HTTP 메시지 컨버터는 ArgumentResolver가 사용한다. HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.

ReturnValueHandler는 응답 값을 변환하고 처리해준다.



☪ HTTP 메시지 컨버터 위치

@RequestBody를 처리하는 ArgumentResolver가 있고 HttpEntity를 처리하는 ArgumentResolver가 있는데 ArgumentResolver들은 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성한다. 이렇게 요청의 경우에는 ArgumentResolver들이 메시지 컨버터를 루프를 돌리면서 처리를 한다.
응답의 경우에도 @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있고 여기서 메시지 컨버터를 호출해서 응답 결과를 만든다.

스프링은 HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler, HttpMessageConverter를 모두 인터페이스로 지원한다.





𖤐 Thymeleaf

html 파일에서 사용.
항상 타임리프 사용 선언을 해줘야 한다.

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

th:href라고 하면 href 속성을 타임리프 뷰 템플릿으로 랜더링 되는 순간 갈아 끼우고 없으면 다시 만든다.

<head>
  <meta charset="utf-8">
  <!--절대 경로로 넣어줌.-->
  <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
</head>

핵심은 th: 가 붙은 부분은 서버사이드에서 랜더링 된다는 것이다. th:xxx 가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
HTML을 파일로 직접 열었을 때는 th:xxx가 있어도 웹 브라우저는 ht: 속성을 알지 못하므로 무시한다. 그래서 파일을 직접 열어도 되고 서버사이드에서 랜더링 된 채로 열어도 된다.



☪ 링크 표현식 @{...}

th:href="@{/css/bootstrap.min.css}"
타임리프는 URL 링크 표현식을 할 때 @{...}를 쓴다.

th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
경로 변수 {itemId} 뿐만 아니라 쿼리 파라미터도 생성할 수 있다.

그러면 생성 링크가 ~~~8080/basic/items/1?query=test 이렇게 생성이 된다.

☪ 리터럴 대체 |...|

타임리프에서 문자와 표현식 등은 분리되어 있어 + 를 사용해야 하는데 리터럴로 + 없이 사용할 수 있다.
th:text = "'Spring framework, ' + ${user.name} + '!'"

th:text = "|Spring framework, ${user.name}!|"


☪ 반복 출력 th:each

th:each = "item : ${items}"
items 컬렉션 데이터가 item 변수에 하나씩 포함된다.


☪ 변수 표현식 ${...}

모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회한다.

<td th:text="${item.price}">10000</td>

☪ 내용 변경 th:text

내용의 값을 th:text의 값으로 변경
10000을 ${item.price}의 값으로 변경.


☪ URL 링크 표현 간단

<a th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
               th:text="${item.id}">회원 ID</a>
<!-- 그대로 URL 표시-->

<a th:href="@{|/basic/items/${item.id}|}"
                th:text="${item.itemName}">상품명</a>
<!-- 리터럴 대체 문법 사용-->

☪ 타임리프와 JSP 차이

JSP를 사용할 때는 JSP 파일을 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 섞여있어 정상적인 확인이 불가능하고 오직 서버로 JSP를 열어야 하지만 타임리프를 사용하면 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과도 확인할 수 있다.

내츄럴 템플릿 : 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징.





𖤐 PRG (Post/Redirect/Get)

새로고침하면 계속 입력한 데이터가 등록되어 저장되는 심각한 문제가 발생한다.
즉 새로고침 할 때마다 상품 데이터를 서버로 다시 전송한다.
그래서 리다이렉트를 통해 상품을 저장한 후에 뷰 템클릿으로 이동하는 것이 아니라
상품 상세 화면으로 리다이렉트를 호출해주면 된다.
그러면 새로고침 했을 때 마지막 호출이 POST/add 가 아닌 GET/items/{id} 가 되는 것이다.

//@PostMapping("/add")
    //파라미터 3개 받음
    public String addItemV5(Item item) {
        itemRepository.save(item);
        return "redirect:/basic/items/" + item.getId();
        //여기서 item.getId() 에 띄어쓰기가 있을 수 있다.
    }




𖤐 RedirectAttributes

URL 인코딩과 pathVariable, 쿼리 파라미터까지 처리해준다.
고객 입장에서 저장이 잘 됐는지 확인이 안될 수 있어 저장이 잘 됐으면 잘 됐다는 메시지를 남기도록 한다.

URL에 띄어쓰기가 있으면 위험하다. 그래서 RedirectAttributes 사용.

@PostMapping("/add")
    public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
        Item savedItem = itemRepository.save(item);
        //리다이렉트와 관련된 속성들을 넣음
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);

        //리다이렉트에 넣은 itemId가 치환되서 getId() 가 들어간다.
        //못 들어간 status는 쿼리 파라미터로 들어간다. (?status=...)
        return "redirect:/basic/items/{itemId}";
        //여기서 item.getId() 에 띄어쓰기가 있을 수 있다.
        //URL에 띄어쓰기가 있어면 위험하다. 그래서 RedirectAttributes 사용.
    }
<h2 th:if="${param.status}" th:text="'저장 완료!!'">

true면 저장 완료라는 메시지가 출력됨.

profile
일상의 인연에 감사하라. 기적은 의외로 가까운 곳에 있을지도 모른다.

0개의 댓글