본문 바로가기
개발자 일지/Spring

[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(6)

by 네빌링 2022. 6. 18.
반응형

- 스프링 MVC의 기본 기능을 알아본다.

- 스프링 로깅에 대해 간단히 알아본다.

- HTTP 요청과 응답을 처리하는 다양한 방식을 알아본다.

- HTTP 메시지 컨버터에 대해 알아본다.

-모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture4_springMVC)


6.스프링 MVC - 기본 기능

 

1.프로젝트 생성 및 Welcome 페이지 만들기

 

스프링 부트 스타터 사이트에서 프로젝트를 생성한다. 이 때 Packaging을 Jar로 선택한다. 

 

 

Jar는 항상 내장서버(톰캣)를 사용하고 webapp 경로를 사용하지 않는다. 내장 서버 사용에 최적화 되어 있다. 

반면, War를 사용하면 내장서버도 사용가능 하지만 주로 외부 서버 배포 목적으로 사용한다.

 

롬복을 사용하기 때문에 인텔리제이 IDE 사용시에는 Annotation Processor 기능을 켜야 한다. 

윈도우 기준 File > Settings > Build, Execution, Deployment > Compiler > Annotation Processors에서 Enable annotation processing을 체크한다.

 

다음으로 Welcome 페이지를 만든다.

 

스프링 부트에 Jar를 사용하면 /resources/static/에 index.html을 두면 Welcome 페이지로 처리해준다.

 

2.로깅 간단히 알아보기

 

스프링부트 라이브러리 사용시 로깅 라이브러리(spring-boot-starter-logging)가 함께 포함된다.

스프링부트 로깅 라이브러리는 기본적으로 slf4j 인터페이스를 구현한 Logback을 기본으로 사용한다.

 

slf4j는 수많은 로그 라이브러리를 통합하여 인터페이스로 제공하는 라이브러리이다.

Logback은 그 중 하나이며 Log4J, Log4J2 등이 있다.

 

로그 선언 방법은 간단하다.

 

[LogTestController.java]

@RestController
public class LogTestController {
    //Logger는 org.lsf4j.Logger 인터페이스 사용하고 getClass()로 자신을 넣으면 됨
    //private final Logger log = LoggerFactory.getLogger(getClass());
    private final Logger log = LoggerFactory.getLogger(LogTestController.class);
	
   	//...    
}

 

getLogger 안에 getClass()를 선언하든지 자기 자신 클래스를 선언하든지 동일하다.

 

롬복을 사용하면 더 간단하게 @Slf4j만으로 로깅 라이브러리 사용이 가능하다.

 

다음은 로깅라이브러리 테스트 코드이다.

 

[LogTestController.java]

//@Slf4j : 아래 log 코드 귀찮으면 이 어노테이션 사용
@Slf4j
@RestController
public class LogTestController {
    //Logger는 org.lsf4j.Logger 인터페이스 사용하고 getClass()로 자신을 넣으면 됨
    //private final Logger log = LoggerFactory.getLogger(getClass());
    //private final Logger log = LoggerFactory.getLogger(LogTestController.class);


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

        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); //에러
        
        //2022-06-15 23:21:27.804  INFO 15428 --- [nio-8080-exec-4] hello.springmvc.basic.LogTestController  : info log=Spring
        //nio-8080-exec-4는 쓰레드풀에서 실행한 쓰레드

        //@RestController를 사용했기 때문에 viewName이 아니라 String 바로 반환
        return "ok";
    }
}

 

여기서 @Controller가 아닌 @RestController를 사용하였다. @Controller의 경우 요청매핑 메소드 반환타입이 String이면 뷰 이름으로 인식되어 뷰를 찾고 뷰가 랜더링 된다. 반면, @RestController는 반환 값으로 뷰를 찾는 것이 아닌 HTTP 메시지 바디에 바로 입력한다. 위의 logTest()는 그 결과 실행결과로 ok 메시지를 받는다.

 

참고로 @Controller를 쓰고 메소드에 @ResponseBody를 붙이면 동일한 기능을 한다.

 

다음으로 logTest()를 살펴보면 log.trace()부터  log.error()까지 코드를 확인할 수 있다. 로그 레벨 순서는 다음과 같다.

 

  • TRACE -> DEBUG -> INFO -> WARN -> ERROR

 

개발서버는 보통 debug를 출력하며 운영은 info를 출력한다. 예를들어 info의 경우 info, warn, error까지 더 상위레벨의 로그까지 나온다.

 

로그 레벨 설정은 application.properties에서 할 수 있다.

 

[application.properties]

#전체 로그 레벨 설정(기본 info)
logging.level.root=info

