스프링 강의 필기/스프링 MVC 1편

6) 스프링 mvc 기본 기능

void_melody 2022. 8. 3. 00:00

*우선 로그에 대해 간단히 알아보자

 

우리는 평소에 System.out.println()같은 시스템 콘솔로 결과를 확인했다.

운영 시스템에서는 이러한 시스템 콘솔을 사용해서 출력하지 않고, 로그 라이브러리를 활용한다.

 

스프링 부트에서는 기본적으로 Logback,Log4J,Log4J2 등 수많은 라이브러리가 있는데

이를 통합해서 인터페이스로 제공하는 SLF4J 라이브러리를 기본으로 쓴다.

즉, SLF4J는 인터페이스이고, 구현체로 Logback 같은 라이브러리를 사용한다.

private Logger log = LoggerFactory.getLogger(getClass());

private static final Logger log = LoggerFactory.getLogger(XXX.class)

 

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

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

        return "ok";
    }
}

보통 @Controller을 하면 반환 값이 String이면 뷰 이름으로 인식해서 뷰를 찾고 뷰를 렌더링한다.

@RestController의 경우, 반환 값으로 뷰를 찾는 것이 아니라 HTTP 메세지 바디에 바로 입력한다.

이는 추후에 @ResponseBody와 관련이 있다. 추후 설명하겠다!

해당 trace, debug, info,warn,error의 순서대로 로그 레벨이 설정되어있다.

기본 default 설정은 info이다.

만약 내가 로그 레벨을 더 높이고 싶다면 application.properties에서 logging.level.hello.springmvc=debug 이런식으로 설정해줄 수 있다.

 

log.debug("data=" + data);
log.debug("data={}", data);

두 코드의 출력은 방식은 동일해보인다. 하지만 다르다.

만약 내가 로그 출력 레벨을 debug 보다 아래인 info로 했다 가정해보자.

그럼에도 위의 + 연산자는 메모리상에서 연산이 처리가 되어서 시간이 발생한다.

그래서 매우 의미없고 메모리 낭비가 되는 코드이다.

하지만 아래의 코드는 아무 일도 발생하지 않는다.

 

@RequestMapping은 알다시피 해당 url이 오면 그걸 매핑해주는 역할을 한다.

하나 짚고 넘어가야할 부분은

@RequestMapping("/hello-basic") 과 @RequestMapping("hello-basic/")은 엄연히 다른 URL이지만 

스프링에선 같은 요청으로 매핑한다.

또, @RequestMapping에서 method속성을 따로 지정해주지 않는다면 HTTP메소드와 무관하게 호출된다.

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

    @RequestMapping("/hello-basic")
    public String helloBasic(){
        log.info("helloBasic");
        return "ok";
    }
    /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     * @PathVariable("userId") String userId -> @PathVariable 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";
    }

    /**
     * 파라미터로 추가 매핑
     * 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";
    }

    /**
     * 특정 헤더로 추가 매핑
     * 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
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }

}

 

이제 HTTP API를 어떻게 매핑하는지 봐보자.

  • 회원 목록 조회 GET /users
  • 회원 등록 POST      /users
  • 회원 조회 GET        /users/{userId}
  • 회원 수정 PATCH   /users/{userId}
  • 회원 삭제 DELETE /users/{userId}
@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,
                          @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";
    }
}

여기 코드에서 짚고 넘어갈 부분은, 

MultiValueMap 정도인 것 같다.

MultiValueMap은 MAP과 유사하지만, 하나의 키에 여러 값을 받을 수 있다.

KeyA=value1&keyA=value2 이렇게 된다면 MultiValueMap을 사용해야겠지.

 

*HTTP 요청 파라미터

HTTP 요청 메세지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법은 크게 3가지이다.

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

v1의 코드를 보자. request와 response를 받고 getParameter을 활용해 원하는 값을 받아오고 있다.

하지만 request와 response는 큰 객체인데 우리가 원하는 것은 상당히 적다. 이를 개선하고 싶다!

  @ResponseBody //@RestController와 같은 역할. view를 찾지 않고 바로 html로 전송.
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge) {

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

인자를 HttpServletRequest과 HttpServletResponse로 받는 것이 아니라, 아예 어노테이션인 @RequestParam으로 받아들이면서 바로 값을 초기화해버렸다. 아까 v1에서의 세 줄이 한번에 인자로 처리되었다. 매우 편리해보인다.

/**
     * @RequestParam 사용
     * HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
     */
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

만약 파라미터의 이름이 변수 이름과 같으면 @RequestParam의 인자값을 생략할 수 있다.

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

또, 만약 단순한 String, int 등의 타입이라면 @RequestParam도 생략이 가능하다.

이 부분은 매우 중요하다. 생략을 하게 되면 스프링 MVC는 내부에서 required = false로 처리해버린다. 즉, 꼭 필요하지는 않다는 것이다.

강사님은 여기까지 생략하는 것엔 부정적인 입장이다.

왜냐하면 @RequestParam이 있어야 이것이 명확하게 요청 파라미터에서 데이터를 읽는 것을 알 수 있기 때문이다.

 

/**
     * @RequestParam.required
     * /request-param-required -> username이 없으므로 예외
     *
     * 주의!
     * /request-param-required?username= -> 빈문자로 통과
     *
     * 주의!
     * /request-param-required
     * int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
    defaultValue 사용)
     */
    @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";
    }

@RequestParam의 인자 중에 required가 있다. required = true는 기본 default 설정이다. 즉, 무조건 있어야한다.

false를 하게 되면 필수가 아니라서 인자로 안 받을 수도 있다는 소리이다.

false라고 하면 값을 입력안받게 되서 null이 될 수 도 있다.

