Spring/[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]

[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 스프링 MVC - 기본 기능

h2boom 2024. 10. 7. 16:57

스프링 MVC - 기본 기능

  • Jar vs War
    • Jar : 항상 내장 서버(톰켓 등)을 사용하고 webapp 경로를 사용하지 않고 내장 서버 사용에 최적화되어 있는 기능
    • War : 내장 서버도 사용 가능하지만 주로 외부 서버에 배포하는 목적으로 사용한다.
    • 최근에는 Jar를 주로 사용

 

  • 스프링 부트에 Jar 사용 시 /resources/static 위치에 index.html 파일을 두면 Welcome 페이지로 처리한다.
    • Jar는 정적 컨텐츠들을 /resources/static 하위에 위치시켜 사용한다.

로깅

  • 운영 시스템에서는 System.out.println()으로 콘솔에 필요한 정보를 출력하지 않고 별도의 로깅 라이브러리를 통해 로그를 출력한다.
  • 스프링 부트는 기본으로 SLF4J와 Logback 로깅 라이브러리를 사용한다.

 

@RestController
public class LogTestController {
    private final Logger log = LoggerFactory.getLogger(getClass()); //내 클래스 지정

    @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";
    }
}
  • 로깅 라이브러리 사용 시 SLF4J의 Logger를 사용하며 getLogger() 인자로 현재 클래스를 넣어준다.
    • @Slf4j 롬복을 사용해도 된다.
  • 콘솔 출력과 달리 로그로 출력 시 날짜 및 시간, 스레드,  ID, 클래스명, 메시지 등 많은 정보가 출력된다.
  • 로그를 찍을 때 로그 레벨을 지정할 수 있다.
    • 로그 레벨 : TRACE > DEBUG > INFO > WARN > ERROR (우측에 위치할수록 수준이 낮음)
    • trace : 가장 상세한 로그 레벨이며 애플리케이션 실행 흐름과 디버깅 정보를 상세히 기록하며 디버깅 시 사용된다.
    • debug : 디버깅 목적으로 사용되며 개발 단계 / 서버에서 주로 사용한다.
    • info : 정보성 메시지(비지니스 정보), 운영 서버에서도 필요한 내용을 기록하는 용도로 사용한다.
    • warn : 경고성 메시지를 기록하기 위해서 사용한다.
    • error : 오류 메시지를 기록하기 위해서 사용한다.
  • 로그 메시지 출력 시 {} 중괄호는 변수로 1:1 대체된다.
    • 중괄호가 여러개 있고 변수가 여러개 있으면 각각 대체 된다.

 

  • ★올바른 로그 사용법★
//잘못된 로그 사용법
log.info("data = " + data)

//올바른 로그 사용법
log.info("data = {}", data)
  • {} 중괄호 대신 =을 사용하게 되면 설정된 로그 레벨보다 높은 수준일 때에도 문자열끼리 더하기 연산이 발생한다.
    그렇기에 = 대신 {} 를 사용해서 의미 없는 연산이 발생하지 않도록 해야한다. 

 

logging.level.패키지명=trace
  • 로깅 시스템 설정은 application.yml / properties 파일에서 할 수 있다.
  • 패키지와 그 하위 로그 레벨 설정할 수 있으며 로그 레벨을 지정하면 해당 레벨을 포함한 하위 수준의 로그까지 확인할 수 있다.
    • ex) trace의 경우 모든 수준의 로그 확인가능, warn의 경우 warn, error 수준의 로그 확인 가능
    • 기본 값은 info 수준이다.
    • 보통 로컬 서버는 trace/debug 개발 서버는 debug, 운영 서버는 info로 지정한다.
  • 로깅은 수준에 맞게 원하는 것만 찍을 수 있지만 System.out.println()으로 콘솔 출력 시 모든 내용이 다 남기 때문에 사용하기에 적절하지 않다.

 

  • 로그 사용 시 장점 => 실무에서 무조건 로그를 사용해야 하는 이유
    • 쓰레드 정보, 클래스 명과 같은 부가 정보를 함께 볼 수 있고 출력 모양을 조정할 수 있다.
    • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고 운영 서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
    • System.out과 같이 콘솔에만 출력하는 것이 아닌 파일, 네트워크 등 로그를 별도의 위치에 남길 수 있다. (파일로 남길 시 로그를 분할 가능)
    • System.out 보다 성능면에서도 좋다.

