개발/spring

ModelAttribute vs RequestBody data bind, 직렬화 & 역직렬화

방푸린 2023. 2. 21. 15:19
반응형

Get, Post  등등 api를 만들다 보면 데이터를 바인드 하는 방식이 다르다는 것을 알게 된다.

대표적으로 ModelAttribute/Request Param과 Request Body 방식을 비교해 보고, 관련해서 빠질 수 없는 직렬화/역직렬화에 대해 정리해 본다.

 

GET mapping

query param -> object

@GetMapping
List<Product> searchProducts(@Valid ProductCriteria productCriteria) { //다수의 @RequestParam를 dto로 한방에 + 검증도 가능
   return productRepository.search(productCriteria);
}

1. @ModelAttribute의 DTO 요구 사항

@ModelAttribute는 폼 데이터쿼리 파라미터에서 값을 받아서 Java 객체로 바인딩합니다. 

일반적으로 필요한 요소:

  • 기본 생성자 (No-args Constructor): 필수
    @ModelAttribute는 데이터 바인딩을 위해 기본 생성자를 사용합니다. 기본 생성자가 없으면 바인딩이 불가능합니다.
  • Getter/Setter 메서드: 필수
    @ModelAttribute는 요청 데이터(폼 데이터나 쿼리 파라미터)를 필드에 바인딩할 때, 객체 필드에 직접 접근하지 않고 setter 메서드를 사용합니다. 따라서 필드마다 setter가 필요합니다. 또한, 검증 후 데이터를 읽어올 때는 getter가 필요합니다.

1. NoArgsConstructor로 객체 생성 후 Setter로 주입

  • setter가 없다면 값을 넘겨도 null로 세팅된다.
  • setter로 변수 이름 변경 가능하다.(받는 변수명은 idSeq -> 바인딩하는 변수는 seq 로 가능)
@Setter
public class ReceivedUserRequest {

  @NotBlank
  private String mailIdx;
}

2. public AllArgsConstructor 로 주입

public class ReceivedUserRequest {

  @NotBlank
  private String mailIdx;

  public ReceivedUserRequest(String mailIdx) {
    this.mailIdx = mailIdx;
  }
}

Spring은 때때로 생성자 기반 바인딩을 사용할 수 있으며, 이는 주로 다음과 같은 상황에서 발생합니다:

  • 폼 데이터URL 파라미터에서 넘어오는 값이 모든 필드에 전달될 때.
  • 객체 생성 시 한 번에 모든 필드를 초기화할 수 있는 AllArgsConstructor가 있는 경우.

> deserialize 자체에는 getter가 없어도 됨(그렇지만 결국 serialization 하다가 필요해서 에러가 남 ㅋㅋ)

Java implicitly adds a no-arg constructor to all classes when there is no constructors defined. If you define any parameterized constructor then the no-arg constructor will not be added.
You need a default constructor (constructor without any argument) in entities. This is needed because JPA/Hibernate uses the default constructor method to create a bean class using reflections. If you don't specify any constructor (nor Lombok annotation), Java will generate a default constructor (automatically generated by the compiler if no constructors have been defined for the class). But if you add a constructor with parameters (or @AllArgsConstructor), then you'll need to add a no args constructor (or @NoArgsConstructor) as well, for JPA/Hibernate to work.

 


POST mapping

request body -> object

참고로 아래 지식이 필요하다.

When using JSON format, Spring Boot will use an ObjectMapper instance to serialize responses and deserialize requests.

 

<getter/setter/constructor 없이 매핑을 시도하면 에러가 난다>

2. @RequestBody의 DTO 요구 사항

@RequestBody는 JSON 또는 XML과 같은 요청 본문을 Java 객체로 **역직렬화(deserialize)**합니다. @RequestBody는 JSON 데이터를 객체 필드에 직접 바인딩하는 방식이므로 필드 접근 방식이 조금 다릅니다.

필요한 요소:

  • 기본 생성자 (No-args Constructor): 필수 아님
    @RequestBody는 JSON 데이터를 역직렬화할 때 Jackson 라이브러리를 사용합니다. Jackson은 기본 생성자를 사용하거나, @JsonCreator 어노테이션을 사용하여 특정 생성자를 통해 객체를 생성할 수 있습니다. 하지만, 기본적으로 기본 생성자가 있는 것이 편리합니다.
  • Getter/Setter 또는 필드 접근: 필수는 아님
    Jackson은 JSON 데이터를 객체로 변환할 때 필드 또는 Getter/Setter를 통해 값을 설정합니다. 필드가 public이라면 직접 필드에 접근할 수도 있지만, 일반적으로 getter/setter를 통해 데이터를 주고받는 것이 더 안전합니다.

1. setter가 없을 경우, (역직렬화에 setter가 사용되기 때문에)

값을 보내도 null로 인식하여 @NotNull validation이 실패한다.

Field error in object 'cancelRequest' on field 'mailIdx': rejected value [null]; codes [NotBlank.cancelRequest.mailIdx,NotBlank.mailIdx,NotBlank]

 

2. 또한 역직렬화 시 기본 생성자 생성 후 setter를 사용하기 때문에

@NoArgsConstructor나 @AllArgsConstructor가 아닌 일부 생성자만 있는 경우는 아래 에러가 난다.(둘 중 하나만 있어도 매핑이 잘 된다)

 cannot deserialize from Object value (no delegate- or property-based Creator)

 

3. @Getter가 없을 경우, 역직렬화는 문제가 없지만 후에 직렬화 과정에서 null로 내려간다.

 


역직렬화 / 직렬화에 대해 정리하자면 아래와 같다.

역직렬화는 기본적으로 다음과 같은 과정을 거쳐서 처리된다.
- object mapper 사용
- 기본 생성자로 객체를 생성함 -> 기본 생성자가 없으면 에러
-필드값을 찾아서 값을 바인딩해줌 ->  public 필드 또는 public 형태의 setter로 바인딩

직렬화는 다음과 같다.
- Spring에서는 기본적으로 jackson 모듈의 ObjectMapper라는 클래스가 직렬화를 처리하며 ObjectMapper의 writeValueAsString이라는 메서드가 사용됨
- ObjectMapper는 public 필드 또는 public 형태의 getter로 값을 가져옴

 

> 직렬화에 대해 조금 더 깊숙이 들어가면..

application/json타입의 데이터는 스프링부트의 MessageConverter ->  MappingJackson2HttpMessageConverter -> Jackson 라이브러리의 Object mapper클래스를 이용해 Reflection으로 객체를 생성하게 된다. 이때 기본 생성자가 없을 경우 Jackson 라이브러리가 deserialize 할 수 없어 예외가 발생한다.


특히 역직렬화 시 기본 생성자가 사용되면 불변 객체가 아니게 되지 않나 싶은데

@NoArgsConstructor(access = AccessLevel.PRIVATE)

위와 같이 제한을 주면 막 생기는 위험을 막을 수 있다.

728x90
반응형