[스프링 MVC - 1편] 스프링 MVC - 기본 기능

지현·2021년 12월 15일
0

스프링

목록 보기
20/32

War는 주로 톰캣을 별도로 설치할 때 사용, JSP를 쓸 때 사용, 내장 서버도 사용가능 하지만 주로 외부 서버에 배포하는 목적으로 사용
Jar는 별도의 톰캣 서버를 설치하는것이 아닌 내장 톰캣을 최적화해서 사용할 때 사용


로깅 간단히 알아보기

운영 시스템에서는 System.out.println() 같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력

로그 선언

  • private Logger log = LoggerFactory.getLogger(getClass()); : slf4j에 있는것을 import해서 사용하기
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class) : slf4j에 있는것을 import해서 사용하기
  • @Slf4j : 롬복이 제공하는 애너테이션
  • 셋중 하나 사용하면 됨

매핑 정보

  • @Controller는 반환 값이 String이면 뷰 이름으로 인식되어 뷰를 찾고 뷰가 랜더링 됨
  • @RestController는 반환 값으로 뷰를 찾는 것이 아니라, String이 바로 그대로 반환되어 HTTP 메시지 바디에 바로 입력

테스트

  • 로그가 출력되는 포멧 : 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지
  • 로그 LEVEL : TRACE > DEBUG > INFO > WARN > ERROR
    • 개발 서버는 debug 출력
    • 운영 서버는 info 출력
  • System.out.println은 항상 출력되기 때문에 필요없는 경우에도 출력됨

로그 레벨 설정

application.properties

#전체 로그 레벨 설정(기본 info), root는 전체를 다 세팅(현재 나의 프로젝트 기본값을 세팅)
logging.level.root=info

#hello.springmvc 패키지와 그 하위 로그 레벨 설정(이 패키지에서는 이게 우선권을 가짐)
logging.level.hello.springmvc=debug

올바른 로그 사용법

  • log.debug("data="+data)
    • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실행
    • 문자 더하기 연산이 발생 > 메모리, CPU 사용 > 쓸모없는 리소스를 사용
    • 사용하면 안됨
  • log.debug("data={}", data)
    • 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않음
    • 메서드를 호출하고 파라미터만 넘기는 것이기 때문에 의미없는 연산이 발생하지 않음

로그 사용시 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있음
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있음 > 애플리케이션 코드를 건들지 않고 설정만으로 로그 레벨을 조절할 수 있다는것이 중요
  • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있음
  • 성능도 일반 System.out보다 좋음 (내부 버퍼링, 멀티 쓰레드 등등 성능 최적화 되어있음)
  • 실무에서는 꼭 로그를 사용해야 함!!

요청 매핑

요청이 왔을 때 어떤 컨트롤러가 매핑이 되어야 하는지?

매핑정보

    @RequestMapping("/hello-basic")
    public String helloString(){
        log.info("helloBasic");
        return "ok";
    }
  • /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑
  • 대부분의 속성을 배열[] 로 제공하므로 다중 설정이 가능 {"/hello-basic", "/hello-go"}
  • 스프링은 다음 URL 요청들을 같은 요청으로 매핑
    • 매핑: /hello-basic
    • URL 요청: /hello-basic , /hello-basic/

HTTP 메서드 매핑

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

    @GetMapping(value = "/mapping-get-v2")
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }
  • @RequestMapping 에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출 > GET, HEAD, POST, PUT, PATCH, DELETE 모두 허용
  • @RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
    • 메서드를 지정하면 GET 요청 외에 나머지 요청들은 405 상태코드(Method Not Allowed)를 반환
  • @GetMapping(value = "/hello-basic")로 축약 가능
    • 요청에 맞춰 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 사용 가능