요청 매핑

  • @Controller vs @RestController
    • @Controller : 반환 값이 String이면 뷰 이름으로 인식하기 때문에 뷰를 찾고 뷰가 렌더링 된다.
    • @RestController : 반환 값으로 뷰를 찾는 것이 아닌 HTTP 메시지 바디에 바로 입력하기 때문에 실행 결과로 문자열을 받을 수 있다.

 

  • @RequestMapping(url) : 해당 URL 호출이 오면 메소드가 실행된다.
    • {}로 url을 다중 설정할 수 있다.
      ex) @RequestMapping({"/hello", "/hello-basic"})
    • URL 요청 시 /hello와 /hello/는 서로 다른 URL이지만 스프링에서 같은 요청으로 매핑한다.
    • method 속성 값을 지정하지 않으면 HTTP 메소드(GET, POST, PUT ... 등)와 무관하게 호출된다.
    • @PostMapping, @GetMapping등 HTTP 메소드 매핑을 method 속성을 사용하지 않고 축약할 수 있다.
    • 메소드끼리 중복되는 URL은 클래스 레벨에서 @RequestMapping(공통url)로 공통 처리할 수 있으며 메소드 레벨에서 해당 정보와 조합해서 사용한다.

 

/**
 * PathVariable(경로 변수) 사용
 * 변수명이 같으면 생략 가능
 *
 * @PathVariable("userId") String userId -> @PathVariable String userId
 */
@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";
}
  • PathVariable(경로 변수) 방식
    • URL에 값 자체가 넘어오는 경우 @PathVariable로 매칭되는 부분을 편리하게 조회할 수 있다.
    • 변수명과 PathVariable의 이름이 같은 경우 PathVariable 명을 생략할 수 있다.
      • 생략하는 경우 자바 컴파일러에 -parameters 옵션을 설정해야 하기에 생략하지 않는 것을 권장한다.
  • 쿼리 파라미터 방식 : url?data1&data2 형태로 데이터가 넘어오는 방식
    • 쿼리 파라미터의 데이터는 @RequestParam으로 데이터를 조회할 수 있다.

 

/**
 * 파라미터로 추가 매핑
 * params="mode",
 * params="!mode"
 * params="mode=debug"
 * params="mode!=debug" (! = )
 * params = {"mode=debug","data=good"}
 */
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
    log.info("mappingParam");
    return "ok";
}
  • params 옵션을 주면 URL도 일치하면서 해당 파라미터 정보까지 매핑되어야 메소드가 실행된다.
    • headers 옵션은 헤더 정보를 매핑
    • consume 옵션은 Content-Type을 매핑
    • produce 옵션은 Accept를 매핑

HTTP 요청 - 기본, 헤더 조회

@Slf4j
@RestController
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,
                          @CookieValue(value = "myCookie", required = false) String cookie) {

        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";
    }
}
  • @Controller에서 사용 가능한 파라미터 (외에도 다양한 것 들이 많다.)
    • HttpMethod - HTTP 메소드 (GET, POST, PUT, PATCH ...등등)
    • Locale - 언어 정보
    • @RequestHeader - MultiValueMap 사용 시 모든 헤더 정보를 조회하고 단건 조회 시 해당 헤더 명을 통해 조회할 수 있다.
    • @CookieValue - 쿠키 정보
    • 단건 조회의 경우 옵션으로 required, defaultValue 등의 옵션을 제공한다.
      • required는 필수 값 여부로 기본 값은 true
      • defaultValue는 값이 없을 경우 기본 값을 지정해줄 수 있다.
  • MultiValueMap : Map과 유사하며 하나의 Key에 여러 Value를 받을 수 있다.
  • @Controller에서 반환 값도 String 외에 많은 타입들이 있다.

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

  • 클라이언트에서 서버로 요청 데이터 전달 방식
    • GET - 쿼리 파라미터 : 메시지 바디 없이 URL의 쿼리 파라미터에 데이터 전달
    • POST - HTML Form : 메시지 바디에 쿼리 파라미터 형식으로 전달
    • HTTP message body : HTTP API에서 주로 사용하며 데이터 형식은 주로 JSON

 

  • HTML Form과 GET 쿼리 파라미터 방식은 조회하는 방식이 같으며 요청 파라미터(request parameter) 조회라고 한다.
    1. getParameter()로 조회
    2. @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 : 파라미터 이름으로 바인딩
    • @RequestParam의 이름과 변수명이 같은 경우 파라미터 이름을 생략하거나 @RequestParam을 생략할 수 있다.
      • 생략하는 경우 자바 컴파일러에 -parameters 옵션을 설정해야 하기에 생략하지 않는 것을 권장한다.
    • ex) @RequestParam("username") String memberName
      = String memberName = request.getParameter("username")
  • @ResponseBody : View 조회를 무시하고 HTTP message body에 직접 해당 내용을 입력한다.
    • @RestController와 같은 기능을 한다.
    • @Controller에서 String 반환타입인 경우 뷰 논리 이름으로 인식하는데 클래스 레벨에 @RestController를 사용하거나 메소드 단위에 @ResponseBody를 사용하면 뷰를 조회하지 않고 HTTP message body에 직접 내용을 입력하기에 실행 결과로 문자열을 받을 수 있다.

 

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

    return "ok";
}
  • HTTP 파라미터 이름과 변수 명이 같은 경우
    • 파라미터 이름을 생략할 수 있다.
    • @RequestParam을 생략할 수 있다.
      • String, int, Integer 등 단순 타입인 경우에만 생략 가능

 

  • ★스프링 부트 3.2 이후 버전 파라미터 이름 인식 문제★
    • @RequestParam을 생략, @PathVariable name 속성 생략, 파라미터 이름 생략하는 등의 경우에 예외 발생 (java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] 
      not specified, and parameter name information not found in class file either.)

    • 예외 발생 해결 방법 (3가지)
      • ★항상 파라미터 이름과 @RequestParam 어노테이션을 모두 적어준다.★
      • 자바 컴파일러에 -parameters 옵션을 넣어준다.
        1. IntelliJ IDEA에서 File Settings를 연다.
        2. Build, Execution, Deployment → Compiler → Java Compiler로 이동
        3. Additional command line parameters라는 항목에 -parameters를 추가한다.
        4. out 폴더를 삭제 후 다시 실행한다.
      • Gradle을 사용해서 빌드하고 실행한다.

 

