스프링 공식 문서 뿌수기(7), Hanlder Methods - 데이터 바인딩 과정

2023. 4. 26. 22:24자바/스프링

728x90
반응형

5, 6번 시리즈는 잠깐 빠져서 IoC Container에 대해 포스팅했다. 이번에는 다시 돌아와서 Annotated Controller의 Handler Methods에 대해 정리해보도록 하겠다! (진작 했어야했는데~~)

 

Handler Methods

 

@RequestMapping 어노테이션을 사용하면 여러가지 핸들러 메서드를 사용할 수 있다. 

 

Method Arguments

 

메서드 인자로는 정말 다양한 타입의 인자들이 들어갈 수 있다. 너무 많기 때문에 해당 부분에 대해서는 링크를 남기도록 하겠다.

 

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-methods

 

지금 단계에서 자주 사용하는 내용들만 먼저 정리해보자.

 

@PathVariable: URI pattern에 따라 매핑받는다.
@RequestParam: multipart files를 포함한 서블릿 요청 인자에 접근을 위해서 사용한다. 
@RequestHeader: 요청된 header의 값을 받을 때 사용한다.
@RequestBody: 요청된 body의 값에 접근할 때 사용된다. 이때 HttpMessageConverter가 사용된다.
@RequestPart: multipart/form-data로 전달된 요청을 처리할 때 사용한다. 이 또한 HttpMessageConverter가 사용된다.
@ModelAttribute: data binding과 validation이 적용된 모델에 접근할 때 사용한다. 이는 Databinder가 사용된다.

 

우테코 레벨 2를 진행하는 단계에서 자주 사용하고 있는 어노테이션만 정리해놓은 것이다. 이 어노테이션을 컨트롤러의 메서드 인자로 사용하게 되면 값을 전달받아서 사용할 수 있다. 

 

자, 여기서 중요한 부분은 어노테이션별로 객체로 바인딩을 하는 주체가 달라진다는 것이다.


HttpMessageConverter

 

@RequestBody와 @RequestPart는 HttpMessageConverter가 사용된다. 

 

HttpMessageConverter 공식문서를 보면 HTTP request와 response로부터 전달된 값을 convert하는 것이라고 한다. 조금 더 이해가는 말로 번역해보자면 HTTP 메세지 바디의 내용을 convert할 때 사용하는 것을 보면 된다. 여기서 convert의 대상 객체에 따라서 HttpMessageConverter가 다르게 사용된다.

 

이를테면 String 문자 자체로 사용할 때에는 StringHttpMessageConverter가 사용되고 객체로 역직렬화해서 사용할 때에는 MappingJackson2HttpMessageConverter가 사용된다. 후자의 경우에는 ObjectMapper를 사용해서 객체를 바인딩하는 방식으로 보면 된다. 

 

HttpMessageConverter를 사용해서 객체로 역직렬화를 할 때에는 기본적으로 기본생성자를 생성해야한다.(기본생성자를 만들지 않는 경우에는 프로퍼티나 역직렬화 어노테이션을 활용해야한다.) 따라서 @RequestBody나 @RequestPart를 사용할 때는 이를 주의해서 사용하면 된다. 이와 관련해서 이전에 삽질 좀 하다가 포스팅했던 기록이 있다.

2023.04.18 - [자바/스프링 삽질 기록] - Spring에서 @RequestBody로 전달받는 객체의 필드에 final 키워드를 붙일 수 있을까?

 


Databinder

 

@ModelAttribute는 Databinder를 사용해서 검증이나 객체로 바인딩하는 과정을 거친다고 한다. Databinder을 활용해서 바인딩하고자하는 객체를 등록하는 경우에는 자바 빈 규약을 지켜야한다고 이전 포스팅에서 설명한 적이 있다.

 

따라서 기본적으로 @ModelAttribute 어노테이션을 사용해서 객체를 바인딩할 때에는

 

(1) 기본 생성자를 사용해 객체를 생성하고,

(2) setter 메서드를 통해 바인딩한다.

 

 

그런데 사실은 기본 생성자가 없더라도, 적절한 다른 생성자가 있는 하나 있는 경우에는 해당 생성자로 바인딩하는 과정을 거친다.

 

 

@ModelAttribute에서 객체를 바인딩하는 메서드 중 직접적으로 객체를 생성하고 바인딩해주는 부분의 메서드이다.