#hello.springmvc 패키지와 그 하위 로그 레벨 설정(TRACE가 가장 낮은 레벨이며 모든 레벨이 다 로그로 보임)
#운영에선 기본적으로 info로 설정하여 info, warn, error를 보여준다.
#로컬에선 trace로 남겨 다 본다.
#아래 코드 주석 시 기본적으로 INFO 레벨로 출력
logging.level.hello.springmvc=trace

 

로그 사용시 주의해야할 점은 '연산'이다. 다음과 같이 + 연산을 사용하는 것보다 {} 방법을 사용한다.

#로그 출력 레벨 info 설정시에도 해당 코드에 있는 + 연산이 실행되어 리소스 낭비
log.debug("data=" + data); 

#다음과 같이 사용
log.debug("data={}", data);

 

로그를 사용하면 System.out과 비교했을 때 다음과 같은 장점들이 있다.

 

  • 쓰레드 정보, 클래스 이름 등 부가정보를 함께 볼 수 있고 출력 모양을 조절 가능
  • 로그를 상황에 맞게 조절 가능(debug, info)
  • 로그를 콘솔 출력 뿐 아니라, 파일이나 네트워크 등 별도 위치에 남길 수 있음
  • 성능도 System.out보다 좋음

 

3.요청 매핑

 

요청을 컨트롤러에서 매핑하는 여러가지 방법들에 대해 알아본다. 

 

[MappingController.java] 

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

    @RequestMapping("/hello-basic")
    public String helloBasic() {
        log.info("helloBasic");
        return "ok";
    }

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

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

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

    /**
     * 파라미터로 추가 매핑
     * ?mode=debug가 아닌 다른 걸 전송하면 400 Bad Request
     * 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";
    }

    /**
     * 특정 헤더로 추가 매핑
     * 헤더에 mode=debug를 안 넣어주면 404를 뱉음
     * headers="mode",
     * headers="!mode"
     * headers="mode=debug"
     * headers="mode!=debug" (! = )
     */
    @GetMapping(value="/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * 내가 보낼 요청 Header에 Content-Type=application/json이 아니라면 415 Unsupported 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() {
        //서버입장에서는 소비하는 입장(consume)
        log.info("mappingConsumes");
        return "ok";
    }

    /**
     * Accept 헤더 기반 Media Type
     * HTTP 요청의 Accept와 produce가 맞아야 함
     * 요청 보낼 때 Accept에 application/json으로 준다면 406 Not Acceptable
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     */
    @PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
    public String mappingProduces() {
        log.info("mappingProduce");
        return "ok";
    }
}

 

@RestController를 사용하여 반환 값 String일 때 HTTP 메시지 바디에 ok를 입력해 출력한다.

 

consumes는 요청시 Header의 Content-Type과 매칭된다. 안 맞으면 415 Unsuppored Media Type을 뱉는다.

produces는 내가 return할 데이터의 타입을 명시한다. 요청 보낼 때 Accept와 매칭되어야 하며 안 맞으면 406 Not Acceptable을 뱉는다.

 

 

4.요청 매핑 - API 예시

 

회원관리를 HTTP API로 만든다고 가정하면 다음과 같이 매핑한다.

 

[MappingClassController.java]

package hello.springmvc.basic.requestmapping;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {


    /**
     * 회원 목록 조회
     */
    @GetMapping
    public String user() {
        return "get users";
    }

    /**
     * 회원 등록
     */
    @PostMapping
    public String addUser() {
        return "post user";
    }

    /**
     * 회원 조회
     */
    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get usersId=" + 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;
    }

}

 

5.HTTP 요청 - 기본, 헤더 조회

 

[RequestHeaderController.java]

@Slf4j
@RestController
public class RequestHeaderController {
    /**
     * 요청시 header 데이터를 조회하는 여러가지 방법
     */
    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String,String> headerMap, //같은키에 여러 value가 있을때 배열반환 가능
                          @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";
    }
}

 

@Slf4j 로그 선언을 해준다.

 

MultiValueMap같은 경우 하나의 키에 여러 값을 받을 수 있다.

 

keyA=value1&keyA=value2와 같은 형태로 요청을 보내면 keyA=[value1, value2]로 담긴다.

 

 

6~8.HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form, @RequestParam, @ModelAttribute

 

이전에 언급한 부분이지만 클라이언트가 서버로 요청을 보내는 방법이 크게 3가지가 있다.

  • GET 쿼리파라미터 : request.getParameter("파라미터명") / 메시지 바디 없음
  • POST HTML Form : 메시지바디에 쿼리파라미터 형식으로 전달. request.getParameter() 사용 가능 / 메시지 바디 있음
  • HTTP message body : HTTP API에서 주로 사용하며 JSON형태로 주로 보냄. HTTP message Body에 직접 데이터를 담아 요청

 