@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(@RequestParam(required = true) String username,
                                   @RequestParam(required = false) int age) {
    log.info("username={}, age={}", username, age);

    return "ok";
}
  • @RequestParam에 required 옵션으로 파라미터 필수 여부를 지정할 수 있다.
    • required = true가 기본 값이며 true인 경우 파라미터 값이 없으면 400 에러가 발생한다.
      • true인 파라미터에 아무값도 넣지 않으면 빈문자로 들어가게 된다.
    • required = false 인 경우 해당 파라미터 값이 없어도 된다.
  • 위 예제에서 파라미터로 username만 넘긴다면?
    • 원래대로라면 오류가 발생하지 않아야 하지만 500 에러가 발생한다.
      => age는 int 타입이므로 null 값이 들어갈 수 없기 때문에 500 에러가 발생하므로 Integer 타입으로 변경해주거나 defaultValue를 사용해야 한다.

 

@ResponseBody
@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";
}
  • defaultValue를 옵션으로 사용하면 required 옵션은 사실상 필요가 없다.
    • defaultValue는 빈 문자의 경우에도 설정한 기본 값으로 적용된다.

 

@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, ....])
    • 파라미터 값이 한 개인 것이 확실한 경우에만 Map을 사용하고 그렇지 않으면  MultiValueMap를 사용하자

HTTP 요청 파라미터 - @ModelAttribute

  • 요청 파라미터를 받아서 필요한 객체를 만들고 객체에 값을 넣어주는 과정을 자동화 해주는 @ModelAttribute 기능을 제공한다.

 

//@ModelAttribute 사용 전
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@RequestParam String username,
                               @RequestParam int age) {
    HelloData helloData = new HelloData();
    helloData.setUsername(username);
    helloData.setAge(age);

    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    log.info("helloData={}", helloData);

    return "ok";
}

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

    return "ok";
}
  • 예제에서 @ModelAttribute 동작 방식
    1. HelloData 객체 생성
    2. 요청 파라미터 이름으로 HelloData 객체의 프로퍼티를 찾는다.
    3. 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 바인딩(입력)한다.
      ex) 파라미터 이름이 username이면 setUsername() 메소드를 찾아서 호출하고 파라미터로 값을 입력한다.
  • 파라미터에 다른 타입이 들어가면 바인딩 오류가 발생한다.
  • @ModelAttribute는 생략할 수 있다.
    • 스프링은 생략 시 다음 규칙을 따른다.
      • String, int, Integer와 같이 단순 타입은 @RequestParam으로 인식
      • 나머지는 @ModelAttribute로 인식 (argument resolver로 지정해둔 타입은 제외)

 

  • 프로퍼티 : 필드와 메소드 간 기능의 중간인 클래스 멤버의 특수한 유형을 의미한다.
    • 일반적으로 Getter, Setter 메소드 호출로 변환된다.
    • ex) 객체에 getUsername(), setUsername() 메소드가 있으면 이 객체는 username이라는 프로퍼티를 가지고 있는 것이다. username의 프로퍼티 값을 변경하면 setUsername()이 호출, 조회하면 getUsername()이 호출된다.

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

  • 요청 파라미터와 다르게 HTTP 메시지 바디를 통해 직접 데이터가 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.
    • 요청 파라미터 = HTML Form 방식, GET 쿼리 파라미터 방식

 

