MVC1 6th Step

최보현·2022년 8월 2일
0

MVC

목록 보기
6/18
post-thumbnail

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

스프링 MVC - 기본 기능

로그(log)

여태까지는 System.out.println()으로 콘솔에 출력을 했다면, 로그는 별도의 라이브러리(스프링 부트에 포함되어 있음)를 활용하여 로그를 출력해낸다.
로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다.

즉, SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 됨 실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용

로그 장점

  1. 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양 조정가능
  2. 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절 가능
  3. 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있음 => 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능
  4. 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 함

로그 선언

  1. private Logger log = LoggerFactory.getLogger(getClass());
  2. private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  3. @Slf4j => 롬복 사용

로그 호출

log.info()
운영에 중요한 정보만 남기기 위해서 info

로그 테스트

@Slf4j //롬복이 제공하는 어노테이션
@RestController
//보통은 뷰 이름이 반환되는데 얘는 그냥 스트링 자체를 반환해 줌
public class LogTestController {
//    private final Logger log = LoggerFactory.getLogger(getClass()); @Slf4j 가 자동으로 생성해줌

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

        System.out.println("name = " + name);

        log.trace("trace log={]",name);
        log.debug("debug log={]",name);
        log.info("info log={}", name);
        log.warn("warn log={]",name);
        log.error("error log={]",name);

        return "ok";
    }
}

매핑 정보 - @RestController

@Controller 는 반환 값이 String 이면 뷰 이름으로 인식됨 -> 뷰를 찾고 뷰가 랜더링 됨 ↔️ @RestController 는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한 => 실행 결과로 ok 메세지를 받을 수 있음(@ResponseBody 와 관련)

출력되는 포멧

시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지

로그 레벨 설정

LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
application.properties에서 설정 가능

#전체 로그 레벨 설정(기본 info)
ex) logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
ex) logging.level.hello.springmvc=debug

올바른 로그 사용법

log.debug("data="+data) 같이 '+' 연산자를 활용하는 방법은 ❌
만약에 log를 찍을 때 '+'를 이용하면 자바라는 언어의 특징은 연산을 먼저 이뤄내기 때문에 trace에서도 trace my loh=Spring이라고 얘를 가지고 있는거임
cpu를 사용하고, 메모리도 사용한거임 => 쓸데없는 리소스 사용 만약에 trace를 보지도 않는데 trace에 +연산자를 사용했을 경우

요청 매핑

요청 매핑이란, 요청이 들어왔을 때 어떤 컨트롤러가 와야하는지를 매핑하는 것

아주 기본 편

HTTP메서드가 지정되지 않아 모든 메서드에서 호출됨

@RestController
public class MappingController {
    private Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/hello-basic")
    // /hello-basic URL이 호출되면 이 메서드가 실행되도록 매핑
    // 대부분 속성을 배열로 제공하므로 다중 설정 가능 ex) "/hello-basic", "/hello-go"
    public String helloBasic(){
        log.info("hellobasic");
        return "ok";
    }

URL 요청으로 /hello-basic 과 /hello-basic/을 준다고 했을 때 이 둘은 엄연히 다른 URL이지만, 스프링은 이 요청들을 같은 요청으로 매핑함

메서드 매핑

     // method 특정 HTTP 메서드 요청만 허용

    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    //GET요청만 허용, POST가 오면 405코드 반환
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }

메서드 매핑 축약

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

경로 변수(PathVariable) 사용

	/**
     * 변수명이 같으면 생략 가능
     * @PathVariable("userId") String userId -> @PathVariable userId
     */
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable String userId){
        log.info("mappingPath userId={}", userId);
        return "ok";
    }
  • 최근 HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호함 ex) /users/1
  • @RequestMapping 은 URL 경로를 템플릿화 할 수 있는데, @PathVariable 을 사용하면 매칭 되는 부분을 편리하게 조회 가능
  • @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";
    }

특정 파라미터 조건 매핑

@GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }

특정 파라미터가 있거나 없는 조건을 추가할 수 있음 => 잘 안씀

특정 헤더 조건 매핑

 @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }

포스트맨으로 테스트를 할 때 헤더 부분을 임의로 새로 생성할 수 있는 부분에 key이름부분과 값 부분을 나눠서 작성해주면 됨!

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

 @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }
  • 포스트맨에서 바디 부분에서 그 파일 형식을 바꿔줄 수 있는데 그렇게 테스트 해줘야 함 ex) json의 경우, raw -> json으로 해줘야함
  • consumes 예시

    consumes = "text/plain"
    consumes = {"text/plain", "application/*"}
    consumes = MediaType.TEXT_PLAIN_VALUE -> 이렇게 쓰는 편이 좋음

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

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