해당 메서드에 대한 docs를 읽어보면 기본적으로는 기본생성자 접근 방식을 사용하지만 그게 아니라면 Primary constructor를 사용한다고한다.  

 

createAttribute 메서드 내에서 생성자를 활용하는 부분인 constructAttribute 메서드에서는 다음과 같은 코드가 있다.

주석으로 적혀있던 내용처럼 ctor.getParameterCount()가 0이면(생성자가 없다면 -> 기본생성자만 있는 경우) 기존에 있던 생성자로 바인딩하는 것을 알 수 있다.

 


Primary Constructor에 관하여

 

흐음... 가장 적절한 생성자를 찾아서 매핑해준다고하는데, 여러 생성자가 있는 경우에는 어떤 것을 우선순위로 가지는지는 잘 모르겠다. 조금 구글링해봤는데, 이에 대해서 정리한 글이나 포스팅은 없다...!! ConstructorProperties도 한번 찾아서 봤는데 여기서는 @Primary 어노테이션을 붙여서 Primary constructor를 사용한다. 여기서 힌트를 얻을 수 있을 것 같기는 한데... 뭐 지금은 Handler에 대해서 공부하고 있으니 이 또한 나중으로 미뤄야겠다.

 

조만간 한번 각 어노테이션별로 객체를 바인딩하는 방법에 대해 깔쌈하게 정리해봐야겠다!


Return Values

 

 

@ResponseBody

리턴 값에서 가장 많이 사용하는 어노테이션은 아마 @ResponseBody가 아닐까 싶다. 이 또한 @RequestBody와 마찬가지로 HttpMessageConverter가 직렬화과정을 거쳐 객체를 json 파일로 바인딩한다. 

객체가 json 바디에 담겨서 전달된다. 

 

위의 예시에서는 @ResponseBody 어노테이션을 붙이지 않았는데 잘 동작한다. 그 이유는 Controller 클래스에서 @RestController를 사용하고 있기 때문이다. @RestController 어노테이션에서는 @ResponseBody가 포함되어있기에 이를 메서드 위에 직접 명시하지 않아도 @ResponseBody가 있는 것처럼 잘 동작한다.


HttpEntity<B>, ResponseEntity<B>

직접적으로 HttpEntity<B>, ResponseEntity<B>를 사용해서 json 바디에 객체를 전달해줄 수도 있다. 이 또한 HttpMessageConverter를 통해 객체를 json으로 바인딩하는 과정을 거친다.

 

위와 같이 body에 객체를 전달하면 {field: 값}의 형태로 직렬화가 된다. 

{

    "id": id,
    "name": "이름",
    "email": "email"

}

ResponseEntity는 .ok와 같은 메서드를 통해서 HTTP status 코드를 전달할 수 있다. 

 

@ResponseBody에서는 위와 같이 HTTP status 코드를 따로 전달하려면 @ResponseStatus를 사용해 status code를 전달하면 된다.


String

 

리턴에 String을 반환하는 방법도 있다. 이때도 조금 복잡한데, 여기서는 ViewResolver가 사용된다. ViewResolver는 특별한 Bean 중 하나이다. 

 

ViewResolver는 다음과 같이 설명되고 있다.

핸들러에서 리턴값으로 전달되는 String 값을 기준으로 view를 랜더링하는데 사용된다.

 

그래서 리턴값으로 문자가 전달되면 그 문자의 이름에 대응되는 view 페이지를 랜더링해서 띄워주는 역할을 우선적으로 한다. 

 

displayItemList를 보면 Model을 인자로 받고, "admin"을 리턴값으로 전달한다. 그러면 viewResolver가 resource/templates/admin.html 파일을 랜더링한다.

 

이때 Model의 역할은 어트리뷰트로 전달된 값을 뷰에 전달해주는 역할을 한다. 그래서 뷰에서 해당 데이터들을 사용할 수 있다.

 

그런데 잘못된 값을 리턴하면 랜더링할 수 있는 객체를 찾지 못해 "TemplateInputException"이 발생한다.

 

근데 @ResponseBody와 함께 사용하면 어떻게 돼?


위의 코드에서 displayItemList에 @ResponseBody를 추가했다. 그리고 url을 입력해보면

 

 

admin이라는 문자가 나온다. 뭐... 당연한 결과인 것 같기도 하다. @ResponseBody 어노테이션을 사용하면 리턴에 사용되는 객체를 직렬화해서 json 형태로 변경하니 말이다!

 