PathVariable(경로 변수) 사용

    /** * PathVariable 사용
     * 변수명이 같으면 생략 가능
     * @PathVariable("userId") String userId -> @PathVariable userId
     * /mapping/userA
     */
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable("userId")String data){
        log.info("mappingPath userId={}",data);
        return "ok";
    }

    /**
     * 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";
    }
  • URL 자체에 값이 들어가있음
    • URL 경로에 어떤 값을 템플릿 형식으로 쓸 수 있음
  • @PathVariable 을 사용하면 매칭 되는 부분을편리하게 조회
    • @PathVariable 의 이름과 파라미터 이름이 같으면 생략 가능
     @GetMapping("/mapping/{userId}")
      public String mappingPath(@PathVariable String userId){
          log.info("mappingPath userId={}",userId);
          return "ok";
      }
  • 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호
  • 참고 : ?userId=userA는 쿼리 파라미터 형식, /mapping/userA는 경로 변수

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

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }
  • 헤더의 content-type이 application/json일 경우에만 매핑
  • 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환
  • consumes은 요청 헤더의 content-type

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

    /** * Accept 헤더 기반 Media Type
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     */
    @PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
  • produces는 요청 헤더의 Accept 기반으로 매핑
  • 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환

HTTP 요청 - 기본, 헤더 조회

@Slf4j //로그
@RestController //응답값을 뷰를 찾는것이 아닌 문자 그대로를 반환
public class RequestHeaderController {

    @RequestMapping("/headers")
    //애너테이션 기반의 컨트롤러는 유연하게 다양한 파라미터를 받아들일 수 있음
    //리턴값도 유연하게 반환 가능(문자열, ModelAndView 등..)
     public String headers(HttpServletRequest request,
                           HttpServletResponse response,
                           HttpMethod httpMethod, //HTTP 메서드 조회
                           Locale locale, //언어 정보
                           @RequestHeader MultiValueMap <String,String> headerMap, 
                           //헤더를 한번에 다 받음
                           @RequestHeader("host") String host, 
                           //헤더를 하나만 받고싶을 때
                           @CookieValue(value ="myCookie",required = false) String cookie
                           // value > 쿠키 이름, required > default가 true
                           ){
        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";
     }
}
  • MultiValueMap
    • 하나의 키에 여러 값을 받을 수 있음
    • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용
    • keyA=value1&keyA=value2
    MultiValueMap<String, String> map = new LinkedMultiValueMap();
    map.add("keyA", "value1");
    map.add("keyA", "value2");
    //[value1,value2] 배열로 반환
    List<String> values = map.get("keyA");

HTTP 요청 - 파라미터, 메세지

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

  • GET - 쿼리 파라미터
    • /url?username=hello&age=20
    • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
  • POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
  • HTTP message body에 데이터를 직접 담아서 요청
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용
    • POST, PUT, PATCH

HTTP 요청 파라미터

쿼리 파라미터, HTML Form

요청 파라미터(request parameter) 조회

  • 요청 파라미터 - GET 쿼리 파라미터, HTML Form
  • HttpServletRequest 의 request.getParameter() 를 사용하면 요청 파라미터를 조회 할 수 있음
@Slf4j
@Controller
public class RequestParamController {
    @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

    @ResponseBody
    // View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력 > @RestController와 같은 효과
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username")String memberName,
            @RequestParam("age") int memberAge){
        log.info("username={}, age={}",memberName,memberAge);
        return "ok";
    }
  • @RequestParam : 파라미터 이름으로 바인딩
    request.getParameter("username") > @RequestParam("username") String memberName
  • @ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age)
  • HTTP 파라미터 이름과 변수명이 같으면 생략 가능
 public String requestParamV4(String username, int age)
  • String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
  • 요청 파라미터의 이름과 변수명이 같을때 생략 가능

파라미터 필수 여부 - requestParamRequired

    public String requestParamRequired(
            @RequestParam(required = true) String username,
            @RequestParam(required = false) Integer age){
        //age값이 파라미터로 들어오지 않으면 null이 입력되는데
        //int는 기본형이라 null을 받지 못함 > Integer로 바꿔주면 사용 가능
        log.info("username={}, age={}",username,age);
        return "ok";
    }
  • @RequestParam.required
    • 파라미터 필수 여부
    • 기본값은 파라미터 필수(true), 없으면 오류
  • username이 없을경우 400 예외가 발생
  • 파라미터 이름만 사용했을 경우 /request-param?username=
    • 파라미터 이름만 있고 값이 없는 경우 빈 문자라는 값이 들어왔기 때문에 통과
  • 기본형(primitive)에 null 입력
    • null 을 int 에 입력하는 것은 불가능(500 예외 발생)
    • null 을 받을 수 있는 Integer로 변경, 또는 defaultValue 사용