consume은 요청의 컨텐츠 타입이라면 produce는 내가 내보내야할 때의 컨텐츠 타입
produce의 경우 accept가 중요함 왜냐, 받을 수 있는지의 여부가 중요! 그래서 postman에서는 보통 accept를 '/'로 해놔서 모든 걸 받아들일 수 잇는데 만약에 application/json같이 json만 받을 수 있게 지정해놓고 produce를 text로 해놓으면 오류뜸

  • produces 예시

    produces = "text/plain"
    produces = {"text/plain", "application/*"}
    produces = MediaType.TEXT_PLAIN_VALUE => 이렇게 쓰는 편이 좋음
    produces = "text/plain;charset=UTF-8"

요청 매핑 - API 예시

@RestController
@RequestMapping("/mapping/users")
//클래스 레벨에 매핑 정보를 둠으로써 메서드 레벨에서 해당 정보를 조합해서 사용
public class MappingClassController {

    /**
     * 회원 목록 조회: GET /users
     * 회원 등록: POST /users
     * 회원 조회: GET /users/{userId}
     * 회원 수정: PATCH /users/{userId}
     * 회원 삭제: DELETE /users/{userId}
     */

    @GetMapping
    public String user(){
        return "get users";
    }
    @PostMapping
    public String addUser(){
        return "post user";
    }
    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId){
        return "get userId=" + userId;
    }
    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId){
        return "update userId=" + userId;
    }
    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId){
        return "delete userId=" + userId;
    }
}

HTTP 요청 - 기본, 헤더 조회

@Slf4j
@RestController
public class RequestHeaderController {
    @RequestMapping("/headers")
    public String headers(
            HttpServletRequest request,
            HttpServletResponse response,
            HttpMethod httpMethod,
            Locale locale,
            @RequestHeader MultiValueMap<String, String> headerMap,
            //모든 HTTP 헤더를 MultiValueMap 형식으로 조회
            @RequestHeader("host") String host,
            //'host'는 필수 헤더이고 하나를 조회하고 싶을때는 @RequestHeader에 이름에 입력해주면 됨
            @CookieValue(value = "myCookie", required = false) String cookie
            //required=false라는 것은 없어도 된 다는 뜻, 쿠키에서 value값으로 주는 것이 쿠키명이 됨
            ){log.info ~~~~~ }
}

MultiValueMap?

Map과 유사하지만, 하나의 키에 여러 값을 받을 수 있음!
=> HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용
ex) keyA=value1&keyA=value2

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

HTTP 요청 데이터 조회

클라이언트에서 서버로 요청 데이터를 전달할 때 주로 사용하는 3가지 방법

  • GET - 쿼리 파라미터
    /url?username=hello&age=20
    메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
    예) 검색, 필터, 페이징등에서 많이 사용하는 방식
  • POST - HTML Form
    content-type: application/x-www-form-urlencoded
    메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
    예) 회원 가입, 상품 주문, HTML Form 사용
    HTTP message body에 데이터를 직접 담아서 요청
  • HTTP API에서 주로 사용, JSON, XML, TEXT
    데이터 형식은 주로 JSON 사용
    POST, PUT, PATCH

요청 파라미터 - 쿼리 파라미터, HTML Form

HttpServletRequest의 request.getParameter()를 사용하면 두 가지 요청 파라미터 조회 가능
GET 쿼리 파라미터나 POST HTML Form 전송 방식이든 형식이 같으므로 구분 없이 조회 가능! => 요청 파라미터(request parameter) 조회

완전 기본 편

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

HTTP 요청 파라미터 - @RequestParam

@RequestParam -> 스프링이 제공해 줌

기초편

@ResponseBody
//리턴값을 그냥 응답 메세지 바디에 넣어서 반환해줌
@RequestMapping("/request-param-v2")
public String requestParamV2(
       @RequestParam("username") String memberName,
       @RequestParam("age") int memberAge) {

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

@RequestParam의 name(value) 속성이 파라미터 이름으로 사용됨
ex) @RequestParam("username") String memberName = request.getParameter("username")

축약편

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

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

