form 태그 POST
- HTML의 form 태그를 사용하여 POST 방식으로 HTTP 요청을 보낼 수 있습니다.
- 이 때 해당 데이터는 HTTP Body에 name=Robbie&age=10 (QueryString) 형태로 서버로 전달됩니다.
- 해당 데이터를 Java의 객체 형태로 받는 방법은 @ModelAttribute 어노테이션을 사용한 후 Body 데이터를 Star 객체에 담습니다.
- Spring 내부적으로는 Jackson 라이브러리를 통해 String이 객체로 변환
POST http://localhost:8080/hello/request/form/model
// Star 클래스
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Star {
private String name;
private int age;
}
@Controller
@RequestMapping({"/hello/request"})
public class RequestController {
public RequestController() {
}
@GetMapping({"/form/html"})
public String helloForm() {
return "hello-request-form";
}
@PostMapping({"/form/model"})
@ResponseBody
public String helloRequestBodyForm(@ModelAttribute Star star) {
return String.format("Hello, @ModelAttribute.<br> name = %s, age = %d", star.getName(), star.getAge());
}
}
API로 통신 테스트한 결과 다음과 같은 결과가 나왔습니다. 디버깅을 해봤을 때 객체에 데이터가 제대로 담기지 못한 것을 확인할 수 있었습니다.
컴파일 된 Star.class 파일을 확인했을 때도 특이사항이 보이지 않았습니다. 제가 생각했을 때 Client에서 데이터가 넘어오면 helloRequestBodyForm() 메서드가 실행되면서 Star star = new Star("Robbert", 10) 로 객체가 생성되고 Getter를 통해 데이터를 가져와야 하는데 왜 제대로 객체가 생성되지 못할까 고민했습니다.
public class Star {
private String name;
private int age;
@Generated
public String getName() {
return this.name;
}
@Generated
public int getAge() {
return this.age;
}
@Generated
public Star(final String name, final int age) {
this.name = name;
this.age = age;
}
@Generated
public Star() {
}
}
원인은 @ModelAttribute 어노테이션 동작 방식에서 찾을 수 있었습니다.
@ModelAttribute는 ModelAttributeMethodProcessor 클래스에서 데이터 바인딩이 이루어 집니다. resolveArgument() 메서드를 따라가보면 동작 방식을 확인할 수 있습니다. 순서는 다음과 같습니다.
- 적절한 생성자를 찾아 새 인스턴스 생성
- 클라이언트가 요청한 파라미터를 기준으로 setter 메서드를 찾아서 실행
코드를 보며 좀 더 자세히 알아보겠습니다. 생성자를 다음과 같은 방법으로 찾습니다. BeanUtils의 getResolvableConstructor() 메서드를 살펴보겠습니다.
public static <T> Constructor<T> getResolvableConstructor(Class<T> clazz) {
Constructor<T> ctor = findPrimaryConstructor(clazz);
if (ctor != null) {
return ctor;
}
Constructor<?>[] ctors = clazz.getConstructors();
if (ctors.length == 1) {
// A single public constructor
return (Constructor<T>) ctors[0];
}
else if (ctors.length == 0) {
// No public constructors -> check non-public
ctors = clazz.getDeclaredConstructors();
if (ctors.length == 1) {
// A single non-public constructor, e.g. from a non-public record type
return (Constructor<T>) ctors[0];
}
}
// Several constructors -> let's try to take the default constructor
try {
return clazz.getDeclaredConstructor();
}
catch (NoSuchMethodException ex) {
// Giving up...
}
// No unique constructor at all
throw new IllegalStateException("No primary or single unique constructor found for " + clazz);
}
가장 먼저 public으로 선언된 생성자를 찾습니다. public 생성자가 없다면 public이 아닌 기본 생성자를 먼저 찾고 다른 생성자가 있다면 매개변수가 가장 적은 생성자를 선택합니다. 즉, 어떤 접근 제한자의 생성자가 존재하더라도 @ModelAttribute를 위한 객체를 생성하는 것에는 문제가 없습니다.
자세한 내용은 다음 링크에서 확인이 가능합니다.
이 내용을 통해 왜 객체에 클라이언트 데이터가 제대로 담기지 않았는지 이해할 수 있었습니다. 기본 생성자가 있다면 기본 생성자가 우선이 되어 객체가 생성되기 때문에 Getter를 이용하여 데이터를 가져와도 default 값만 존재합니다.
그래서 Star 클래스를 수정한다면 다음 2가지 방법이 있습니다.
// 1. 첫 번째 방법
@Getter
@Setter
@NoArgsConstructor
public class Star {
private String name;
private int age;
}
// 2. 두 번째 방법
@Getter
@AllArgsConstructor
public class Star {
private String name;
private int age;
}
Query String 방식
?name=Robbie&age=95 처럼 데이터가 두 개만 있다면 괜찮지만 여러 개 있다면 @RequestParam 어노테이션 하나씩 받아오기 힘들 수 있습니다. 이 때 @ModelAttribute 어노테이션을 사용한다면 Java의 객체로 데이터를 받아올 수 있습니다.
파라미터에 선언한 Star 객체가 생성되고 오버로딩된 생성자 혹은 Setter 메서드를 통해 요청된 name&age 값이 담겨집니다.
@Controller
@RequestMapping("/hello/request")
public class RequestController {
@GetMapping("/form/html")
public String helloForm() {
return "hello-request-form";
}
@GetMapping("/form/param/model")
@ResponseBody
public String helloRequestParam(@ModelAttribute Star star) {
return String.format("Hello, @ModelAttribute.<br> name = %s, age = %d", star.getName(), star.getAge());
}
}
@ModelAttribute는 생략이 가능합니다.
Spring에서는 @ModelAttribute 뿐만 아니라 @RequestParam도 생략이 가능합니다.
그렇다면 Spring은 이를 어떻게 구분할까요?
간단하게 설명하자면 Spring은 해당 파라미터(매개변수)가 SimpleValueType 이라면 @RequestParam으로 간주하고 아니라면 @ModelAttribute가 생략되어있다 판단합니다.
SimpleValueType은 원시타입(int), Wrapper타입(Integer), Data 등의 타입을 의미합니다.
@RequestBody
HTTP Body에 JSON 데이터를 담아 서버에 전달할 때 해당 Body 데이터를 Java의 객체로 전달 받을 수 있습니다.
@Controller
@RequestMapping("/hello/request")
public class RequestController {
@GetMapping("/form/html")
public String helloForm() {
return "hello-request-form";
}
@PostMapping("/form/json")
@ResponseBody
public String helloPostRequestJson(@RequestBody Star star) {
return String.format("Hello, @RequestBody.<br> name = %s, age = %d", star.getName(), star.getAge());
}
}
HTTP Body에 { "name":"Jenny", "age":59 } JSON 형태로 데이터가 서버에 전달됐을 때 @RequestBody 어노테이션을 사용해 데이터를 객체 형태로 받을 수 있습니다.
'Spring' 카테고리의 다른 글
[Spring] JDBC란 무엇이고 어떻게 사용할까? (1) | 2024.09.30 |
---|---|
[Spring] DTO(Data Transfer Object) (0) | 2024.09.30 |
[Spring] Path Variable 과 Request Param (0) | 2024.09.26 |
[Spring] Jackson 이란 무엇인가? (0) | 2024.09.26 |
[Spring] 데이터를 Client에게 반환하는 방법(Response) (1) | 2024.09.26 |