기본 값 적용 - requestParamDefault

    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(defaultValue = "guest") String username,
            @RequestParam(defaultValue = "-1") int age){
        log.info("username={}, age={}",username,age);
        return "ok";
    }
  • 파라미터에 값이 없는 경우 기본 값을 defaultValue값으로 적용하겠다!
  • 이미 기본 값이 있기 때문에 required 는 의미가 없음
  • defaultValue 는 빈 문자의 경우에도 설정한 기본 값이 적용됨
    /request-param?username=

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

    @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으로 조회
    • @RequestParam Map
      • Map(key=value)
    • @RequestParam MultiValueMap
      • MultiValueMap(key=[value1, value2, ...])
      • 하나의 키에 여러가지 값이 들어감
  • 파라미터의 값이 1개가 확실하다면 Map 을 사용해도 되지만, 그렇지 않다면 MultiValueMap 을 사용 > 보통 파라미터 값은 1개를 사용

@ModelAttribute

요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣을 때 사용

롬복 @Data : @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용

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

스프링MVC는 @ModelAttribute 가 있으면
1. HelloData 객체를 생성
2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾음(getXXX, setXXX)
3. 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)
예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력
4. 데이터의 타입이 잘못 입력되면 (int형인데 문자 입력하는 경우) 바인딩 오류 발생

프로퍼티

  • 객체에 getUsername() , setUsername() 메서드가 있으면, 이 객체는 username 이라는 프로퍼티를 가지고 있다고 함
  • username 프로퍼티의 값을 변경하면 setUsername() 이 호출되고, 조회하면 getUsername()이 호출
    public String modelAttributV2(HelloData helloData){
  • @ModelAttribute 는 생략 가능
  • @RequestParam와 구별 방법(@RequestParam도 생략 가능하기 때문에)
    • String , int , Integer 같은 단순 타입 = @RequestParam
    • 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 제외)

HTTP 요청 메시지

단순 텍스트

HTTP message body에 데이터를 직접 담아서 요청

  • HTTP API에서 주로 사용, JSON, XML, TEXT
  • 데이터 형식은 주로 JSON 사용
  • POST, PUT, PATCH
  • 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 데이터가 넘어오는 경우는 @RequestParam , @ModelAttribute 를 사용할 수 없음 (HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정)
    @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);
        //스트림은 바이트코드이기때문에 어떤 인코딩으로 해서 문자로 바꿀건지 지정
        //지정 안하면 default를 사용

        log.info("messageBody={}",messageBody);
        response.getWriter().write("ok");
    }

    @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 메시지 바디에 담아서 전송하고, 읽기
  • HTTP 메시지 바디의 데이터를 InputStream 을 사용해서 직접 읽을 수 있음
  • 스프링 MVC가 지원하는 파라미터
    • InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
    • OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
        //스프링이 알아서 문자형인것을 확인하고 httpBody에 있는 것을 문자로 바꿔서 넣어주는 HttpMessageConverter가 동작
        String messageBody=httpEntity.getBody();
        //http 메세지에 있는 바디를 꺼냄(string으로 변환하여)
        log.info("messageBody={}",messageBody);
        return new HttpEntity<>("ok");
    }
  • HttpEntity: HTTP header, body 정보를 편리하게 조회
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능(@RequestParam, @ModelAttribute)과 관계 없음
  • HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • view 조회 안함, 데이터를 Http 응답메세지에 바로 넣어버림
  • 스프링 MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데, HTTP 메시지 컨버터( HttpMessageConverter )를 사용
  • HttpEntity 를 상속받은 객체들
    • RequestEntity
      • HttpMethod, url 정보가 추가, 요청에서 사용
    • ResponseEntity
      • HTTP 상태 코드 설정 가능, 응답에서 사용
    @ResponseBody
    @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 메세지 컨버터 동작
  • @ResponseBody
    • @ResponseBody 를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달
    • view를 사용하지 않음, 데이터를 Http 응답메세지에 바로 넣어버림