@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");
}
  • 메시지 바디로 넘어오는 경우 Stream을 통해 데이터를 받아와야 한다.
    • Stream은 byte 타입이기에 StreamUtils.copyToString()을 통해 문자열로 변환해줘야 한다.
  • 메소드의 파라미터로 request/response 말고 InputStream, Writer를 직접 받아올 수 있다.

 

@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 : HTTP header, body 정보를 편리하게 조회할 수 있도록 도와주는 객체이다.
    • 메시지 바디 정보를 직접 조회 가능
    • 요청 파라미터를 조회하는 기능과는 관계가 없다.
    • 응답에도 사용할 수 있다.
  • HttpEntity를 상속받은 RequestEntity, ResponseEntity도 있다.
    • HTTP 상태 코드 지정 가능

 

  • HttpMessageConverter : 스프링MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는 기능이다.

 

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

    return "ok";
}
  • @RequestBody : HTTP 메시지 바디 정보를 편리하게 조회할 수 있도록 도와주는 어노테이션이다.
    • 메시지 바디 정보를 읽어서 해당 객체에 담아주는 역할
    • 헤더 정보가 필요한 경우 HttpEntity나 @RequestHeader를 사용하면 된다.
  • @ResponseBody : 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달하도록 도와주는 어노테이션이다.
    • 이 경우 view를 사용하지 않는다.

 

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

 

  • @RequestBody, @ResponseBody 모두 HttpEntity로 대체해서 사용할 수 있다.

HTTP 요청 메시지 - JSON

@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 방식
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

    return "ok";
}

//HttpEntity 방식
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> data) throws IOException {
    HelloData helloData = data.getBody();
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

    return "ok";
}
  • @RequestBody에 직접 만든 객체를 지정할 수 있다.
  • HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디 내용을 우리가 원하는 문자나 객체등으로 변환해준다.
    • HTTP 메시지 컨버터에는 문자열을 처리하는 컨버터, JSON을 처리하는 컨버터 등이 있다.
  • @RequestBody는 생략하면 안된다.
    • @RequestBody 생략 시 단순 타입은 @RequestParam, 나머지는 @ModelAttribute로 취급된다.
  • JSON 타입의 경우 HTTP 요청 시 Content-Type이 application/json인지 꼭 확인해야 한다.
    • 그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행된다.

 

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

    return helloData;
}
  • @ResponseBody가 붙으면 객체를 JSON 형태로 반환할 수 있다.
    • @ResponseBody를 사용하면 문자열 뿐만 아니라 객체를 HTTP 메시지 바디에 직접 넣을 수 있다.
  • 예제에서 HttpMessageConverter 동작 방식
    1. 클라이언트에게서 전달받은 JSON 데이터를 @RequestBody에 의해 HelloData 객체로 변환
    2. HelloData 객체를 @ResponseBody에 의해 JSON 데이터 타입으로 변환 후 반환

 

  • 객체(JSON)를 반환하는 경우 HTTP Accept가 application/json 타입인지 확인해야 한다.

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

  • 정적 리소스 : 파일의 변경 없이 그대로 서비스하는 것
    • 스프링 부트는 클래스 패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
      /static, /public, /resources, /META-INF/resources
    • ex) 웹 브라우저에 정적인 HTML, css, js를 제공할 때
  • 뷰 템플릿 사용
    •  뷰 템플릿 경로는 src/main/resources/templates 이다.
    • ex) 웹 브라우저에 동적인 HTML을 제공할 때
  • HTTP 메시지 사용
    • HTTP API를 제공하는 경우 HTML이 아닌 데이터를 전달해야 하므로 HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${data}">empty</p>
</body>
</html>
  • Thymleaf 뷰 템플릿 예제
    • 렌더링 시 <p th:text="${data}">empty</p> 태그로 인해 Content(empty)에 data 변수 값이 치환되게 된다.

 