그런데 int는 원시 타입이라 null을 받을 수 없다. 그래서 이럴 땐 Integer 같은 object 형태로 받아야한다.

  @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를 설정해버리면 만약 값을 안받으면 해당 값으로 처리하겠다라는 의미이니 이럴 땐 null을 걱정하지 않아도 된다.

 

@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 형태로 받을 수 있다.

파라미터의 값이 한 개로 확실하다면 Map을 쓰면 되지만, 여러 개일 것 같다면 MultiValueMap을 사용하는 것이 맞다.

 

Http 요청 파라미터 - @ModelAttribute)

보통 우리가 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야한다.

@RequestParam String username;
@RequestParam int age;

HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);

스프링은 위의 코드를 자동화해주는 @ModelAttribute 기능을 제공한다.

우선 그 전에 요청 파라미터를 받을 객체를 가정해놓고 시작해보자.

@Data
public class HelloData{
	private String username;
    private int age;
}

@Data는 롬복 기능이다.

@Getter, @Setter, @ToString, @EqualsAndHashCode 등을 다 자동 적용해준다.

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

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

위의 코드에서 @ModelAttribute를 생략해도 정상적으로 작동하는 것을 볼 수 있다.

스프링에서

  • String,int, Integer 같은 단순 타입 = @RequestParam이 생략했다 가정한다.
  •  나머지 = @ModelAttribute가 생략했다 가정한다.

HTTP 요청 메세지 - 단순 텍스트)

HTTP message body는 데이터를 직접 담아서 요청한다.

주로 JSON 형식일 것이다.

요청 파라미터와 다르게, 메세지 바디를 통해 데이터가 직접 넘어오는 경우엔 @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);

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

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

아까와 마찬가지로, request와 response 전부가 필요는 없다.

@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 응답 메세지의 바디에 직접 결과를 출력하는 역할을 한다.

@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를 받는 경우도 있다.

메세지 바디 정보를 직접 조회할 수 있다.

반환 할 때도 메세지 바디 정보를 직접 반환할 수 있다.

@ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV3(@RequestBody String messageBody) throws IOException {

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

        return "ok";
    }

v3의 경우는 객체를 받아서 string으로 전환하는 작업이 필요했다.

v4의 경우는 @RequestBody를 통해서 메세지 바디 정보를 조회했다.

 

정리하자면

요청 파라미터를 조회하는 기능은 @RequestParam과 @ModelAttribute를 사용하고

http 메세지 바디를 조회하는 기능은 @RequestBody를 사용하면 된다.

 

HTTP 요청 메세지 - JSON)

@Slf4j
@Controller
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());

    }

문자열로 된 JSON 데이터를 objectMapper를 사용해서 자바 객체로 변환하고 있다.

하지만 해당 request와 response를 인자로 줄 필요가 없어 보인다.

@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를 활용할 수 있다.

여기서, 문자로 변환하고 다시 json으로 변환하는 과정이 불편해보인다. 한번에 객체로 변환시킬 수 있는 방법이 필요해보인다.

  @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

아예 @RequestBody에 직접 만든 객체를 저장해버리는 것이다.

HttpEntity나 @RequestBody를 사용하면 HTTP메세지 컨버터라는 녀석이 바디의 내용을 우리가 원하는 문자나 객체로

변환해준다.

여기서 @RequestBody는 생략이 불가하다.

아까 내용에서 @RequestParam  또는 @ModelAttribute만 생략이 가능하다 했다.

따라서 여기서 @RequestBody를 생략하면 객체니까 @ModelAttribute로 스프링이 변환시켜 버려서 오류가 난다.

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

아니면 아까처럼 HttpEntity를 활용해도 된다.

 @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {

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

반환 값을 아예 객체로 지정해줄 수 있다.

이렇게 되면

@RequestBody  요청 

: JSON요청 -> HTTP메세지컨버터 -> 객체

@ResponseBody 응답

: 객체 -> HTTP메세지컨버터 -> JSON 이 된다.

 

 

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

  • 정적 리소스
    정적인 html,css,js 
  • 뷰 템플릿 사용
    동적인 HTML을 제공할 때 뷰 템플릿 사용
  • HTTP 메세지 사용
    데이터를 전달해야 하므로, 메세지 바디에 JSON 형식의 데이터 전송

정적 리소스의 경우

/static, /public, /resources/,  META-INF/resources 에 있을 경우 정적으로 제공된다.

 

뷰 템플릿을 거쳐서 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>
<p th:text="${data}">empty</p>

해당 부분에서 text는 empty 저 부분을 가리킨다.

즉 empty부분에 data를 채워넣겠다라는 소리다.

@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 responseViewV1(Model model){
        model.addAttribute("data", "hello!");
        return "response/hello";
    }
}

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

 

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

@Slf4j
//@Controller
//@ResponseBody
@RestController
public class ResponseBodyController {
    @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";
    }

    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1(){
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
		
        // 만약에 상황에 따라 동적으로 HttpStatus를 지정해주려한다면 ResponseEntity<>를 사용
        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }

	//ResPonseBody를 사용할 경우 ResponseStatus를 지정할 수 없어 @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;
    }

}

 

responseBodyV1

서블릿을 직접 다룰 때처럼

HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다.

 

responseBodyV2

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

 

responseBodyV3

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

 

responseBodyJsonV1

ResponseEntity 를 반환한다. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다.

 

responseBodyJsonV2

ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody 를 사용하면 이런 것을 설정하기 까다롭다. @ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다. 물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다. 프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity 를 사용하면 된다.

 

@RestController

@Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody 가 적용되는 효과가 있다. 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다. 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러이다. 참고로 @ResponseBody 는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController 에노테이션 안에 @ResponseBody 가 적용되어 있다