요청 파라미터를 조회하는 기능 : @RequestParam , @ModelAttribute
HTTP 메시지 바디를 직접 조회하는 기능 : @RequestBody


JSON

    @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 를 사용해서 자바 객체로 변환
    @ResponseBody
    @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 를 통해서 자바 객체로 변환
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData helloData) {
        log.info("username={}, age={}",helloData.getUsername(),helloData.getAge());
        return "ok";
    }
  • @RequestBody에 직접 만든 객체를 지정
  • HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환
  • HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환
    HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); 이 역할을 대신 해줌
  • @RequestBody는 생략 불가능
    • 이 경우 HelloData에 @RequestBody 를 생략하면 @ModelAttribute 가 적용됨
      HelloData data > @ModelAttribute HelloData data
    • 생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리
    • String , int , Integer 같은 단순 타입인 경우는 @RequestParam가 적용됨
  • HttpEntity< HelloData >로도 사용 가능
/*(Accept : application/json)여야 json으로 받을 수 있음*/
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}",data.getUsername(),data.getAge());
        return data;
    }
  • @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어 응답할 수 있음
  • HttpEntity도 사용 가능
  • @RequestBody 요청
    JSON 요청 > HTTP 메시지 컨버터 > 객체
  • @ResponseBody 응답
    객체 > HTTP 메시지 컨버터 > JSON 응답

HTTP 응답

스프링(서버)에서 응답 데이터를 만드는 방법은 크게 3가지

  • 정적 리소스
    • 웹 브라우저에 정적인 HTML, css, js을 제공할 때
  • 뷰 템플릿 사용
    • 웹 브라우저에 동적인 HTML을 제공할 때
  • HTTP 메시지 사용
    • HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄

정적 리소스

  • 해당 파일을 변경 없이 그대로 서비스하는 것
  • 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공
    • /static
    • /public
    • /resources
    • /META-INF/resources
  • 정적으로 스프링부트가 내장 톰캣을 통해서 자동으로 서빙해줌

다음 경로에 파일이 들어있으면
src/main/resources/static/basic/hello-form.html

웹 브라우저에서 다음과 같이 실행
http://localhost:8080/basic/hello-form.html

뷰 템플릿

  • 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달
  • 스프링 부트는 기본 뷰 템플릿 경로를 제공
    src/main/resources/templates
    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1(){
        ModelAndView mav=new ModelAndView("response/hello")
                .addObject("data","hello!");
        return mav;
    }

ModelAndView로 반환하는 경우

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

String을 반환하는 경우

  • View
    • @ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링
    • templates/response/hello.html경로의 뷰 템플릿이 렌더링됨
  • HTTP 메시지
    • @ResponseBody 가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello 라는 문자가 입력
    //권장하지 않음
    @RequestMapping("/response/hello")
    //경로의 이름을 똑같이 넣어주면 return이 없어도 됨
    //컨트롤러의 경로의 이름과 뷰의 논리적 이름이 같으면 요청온 경로가 논리적 뷰의 이름으로 그냥 실행됨
    public void responseViewV3(Model model){
        model.addAttribute("data","hello!");
    }

Void를 반환하는 경우

  • @Controller 를 사용하고, HttpServletResponse, OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용
    • 요청 URL: /response/hello
    • 실행: templates/response/hello.html
  • 권장하지 않음

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

정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 메시지를 전달하는 경우

문자를 처리할 때

    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException{
        response.getWriter().write("ok");
    }

    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2(){
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }

    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3(){
        return "ok";
    }
    
  • responseBodyV1
    • HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달
    • response.getWriter().write("ok")
  • responseBodyV2
    • ResponseEntity 엔티티는 HttpEntity 를 상속 받음
    • HttpEntity는 HTTP 메시지의 헤더, 바디 정보 가지고 있음
    • ResponseEntity는 HTTP 응답 코드를 설정 기능 추가
  • responseBodyV3
    • @ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력
    • ResponseEntity 도 동일한 방식으로 동작