//뷰 템플릿 호출 예제
@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가 없으면 뷰 리졸버가 실행되서 뷰의 논리이름으로 뷰를 찾고 렌더링한다.
    • @ResponseBody가 있으면 뷰 리졸버를 실행하지 않고 HTTP 메시지 바디에 직접 내용을 입력한다.
      • @ResponseBody나 HttpEntity를 사용하는 것은 뷰 템플릿을 사용하는 것이 아닌 HTTP 메시지 바디에 직접 응답 데이터를 출력하는 것이다.
  • 예제의 경우 resoureces/templates/response/hello.html 뷰를 찾는다.

 

  • void를 반환하는 경우
    • @Controller를 사용하고 HttpServletResponse나 OutputStream같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용한다.
      • 뷰 논리 이름과 Controller의 매핑URL이 같은 경우 void로 반환해도 되지만 너무 명시성이 떨어지기에 권장하지 않는 방법.

 

  • 스프링 부트에 Thymleaf 라이브러리 추가 시 자동으로 Thymleaf 뷰 리졸버와 필요한 스프링 빈들을 등록한다.
    • 기본 값으로 prefix = classpath:/templates/ 이고 suffix=.html이기에 필요한 경우 application.yml/properties에 설정하면 된다.

HTTP 응답 - 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";
}
  • ResponseEntity나 @ResponseBody를 사용해서 view를 사용하지 않고 HTTP 메시지를 직접 입력

 

//응답 - HTTP API (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;
}
  • @ResponseEntity는 HTTP 응답 코드를 설정할 수 있다.
    • 동적으로 응답 코드를 설정하려면 ResponseEntity를 사용해야 한다.

 

  • @RestController = @Controller + @ResponseBody
    • 각 메소드마다 @ResponseBody를 사용하려면 번거롭기에 클래스 레벨에서 사용해도 된다.
    • @ResponseBody를 클래스 레벨에서 사용하는 경우 @RestController를 사용하면 된다.
    • 즉 @RestController는 Rest API(HTTP API)를 만들 때 사용하는 어노테이션이다.

HTTP 메시지 컨버터

  • @ResponseBody, @RequestBody, HttpEntity 사용
    • HTTP 바디에 문자 내용을 직접 반환
    • 뷰 리졸버 대신 HttpMessageConverter가 동작
    • 기본 문자 처리 - StringHttpMessageConverter
    • 기본 객체 처리 - MappingJackson2HttpMessageConverter
    • byte 처리 등 기타 여러 HttpMessageConverter가 등록되어 있다.

 

  • 응답의 경우 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보를 조합해서 HttpMessageConverter가 선택된다.

 

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

 

  • 메시지 컨버터 종류 (이외에도 많다)
    • 0순위 ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다.
      • 클래스 타입: byte[] , 미디어타입: */* ,
      • 요청 예) @RequestBody byte[] data
      • 응답 예) @ResponseBody return byte[]
        쓰기 미디어타입 : application/octet-stream
    • 1순위 StringHttpMessageConverter : String 문자로 데이터를 처리한다.
      • 클래스 타입: String , 미디어타입: */*
      • 요청 예) @RequestBody String data
      • 응답 예) @ResponseBody return "ok"
        쓰기 미디어타입 : text/plain
    • 2순위 MappingJackson2HttpMessageConverter : application/json
      • 클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
      • 요청 예) @RequestBody HelloData data
      • 응답 예) @ResponseBody return helloData 
        쓰기 미디어타입 : application/json 관련

 

  • @RequestBody String 타입과 Content-Type에 application/json이 넘어오는 경우
    1. String이 JSON보다 우선순위가 높고 @RequestBody의 String 타입에 의해 먼저 String 메시지 컨버터가 선택된다.
    2. String 메시지 컨버터의 경우 모든 Content-Type을 허용하기에 String 메시지 컨버터로 처리된다.

 

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

 

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

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

  • ArgumentResolver : 컨트롤러가 필요로 하는 다양한 파라미터 값(객체)를 생성한다.
    • 파라미터 값이 준비가 되면 컨트롤러를 호출하면서 값을 넘겨준다.
    • supportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크 후 지원하면 resolveArgument()를 호출해서 실제 객체를 생성한다.

 

  • ReturnValueHandler : 응답 값을 변환하고 처리한다.
    • String으로 뷰 이름을 반환해도 ReturnValueHandler가 동작하도록 처리해준다.

 

  • HTTP 메시지 컨버터는 ArgumentResolver와 ReturnValueHandler에 각각 위치하고 동작한다.

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

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com