HTTP 파라미터의 이름이 변수 이름과 같으면 requestParam의 이름 생략 가능

더 축약편

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

String, int, Integer 같이 단순 타입이면 @RequestParam 생략 가능
어노테이션을 생략하면 스프링 내부에서 required=false를 적용함

파라미터 필수 여부

@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";
    }
  • @RequestParam의 required는 디폴트값이 true임
  • 자바에서 int는 null값이 들어갈 수 없음 0이라도 들어가야 함 -> 만약에 age의 required를 false로 두고 싶으면 int를 Integer로 바꿔줘야 함 왜냐, Integer는 객체이기 때문에 null값 가능
  • 만약 required true인 상태에서 그냥 아무것도 입력안하고 value만 적어도 알아서 빈문자를 넣어줘서 return이 ok가 뜸

기본값 적용

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

디폴트 밸류가 있으면 솔직히 리콰이어드의 존재의 의미는 희미해짐 왜냐 기본값이 설정되었기 때문, 디폴트 밸류는 빈문자의 경우에도 그냥 기본값을 때려줌

파라미터를 Map으로 조회 - requestParamMap

@RequestMapping("/request-param-map")
    public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
        log.info("username = {}, age = {}",paramMap.get("username"),paramMap.get("age"));
        return "ok";
    }

파라미터 값이 1개가 확실하면 Map을 써도 Ok, 애매하면 MultiValueMap 사용

HTTP 요청 파라미터 - @ModelAttribute

요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주는 일이 다반사
그럼 주로 작성하게 되는 코드가

@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);

@ModelAttribute는 이 과정을 완전히 자동화해줌

예시를 위해 사용될 객체

@Data
//@Getter, @Setter, @ToString 등을 자동 적용해줌
public class HelloData {
    private String username;
    private int age;
}

ModelAttribute 적용

@RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData){
        log.info("username = {}, age = {}",helloData.getUsername(),helloData.getAge());
        log.info("helloData={}", helloData);
        //이렇게 작성해도 출력됨 왜냐 @Data에 toString기능이 있어서
        return "ok";
    }

작동 순서
1. HelloData 객체 생성
2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.

  • 객체 프로퍼티란? getXXX, setXXX를 찾는다는 뜻
  1. 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력함
  2. 파라미터의 이름이 username이면 setUsername()을 찾아서 호출하면서 값 입력

축약 버전

 @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData){
        log.info("username = {}, age = {}",helloData.getUsername(),helloData.getAge());
        return "ok";
    }

@RequestParam도 생략이 가능하고 @ModelAttribute도 생략이 가능함 뭐 어쩌자는겨!
String, int, Integer 같은 단순 타입 -> @RequestParam
나머지(argumentresolver{ex: HttpServeltRequest, etc} 제외) -> @ModelAttribute로 여김

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

요청 파라미터와는 달리, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우 앞에 배운 내용들이 적용이 안됨 -> 우선, 단순 텍스트의 경우에는 InputStream 사용

기본편

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

Input, Output 스트림, Reader

@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");
    }
  • InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력

HttpEntity

@PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
        String messagebody = httpEntity.getBody();
        log.info("messagebody={}",messagebody);
        return new HttpEntity<>("ok");
HttpEntity?

httpentitiy의 역할은 copytostring 같이 그냥 httpbody에 있는 내용을 문자로 바꿔주는 역할을 함 즉, copytostring부분이 필요가 없어지는 거지 => http message converter 라는게 동작함

  • HTTP header와 body를 편리하게 조회
  • 메시지 바디 정보를 직접 조회
  • 요청 파라미터를 조회하는 기능과 관계❌
  • 응답에도 사용 가능(메시지 바디 정보 직접 반환, 헤더 정보 포함 가능, view 조회❌)
HttpEntity를 상속받은 객체들
  • RequestEntity
    HttpMethod, url 정보가 추가, 요청에서 사용
  • ResponseEntity
    HTTP 상태 코드 설정 가능, 응답에서 사용
    ex) return new ResponseEntity("Hello World", responseHeaders, HttpStatus.CREATED)

@RequestBody 사용

@PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messagebody) throws IOException {
        log.info("messagebody={}",messagebody);
        return "ok";
    }
@RequestBody?

@RequestBody 를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있음, 참고로 헤더 정보가 필요하다면 HttpEntity 를 사용하거나 @RequestHeader 를 사용하면 됨! 이 기능은 요청 파라미터를 조회하는 @RequestParam , @ModelAttribute 와는 전혀 관계 ❌