GET 쿼리파라미터와 POST HTML Form 전송 방식은 형식이 같기 때문에 구분없이 request.getParameter()를 사용하여 조회 가능하다. 

 

그리고 더 쉬운 방법으로 조회가 가능한데, @RequestParam 어노테이션을 사용하면 단순타입(String, int 등)을 바로 가져올 수 있다. 단순타입이면 생략도 가능하다.

 

또 요청 파라미터가 참조형 객체일 때 @ModelAttribute를 사용할 수 있다. 객체면 생략도 가능하다.

 

그밖에 여러 기능 등은 아래 코드 주석으로 적어두었다.

 

[RequestParamController.java]

@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
    @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 지정 생략 가능)
    */
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(@RequestParam String username, @RequestParam int age) {
        log.info("username={}, age={}", username, age);

        return "ok";
    }

    /**
     * @RequestParam조차 생략 가능(단순타입 String, int 등)
     */
    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age) {
        log.info("username={}, age={}", username, age);

        return "ok";
    }


    /**
     * required=true값 안주면 400 Bad Request
     * error msg : Required request parameter 'username' for method parameter type String is not present
     * age값 안 주면 500 Error -> int는 null이 들어갈 수 없기 때문에 객체 Integer로 바꿔줘야 null처리가 된다.
     */
    @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";
    }


    /**
     * int에 null이 못 들어가는데, defaultValue를 주면 됨
     * defaultValue가 들어가면 실질적으로 required=false가 필요없다.
     * defaultValue는 빈 문자라도 설정값 적용 ex)/request-param-default?username=은 guest
     */
    @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";
    }

    /**
     * Map으로 파라미터 처리해서 get("name")처리 가능
     * MultiValueMap도 가능하다. ex) ?userIds=id1&userIds=id2로 넣으면 (key=userIds, value=[id1,id2])
     */
    @ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));

        return "ok";
    }

    /**
     * @ModelAttribute를 사용하여 객체를 한 번에 받아 사용 가능
     * age=abc처럼 숫자가 들어가야할 곳에 문자가 들어가면 BindException이 발생
     */
    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }

    /**
     * @ModelAttribute 생략 가능(String 등은 @RequestParam을 생략 가능)
     */
    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }
}

 

9.HTTP 요청 파라미터 - 단순 텍스트

 

위에서 설명한 요청 파라미터와 다르게 HTTP 메시지 바디에 데이터를 직접 넣어서 요청하는 경우는 @RequestParam, @ModelAttribute 등 사용 불가하다.

 

API에서는 InputStream, HttpEntity(RequestEntity), @RequestBody를 통해 요청데이터를 꺼낼 수 있다. 다음과 같다.

 

[RequestBodyStringController.java]

@Slf4j
@Controller
public class RequestBodyStringController {
    @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");
    }

    /**
     * InputStream, OutputStream, Writer 등을 직접 받을 수 있음
     * @throws IOException
     */
    @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");
    }

    /**
     * HttpEntity : httpHeader, httpBody 편리하게 조회 가능
     * RequestEntity, ResponseEntity는 HttpEntity를 상속한 객체(몇 가지 기능 더 제공)
     * 스프링MVC 내부에서 HTTP 메시지 바디를 읽어 문자 및 객체 변환하여 전달하는데, 이 때 HTTPMessageConverter 기능 사용
     * @throws IOException
     */
    @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");
    }

    /**
     * 요청 메시지를 @RequestBody로 처리 가능
     * 헤더정보 필요시 @RequestHeader, HttpEntity 추가 사용
     * 메시지 바디 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 전혀 관계 없음
     */
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {

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

        return "ok";
    }
}

 

10.HTTP 요청 파라미터 - JSON

 

단순 텍스트는 위와 같이 inputStream이나 @RequestBody등으로 처리 가능하다. JSON 요청을 보낼 경우 Jackson라이브러리의 ObjectMapper를 통해 변환하는 작업이 필요하다.

 

[RequestBodyJsonController.java]

/**
 * {"username":"hello", "age":20}
 * content-type: application/json
 */
