이걸 모르고 개발하다 고생한 내 모습..
SpringBoot API 개발시 많이들 모르고 넘어가는..
@RequestBody, @ModelAttribute 의 기능 차이입니다!
저도 PostMan 으로 테스트 해봤을때, JSON형식으로 request를 보냈습니다. 하지만 자꾸 반환값으로 Null이 나오는 것은 왜 일까요?
null Exception이 발생한다면 먼저 Controller Layer를 먼저 의심해보십쇼!
제가 이걸 몰라서 왜 그랬는지 한참을 해맸습니다.
아무튼
아래 코드를 봅시다
@PostMapping
public ResponseEntity<String> createPost(@ModelAttribute PostDto postDto)
@PostMapping
public ResponseEntity<String> createComment(@RequestBody CommentDto commentDto);
두 코드의 차이는 어노테이션 차이입니다.
Body를 어떤 형식으로 주냐에 따라서 받지 못하는 경우가 있습니다. 이는 SpringBoot내부의 MessageHandler가 어노테이션마다 다르기 때문인데요. 하나씩 짚어봅시다
이는 HTTP(JSON, XML)을 Java 오브젝트로 변환합니다.
이 데이터는 Spring 내부에서 HttpMessageConverter를 통해 타입에 맞게 변환되는데,Dto에 JSON을 담아서 던지는 예제 코드와 함께 이야기 해봅시다.
Controller 에서 @RequestBody를 사용해서 Dto를 받아보겠습니다
@PostMapping("/requestbody")
public ResponseEntity<RequestBodyDto> testRequestBody(@RequestBody RequestBodyDto requestBodyDto) {
return ResponseEntity.ok(requestBodyDto);
}
public class RequestBodyDto {
private String name;
private long age;
private String password;
private String email;
public RequestBodyDto() {
}
}
Dto객체를 JSON 문자열 변환한 뒤, 이를 Post 요청을 하게 되서 Controller로 전달 됩니다!
Dto에 기본 생성자만 있고 @AllArgsConstructor
를 쓰지도 않았습니다.
어떻게 생성자 없이 Java 객체를 생성할 수 있었을까? 바로 계속 언급한 MessageConverter 입니다.
@RequestBody org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver
클래스의 readWithMessageConverters()라는 메서드를 보면
Spring에 등록된 MessageConverter중
MappingJackson2HttpMessageConverter를 사용합니다.
이 Converter를 보면 내부에서 ObjectMapper를 통해서 JSON값을 Java 객체로 역직렬화를 하는데
직렬화 가능한 클래스들은 기본 생성자를 선언 꼭꼭! 해줘야 한다. -> @RequestBody에 사용하는 Dto는 기본생성자를 정의하지 않으면 바인딩 실패!
Jackson ObjectMapper는 JSON 오브젝트의 필드를 Java 오브젝트에 맵핑할 때 getter,setter을 사용하는데 앞의 get, set를 지우고 나머지 문자의 첫 문자를 소문자로 변환한 문자열을 참조합니다.
그래서 RequestBodyDto에 getter, setter메서드를 모두 정의해줘야 합니다. 안하면 HttpMessageNotWritableException
발생합니다.
@RequestBody를 사용하면 요청 본문의 JSON, XML, Text 등의 데이터가 적합한 HttpMessageConverter를 통해 파싱되어 Java 객체로 변환 된다.
@RequestBody를 사용할 객체는 필드를 바인딩할 생성자나 setter 메서드가 필요없다.
직렬화를 위해 기본 생성자는 필수다.
또한 데이터 바인딩을 위한 필드명을 알아내기 위해 getter나 setter 중 1가지는 정의되어 있어야 한다.
api/url?name=req&age=1
같은 Query String 형태 혹은 요청 본문에 삽입되는 Form 형태의 데이터를 처리합니다!.
rm(post("/modelattribute")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content("name=req&password=pass&email=naver"))
.andExpect(status().isOk())
.andExpect(jsonPath("name").value("req"))
.andExpect(jsonPath("pass").value("pass"))
//...
이런방식으로 보내야 응답을 성공합니다@GetMapping("/modelattribute")
public ResponseEntity<ModelAttributeDto> testModelAttribute(@ModelAttribute ModelAttributeDto modelAttributeDto) {
return ResponseEntity.ok(modelAttributeDto);
}
ModelAttributeDto
@Setter
public class ModelAttributeDto {
private String name;
private long age;
private String password;
private String email;
//Getter만 정의
}
생성자 없어도 가능하다! 하지만 Setter는 있어야 ModelAttribute 에서 Binding이 가능하다!