json을 처리할 때

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

    @ResponseStatus(HttpStatus.OK) //상태코드 지정
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2(){
        HelloData helloData=new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        return helloData;
    }
  • responseBodyJsonV1
    • ResponseEntity 를 반환
    • HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환
  • responseBodyJsonV2
    • @ResponseStatus(HttpStatus.OK) 애노테이션을 사용해 응답 코드 설정 > 동적으로 변경하려면 ResponseEntity를 사용해야함

@RestController

  • 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력
  • @ResponseBody는 클래스 레벨에 두면 전체에 메서드에 적용됨
  • @RestController = @Controller + @ResponseBody
  • @Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있음

HTTP 메시지 컨버터

HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리

@ResponseBody 사용 원리

  • @ResponseBody 를 사용하면 HTTP의 BODY에 문자 내용을 직접 반환
  • viewResolver 대신에 HttpMessageConverter 가 동작
    • 기본 문자처리: StringHttpMessageConverter
    • 기본 객체처리: MappingJackson2HttpMessageConverter
  • byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
  • 응답의 경우 클라이언트의 HTTP Accept 해더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HttpMessageConverter가 선택됨

HTTP 메시지 컨버터의 사용

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

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

  • 스프링 부트가 올라올 때 기본적으로 메세지 컨버터 몇개가 등록됨
  • 대상 클래스 타입과 미디어 타입(요청일 경우에 Content-type, 응답일경우에는 Accpet) 둘을 체크해서 사용여부를 결정
  • 0 = ByteArrayHttpMessageConverter
    • byte[] 데이터를 처리
    • 클래스 타입 : byte[], 미디어타입 : */* (아무거나 다 됨)
    • 요청 @RequestBody byte[] data
    • 응답 @ResponseBody return byte[] 으로 하면
      쓰기 미디어타입은 application/octet-stream application/octet-stream로 적용됨
  • 1 = StringHttpMessageConverter
    • String 문자로 데이터를 처리
    • 클래스 타입 : String, 미디어타입 : */*
    • 요청 @RequestBody String data
    • 응답 @ResponseBody return "ok"면 쓰기 미디어타입은 text/plain
  • 2 = MappingJackson2HttpMessageConverter
    • 객체를 json으로 또는 json을 객체로 처리, application/json을 주로 처리
    • 클래스 타입 : 객체 또는 HashMap, 미디어타입 : application/json 관련
    • 요청 @RequestBody HelloData data
    • 응답 @ResponseBody return helloData 쓰기 미디어타입 application/json 관련

데이터 처리 순서

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

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


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

RequestMappingHandlerAdapter 동작 방식

RequestMappingHandlerAdapter는 @RequestMapping 을 처리하는 핸들러 어댑터

ArgumentResolver

  • 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었음
    (HttpServletRequest, Model, @RequestParam, @ModelAttribute, @RequestBody, HttpEntity 등)
  • 파라미터를 유연하게 처리할 수 있는 이유 > ArgumentResolver
  • 애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는
    ArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성
  • 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨줌
  • 동작 방식
    • ArgumentResolver 의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크
    • 지원하면 resolveArgument() 를 호출해서 실제 객체를 생성
    • 생성된 객체가 컨트롤러 호출시 넘어감

ReturnValueHandler

  • HandlerMethodReturnValueHandler를 줄여서 ReturnValueHandler
  • 응답 값을 변환하고 처리(ModelAndView , @ResponseBody , HttpEntity , String 등)
  • 컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유

HTTP 메시지 컨버터

  • HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터의 값에 사용되고, @ResponseBody의 경우도 컨트롤러의 반환 값을 이용
  • HTTP 메시지 컨버터는 ArgumentResolver와 ReturnValueHandler가 사용
  • 요청의 경우 @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity 를 처리하는 ArgumentResolver가 있는데 이 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성
  • 응답의 경우 @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만듦
  • ArgumentResolver들 중에서 RequestBody나 HttpEntity를 처리해야하는 ArgumentResolver인 경우에 한해서 메세지 컨버터 사용!!! ReturnValueHandler경우에도 마찬가지



출처
[인프런] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

0개의 댓글