@Slf4j
@Controller
public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * HttpServletRequest로 HTTP 메시지 바디에서 데이터를 읽어온 후 문자 변환 -> Jackson라이브러리로 자바객체 변환
     */
    @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);

        //json형식 요청 데이터를 그대로 출력
        //messageBody={"username":"hello", "age": "20"}
        log.info("messageBody={}", messageBody);

        //json 형식으로 요청 온 데이터를 HelloData로 역직렬화.
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

        //역직렬화된 HelloData 데이터 출력
        //username=hello, age=20
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

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

    /**
     * 요청 데이터를 바로 objectMapper로 역직렬화하여 객체로 변환
     */
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        //json형식 요청 데이터를 그대로 출력
        //messageBody={"username":"hello", "age": "20"}
        log.info("messageBody={}", messageBody);

        //json 형식으로 요청 온 데이터를 HelloData로 역직렬화.
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

        //역직렬화된 HelloData 데이터 출력
        //username=hello, age=20
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }

    /**
     * @RequestBody생략하면 @ModelAttribute가 생략된 것으로 판단 되므로 생략 불가
     * 요청 데이터를 objectMapper 없이 바로 객체로 받을 수 있음
     * json 데이터 형식(content-type:application/json)을 받으면 HTTP메시지컨버터가 ObjectMapper역할을 해줌
     */
    @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로도 json형식을 받을 수 있다.
     * json 데이터 형식(content-type:application/json)을 받으면 HTTP메시지컨버터가 ObjectMapper역할을 해줌
     */
    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) throws IOException {
        HelloData data = httpEntity.getBody();
        log.info("username={}, age={}", data.getUsername(), data.getAge());

        return "ok";
    }

    /**
     * 반환타입도 HelloData타입으로 가능
     * HttpMessageConverter에 의해 객체가 json문자형태로 변환되어 반환
     * 요청시 json to object / 응답시 object to json
     * 응답 나갈 때 json으로 나갈지 판단은 요청시 accept 확인해야 함
     */
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) throws IOException {
        log.info("username={}, age={}", data.getUsername(), data.getAge());

        return data;
    }
}

 

위의 requestBodyJsonV3을 보면 알 수 있듯이 문자로 변환하는 작업이 필요없이 바로 객체로 받을 수가 있다.

 

HttpEntity, @RequestBody 등을 사용하면 HTTP 메세지 컨버터가 HTTP 메시지 바디의 내용(문자, JSON 등)을 문자, 객체 등으로 변환해준다.

 

참고로 요청시 content-type : application/json 여부를 확인해야 한다. 그래야 JSON을 처리할 수 있는 메시지 컨버터가 실행된다. 

 

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

 

스프링 서버에서 응답 만드는 방법은 크게 3가지다.

 

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

 

정적리소스

 

스프링부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.

  • /static, /public, /resources, /META-INF/resources

 

src/main/resources는 리소스를 보관하는 곳이며 클래스패스의 시작 경로이다. 

//정적 리소스 경로
src/main/resources/static

//파일
src/main/resources/static/basic/hello-form.html

//실행방법
http://localhost:8080/basic/hello-form.html

 

뷰템플릿

 

HTML을 동적으로 생성하여 전달한다.

 

//뷰템플릿 경로
src/main/resources/templates

//뷰템플릿 생성
src/main/resources/templates/response/hello.html

 

아래와 같이 html을 만든다.

 

[src/main/resources/templates/response/hello.html]

<!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>

 

위의 뷰 템플릿을 호출하는 부분을 만든다.

 

[ResponseViewController.java]

//@ResponseBody가 없기 때문에 뷰템플릿으로 반환 처리
@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!");
    }
}

 

@ResponseBody가 없으면 response/hello로 뷰리졸버가 실행되어 뷰를 찾고 랜더링한다.

@ResponseBody가 있으면(또는 HttpEntity 반환시) response/hello라는 문자가 HTTP 메시지 바디에 직접 입력된다.

 

response/hello는 논리명이기 때문에 실제 실행은 "template/resources/hello.html"로 된다.

 

그리고 스프링 프로젝트 생성시 타임리프(thymeleaf)를 의존성주입으로 추가해뒀기 때문에 자동으로 ThymeleaftViewResolver와 스프링 빈들을 등록한다.

 

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

 

HTTP API의 경우 데이터를 전달해야 하기 때문에 메시지 바디에 JSON형태로 보통 실어서 보낸다. 참고로 HTML이나 뷰템플릿을 사용해도 HTTP 바디에 HTML 데이터가 담겨서 전달된다. 여기서의 설명은 직접 HTTP 응답메시지를 전달하는 경우다.

 

[ResponseBodyController.java]

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