어쨌든 어떤 것이 더 높은 우선순위를 가질까 고민하다가 한번 돌려서 확인해봤다. ㅎㅎ

 

뭐... 그 밖에 모델과 관련된 어노테이션들이 존재하는데, 이는 나중에 Model 파트를 학습해보고 정리해봐야겠다.

 

 

Type Conversion

 

 

@RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, @CookieValue 등등 String을 요청을 전달받는 어노테이션을 인자로 사용하는 경우에는 String이 아닌 다른 값으로 바인딩할 수 있다. 물론 복잡한 객체가 아니라 우리가 일상적으로 많이 사용하는 데이터 타입(int, long, Date 등등)으로 바인딩하는 것이 가능하다.

 

만약 복잡한 객체로 바인딩하려고 한다면 WebDataBinder를 사용해서 포메터를 등록하거나 값을 처리해줄 수 있다.(라고 나와있지만 솔직히 부연 설명이 더 있어야할 것 같다. 여기서 DataBinder를 사용한다는 것은 @ModelAttribute을 사용할 때 객체로 전달받고자 한다면 WebDataBinder를 사용하라는 의미인 것 같다. @RequestParam과 같은 어노테이션에서는 바인딩을 할 때 DataBinder를 사용하지 않는 것으로 알고 있다. @RequestParam은 Converter를 사용해서 바인딩한다.)

 

null에 대해서 처리해주고 싶으면 null을 다룰 수 있는 데이터 타입(Long, Integer 등)을 사용해주면된다. null을 받도록 허용하고 싶으면 @Nullable이나 required = false 옵션을 주어서 null이어도 예외가 터지지 않게 처리할 수 있다.

 

@RequestParam이나 @RequestHeader, @PathVariable, @Matrix Variable은 Map<String, String> 형태로로 바인딩해서 사용할 수도 있다. 어떻게 가능한지는 찾아봤는데... 나오지 않는다. 바인딩 방법을 모르겠지만 일단 공식문서에서도 Map을 사용할 수 있다고 설명하고 있기 때문에 일단 사용방법만 알고 넘겨야겠다. 나중에 어떻게 되는지 한번 알아봐야지...!!

 

정리

 

 

후... 공식문서를 보면서 이것저것 찾아보고 작성을 하다보니 이번 포스팅에서는 유난히 정리가 안된 느낌이다. 아무리 개인 학습을 위한 포스팅을 한다고 하더라도 어느 정도 가독성 좋게 이를 처리하는 것이 좋지 않을까?

 

우리가 자주 사용하는 어노테이션에 대해서는 세가지로 분류해볼 수 있을 것 같다.

 

(1) HttpMessageConverter로 값을 바인딩하는 경우 

HttpMessageConverter를 사용하는 경우에는 ObjectMapper를 사용해 값을 바인딩하기 때문에 직렬화나 역직렬화 과정을 거치게 된다. 따라서 일반적으로는 기본 생성자가 필요하다. 이는 Jackson 라이브러리를 사용하는데, Jackson은 json과 관련된 라이브러리이니 @ResponseBody, @RequestBody와 같이 json 형태로 사용될 수 있는 것들은 이러한 과정을 거친다고 생각하자.

 

 

(2) DataBinder로 값을 바인딩하는 경우

DataBinder를 사용하는 경우에는 일반적으로는 WebDataBinder가 사용된다. 이 경우에는 setter와 기본생성자가 있어야하는데, 만약에 기본생성자가 없고, 다른 생성자가 정의되어있는 경우에는 그 생성자를 사용해 바인딩한다. @ModelAttribute가 이에 해당한다. 이는 하나의 챕터로 사용될만큼 내용이 조금 많기 때문에 다음에 한번 더 자세히 정리해봐야겠다.

 

 

(3) 그 외의 경우(뭔지 모름, converter 중 하나인 것 같다...!)

@RequestParam이나 @PathVariable 등등 그 외에는 int, long, Date와 같은 다른 타입으로 캐스팅이 된다. 또한 Map<String, String>의 형태도 가능하다. 이 또한 포멧팅이 가능한데 ConversionService에 Converter를 등록?해서 원하는대로 포멧팅을 할 수 있다.

 

 

이 밖에도 다양한 return value나 파라미터가 있지만 일단 지금 미션을 진행하면서 자주 사용하는 것들에 대해서 먼저 정리했다. 글 구석구석에 빈틈이 여기저기 숨겨져 있으니 나중에 꼭 정리해야겠다.

728x90
반응형