HTTP 요청 메시지 - JSON

기본편

public class RequestBodyJsonController {
    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);

        log.info("messagebody={}",messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username = {}, age = {}", helloData.getUsername(),helloData.getAge());

        response.getWriter().write("ok");
    }
  • HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환
  • 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper 를 사용해서 자바 객체로 변환

RequestBody 사용

@PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        log.info("messagebody={}",messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username = {}, age = {}", helloData.getUsername(),helloData.getAge());

        return "ok";
    }
  • @RequestBody 를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에
    저장
  • 문자로 된 JSON 데이터인 messageBody 를 objectMapper 를 통해서 자바 객체로 변환

축약편

@PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData helloData) {
  //HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); Http 메시지 컨버터가 대신 해줌
        log.info("username = {}, age = {}", helloData.getUsername(),helloData.getAge());
        return "ok";
    }
  • @RequestBody의 파라미터에 직접 만든 객체를 지정할 수 있다.
  • @RequestBody 또한 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을
    원하는 문자나 객체 등으로 변환해줌
  • 문자 뿐만 아니라 JSON도 객체로 변환해주는데, V2에서 했던 작업을 대신 처리
  • @RequestBody는 생략 불가능, HelloData에 @RequestBody 를 생략하면 @ModelAttribute 가 적용됨 => HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 됨

HttpEntity 사용

@PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
        HelloData helloData = httpEntity.getBody();
        log.info("username = {}, age = {}", helloData.getUsername(),helloData.getAge());
        return "ok";
    }

RequestBody 사용

@PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username = {}, age = {}", data.getUsername(),data.getAge());
        return data;
    }

얘 같은 경우 HelloData가 생성이 되고 메세지 컨버터가 얘를 json으로 변환한 다음 그 json이 Http 메시지 바디에 바로 꽂히게 됨

응답의 경우에도 @ResponseBody 를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있음 -> HttpEntity 를 사용해도 됨
실행 순서
@RequestBody 요청
1. JSON 요청
2. HTTP 메시지 컨버터
3. 객체 생성
@ResponseBody 응답
1. 객체 생성
2. HTTP 메시지 컨버
3. JSON 응답

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

서버에서 응답 데이터를 만드는 방법은 크게 3가지
1. 정적 리소스 2. 뷰 템플릿 사용 3. HTTP 메시지 사용

정적 리소스

정적 리소스는 resources/static 안에 들어 있는 파일을 말하며 변경 없이 그대로 서비스 됨

뷰 템플릿

뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달
일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만 뷰 템플릿이 만들 수
있는 것이라면 뭐든지 가능
뷰 템플릿은 resouces/templates 안에 있는 파일을 말함

뷰 템플릿을 호출하는 컨트롤러

@Controller
public class ResponseViewController {
    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1(){
        ModelAndView mav = new ModelAndView("response/hello")
                .addObject("data", "hello!");

        return mav;
    }

    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model){
        model.addAttribute("data", "hello!");
        return "response/hello";
    }

    @RequestMapping("/response/hello")
    public void responseViewV3(Model model){
        model.addAttribute("data", "hello!");
    }
    //컨트롤러의 경로 이름과 뷰의 논리적 이름이 동일하면 생략 가능
}

String을 반환하는 경우
@Responsebody가 없으면 response/hello로 뷰 리졸버가 실행되어 뷰를 찾고, 렌더링 함 ↔️ 있는 경우에는 뷰 리졸버를 실행하지 않고, 메시지 바디에 직접 response/hello 라는 문자 입력
Void를 반환하는 경우
@Controller 를 사용하고, HttpServletResponse , OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용
HTTP 메시지
@ResponseBody , HttpEntity 를 사용하면, 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 응답 데이터 출력 가능

타임리프 설정 방법

  1. 라이브러리 추가
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    스프링부트가 자동으로 타임리프뷰리졸버와 필요한 스프링 빈들을 등록해줌
  2. 기본 설정값
    application.properties에 쓰면 됨
    spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html

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

HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄

@Slf4j
@Controller
public class ResponseBodyController {
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok");
    }
  //서블릿을 직접 다룰 때 처럼 HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지 전달

    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2(){
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }
  //HttpEntity를 상속받은 ResponseEntity는 HTTP메시지의 헤더, 바디 정보(HttpEntity) + HTTP 응답 코드 설정 가능

    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() throws IOException {
        return "ok";
    }
  //@ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력 가능, ResponseEntity 도 동일한 방식으로 동작한다

    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1(){
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);

        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }
  //ResponseEntity 를 반환 -> HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환

    @ResponseStatus(HttpStatus.OK) //응답 코드 설정 가능
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2(){
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);

        return helloData; //얘는 상태코드 지정이 안되서 ResponseStatus 어노테이션을 사용함
    }
}

현재 ResponseBody가 많이 중복되어 있는 클래스 레벨에 @ResponseBody를 두면 ResponseBody를 각 메서드별로 생략해도 됨(어노테이션 안에 @ResponseBody가 들어 있음)
=> @RestController(@Controller + @ResponseBody) 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody 가 적용되는 효과가 있음 => 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력하고 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러

HTTP 메시지 컨버터

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

@ResponseBody 사용 원리


동작 순서
1. HTTP의 BODY에 문자 내용을 직접 반환
2. viewResolver 대신에 HttpMessageConverter 가 동작
2-1. 기본 문자처리: StringHttpMessageConverter
2-2. 기본 객체처리: MappingJackson2HttpMessageConverter
+) byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
스프링에서는 다음의 경우에 HTTP 메시지 컨버터 적용

  • HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
  • HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)
    HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용됨
  • canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
  • read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

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

0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정 -> 만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어감
ByteArrayHttpMessageConverter

  • byte[] 데이터를 처리한다.
  • 클래스 타입: byte[] , 미디어타입: / ,
  • 요청 예) @RequestBody byte[] data
  • 응답 예) @ResponseBody return byte[] 쓰기 미디어타입 application/octet-stream
    StringHttpMessageConverter
  • String 문자로 데이터를 처리한다.
  • 클래스 타입: String , 미디어타입: /
  • 요청 예) @RequestBody String data
  • 응답 예) @ResponseBody return "ok" 쓰기 미디어타입 text/plain
    MappingJackson2HttpMessageConverter
  • application/json
  • 클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
  • 요청 예) @RequestBody HelloData data
  • 응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json 관련

HTTP 메시지 컨버터 작동 순서

HTTP 요청 데이터 읽기

  1. HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터를 사용
  2. 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 를 호출
    2-1. 대상 클래스 타입을 지원하는지 ex) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )
    2-2. HTTP 요청의 Content-Type 미디어 타입을 지원하는지
    ex) text/plain , application/json , /
  3. canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환

HTTP 응답 데이터 생성

  1. 컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환
  2. 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출
    2-1. 대상 클래스 타입을 지원하는지 ex) return의 대상 클래스 ( byte[] , String , HelloData )
    2-2. HTTP 요청의 Accept 미디어 타입을 지원하는지(더 정확히는 @RequestMapping 의 produces )
    ex) text/plain , application/json , /
  3. canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성

요청 매핑 헨들러 어뎁터 구조

@RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter (요청 매핑 헨들러 어뎁터)에 메시지 컨버터가 사용된다

ArgumentResolver

애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있음 ex) HttpServletRequest, Model, @RequestParam , @ModelAttribute 등등
=> 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분
애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter 는 바로 이
ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성 -> 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨줌

동작 방식

  1. ArgumentResolver 의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크
  2. 지원하면 resolveArgument() 를 호출해서 실제 객체를 생성
  3. 생성된 객체가 컨트롤러 호출시 넘어감
    +) 직접 이 인터페이스를 확장해서 원하는 ArgumentResolver 를 만들 수 있음

ReturnValueHandler

ArgumentResolver 와 비슷하게 이것은 응답 값을 변환하고 처리
컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분

그래서 HTTP 메시지 컨버터는 최종적으로 어디에 위치하는가


앞서 배운 요청과 응답시에 메시지 컨버터가 작동하므로 그곳에 위치함
요청의 경우 @RequestBody와 HttpEntity 를 처리하는 ArgumentResolver 가 있음, 이 ArgumentResolver가 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성
응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있음 => 이 곳에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만듬
스프링 MVC는 @RequestBody @ResponseBody 가 있으면
RequestResponseBodyMethodProcessor (ArgumentResolver)
HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용

HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter

스프링은 위의 3가지를 모두 인터페이스로 제공함 -> 필요에 따라 언제든지 기능 확장 가능(하지만 거의 하지 않음)

profile
Novice Developer's Blog

0개의 댓글