//@ResponseBody를 클래스레벨에 넣으면 다 적용됨
//@Controller + @ResponseBody = @RestController
@Slf4j
@Controller
public class ResponseBodyController {
    //http message body에 직접 응답메시지 담기 v1 : 서블릿을 직접 다룰 때와 같음
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok");
    }

    //http message body에 직접 응답메시지 담기 v2 : HttpEntity를 상속한 ResponseEntity 반환. 상태코드 지정 가능
    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2() {
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }

    //http message body에 직접 응답메시지 담기 v3 : @ResponseBody를 사용하여 그대로 넘김
    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
        return "ok";
    }

    //HTTP 메시지 컨버터를 통해 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);
    }

    //얘도 v1처럼 Http상태를 바꾸려면 @ResponseStatus 사용
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);

        return helloData;
    }
}

 

13.HTTP 메시지 컨버터

 

HTTP API처럼 JSON 데이터를 메시지 바디에서 직접 읽거나 직접 쓰는 경우 HTTP 메시지 컨버터가 작동한다.

 

응답시 HttpMessageConverter가 작동하는 예시는 다음과 같다.

  • localhost:8080/hello-api 호출
  • helloController의 hello-api URL을 처리할 메소드 실행
  • @ReponseBody가 붙어있으면 HttpMessageConverter가 실행되어 return값 반환

 

@ResponseBody를 사용하면 viewResolver 대신에 HttpMessageConverter가 작동하는 것이다.

요청과 다르게 응답의 경우 요청 Http Header의 Accept와 서버의 컨트롤러 반환타입 정보를 조합하여 컨버터의 종류(StringHttpMessageConverter, MappingJackson2HttpMessageConverter 등) 선택한다. 

 

스프링 MVC는 다음의 경우 HTTP 메시지 컨버터가 적용된다.

  • HTTP Request : @RequestBody, HttpEntity(RequestEntity)
  • HTTP Response : @ResponseBody, HttpEntity(ReponseEntity)

 

HttpMessageConverter의 내부에는 크게 2가지 기능이 있다.

  • canRead(), canWriter() : 메시지 컨버터가 해당클래스와 미디어타입을 지원하는지 체크하는 기능
  • read(), write() : 메시지 컨버터를 통해 메시지를 읽고 쓰는 기능

 

 

[HttpMessageConverter.java]

public interface HttpMessageConverter<T> {
	//주어진 클래스가 이 컨버터로 읽힐 수 있는지 판단.  media타입은 일반적으로 요청시 Content-Type
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

	//주어진 클래스가 이 컨버터를 통해 쓰여질 수 있는지 판단. media타입은 일반적으로 요청시 Accept
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

	//...생략

	//주어진 inputMessage로부터 해당 타입의 객체를 읽음
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	//주어진 객체를 outputMessage로 쓰기
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

 

 

그리고 메시지 컨버터 작동 순위도 있는데 다음과 같다. 참고로 아래의 메시지 컨버터들은 HttpMessageConverter 인터페이스를 상속한 컨버터들이다.

  • 0순위 : ByteArrayHttpMessageConverter
  • 1순위 : StringHttpMessageConverter
  • 2순위 : MappingJackson2HttpMessageConverter

 

ByteArrayHttpMessageConverter의 경우 byte[]를 처리한다. 클래스타입이 byte[]이고 미디어타입이 */*인 경우 동작한다.

  • Request 예시 : @RequestBody byte[] data
  • Reponse 예시 : @ResponseBody return byte[] / 쓰기 미디어 타입은 application/octet-stream(자동 결정)

 

StringHttpMessageConverter의 경우 String문자를 처리한다.클래스타입은 String, 미디어타입은 */*인 경우 동작한다.

  • Request 예시 : @RequestBody String data
  • Response 예시 : @ResponseBody return "ok" / 쓰기 미디어 타입 text/plain

 

MappingJackson2HttpMessageConverter는 클래스타입은 객체나 JSON타입을 주로 처리한다. 미디어타입은 application/json이다.

  • Request 예시 : @RequestBody HelloData data
  • Response 예시 : @ReponseBody return helloData / 쓰기 미디어 타입 application/json

 

요청데이터를 읽는 방법에 대한 정리를 다음과 같이 할 수 있다.

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

 

응답데이터를 읽는 방법에 대한 정리를 다음과 같이 할 수 있다.

  • 컨트롤러 반환시 @ResponseBody, HttpEntity 반환타입 체크한다.
  • 메시지 컨버터의 canWrite()가 호출되어 메시지를 쓸 수 있는지 확인한다.
    • 1)대상 클래스 타입을 확인 : byte[], String, HelloData
    • 2)HTTP 요청의 Accept 미디어타입 지원여부 확인(@RequestMapping의 produces)
  • canWrite() 조건 만족시 write()를 호출하여 HTTP 응답메시지 바디에 데이터를 생성한다.

 

 

 

 

반응형