Spring은 ObjectMapper
라는 클래스를 사용해 Json값을 Spring의 자바 객체로 변환한다.
spring-boot-starter-web
을 gradle에 의존성으로 추가하면 jackson
이라는 라이브러리도 함께 가져오는데, jackson
안에 ObjectMapper
가 존재한다.
ObjectMapper
는 writeValue()
메서드를 통해 object를 json으로 직렬화할 수 있다.
getter
또는 setter
를 선언해주어야 한다.
내부적으로 getXXX
, setXXX
형태의 메서드를 찾아, json의 key이름을 할당한다.
ex)
name필드의 getter인
getName()
메서드가 있을 때
getName
이라는 이름을 파싱해Name
을 얻는다.Name
의 앞글자를 소문자로 만든name
을 json key이름으로 할당한다.
ObjectMapper
는 readValue()
메서드를 통해 json을 object로 역직렬화 할 수 있다.
스프링 웹에서는 @RequestBody
를 사용하면, readValue()
를 수행해준다.
기본생성자
와 getter
또는 setter
를 선언해주어야한다.
ObjectMapper
는 리플렉션을 사용해 기본생성자와 getter, setter를 참고한다.
단, 리플렉션은 생성자의 인자에 대한 정보는 알아낼 수 없다. 따라서 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 리플렉션을 사용해 객체를 생성할 수 없다. ObjectMapper
는 리플렉션을 사용해 역직렬화를 하려면 기본생성자를 선언해주어야한다.
@Getter
@RequiredArgsConstructor
public class ProductRequest {
private final String name;
private final Integer price;
}
잘 되는데요...?
Spring Boot로 미션을 하다보면 기본생성자없이도 직렬화&역직렬화가 잘 되는 것을 볼 수 있었다.
분명 기본생성자가 있어야 역직렬화를 할 수 있다고 들었는데, 어떻게 정상적으로 동작하는 걸까?
'org.springframework.boot:spring-boot-starter-web'
를 gradle에 추가하면 com.fasterxml.jackson.module:jackson-module-parameter-names
도 자동으로 추가한다.
com.fasterxml.jackson.module:jackson-module-parameter-names
모듈안의 ParameterNamesModule
클래스가 JsonCreator
를 사용해 기본생성자가 없는 객체도 역직렬화가 가능하게 설정해준다.
public class ParameterNamesModule extends SimpleModule
{
private static final long serialVersionUID = 1L;
private final JsonCreator.Mode creatorBinding;
public ParameterNamesModule(JsonCreator.Mode creatorBinding) {
super(PackageVersion.VERSION);
this.creatorBinding = creatorBinding;
}
public ParameterNamesModule() {
super(PackageVersion.VERSION);
this.creatorBinding = null;
}
}
다만, 이는
@RequestBody
와 함께 쓰일 때만 적용된다.임의로 ObjectMapper 객체를 생성해
readValue()
를 사용하면 적용되지 않는다.
해당 모듈을 제거하면, 기본생성자 없이 역직렬화가 수행되지 않는다.
dependencies {
implementation ('org.springframework.boot:spring-boot-starter-web'){
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-parameter-names'
}
}
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.example.Product` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
장바구니 미션에서 CartProductRequest
를 사용해 장바구니에 저장할 Product
의 ID를 입력받았다.
CartProductRequest
는 productId
라는 필드만 가지고 있다.
@Getter
@AllArgsConstructor
public class CartProductRequest {
@NotNull(message = "[ERROR] 상품 아이디를 입력해주세요.")
private final Long productId;
}
해당 DTO를 사용해서 요청을 보내니 다음과 같은 에러가 발생했다.
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of
cart.dto.CartProductRequest
(although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance ofcart.dto.CartProductRequest
(although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]]
CartProductRequest
를 역직렬화 할 수 없어 발생한 에러라고 한다! 필드가 두 개 이상인 DTO들은 잘 역직렬화가 되는 것을 보아 이 에러는 필드가 하나일 때 발생하는 에러인 것 같다. 왜 필드가 하나이면 JSON parse error가 발생하는 걸까?
JsonCreator
의 Mode를 어떤 것으로 설정해야 할지 몰라서 그렇다고 한다...
public enum Mode {
DEFAULT,
DELEGATING,
PROPERTIES,
DISABLED
}
지선생님... 헬프미...
DEFAULT: 이 모드는 호출자가 모드 선택에 대한 휴리스틱(heuristics)을 사용해야 함을 나타내는 의사 모드입니다. 주로 구조화된 타입을 사용하는 단일 인자 생성자에 대해 델리게이트 모드를 선호합니다. 구체적인 모드를 지정하지 않았을 때의 기본값입니다.
DELEGATING: 이 모드는 생성자가 단일 인자를 사용하는 경우, 전체 입력 데이터 값을 해당 인자의 선언된 타입에 바인딩하고, 이 "델리게이트(delegate)" 값이 생성자의 인자로 전달되어 객체를 생성합니다. 이 모드는 단일 인자 생성자에 주로 사용됩니다.
PROPERTIES: 이 모드는 생성자의 인자를 입력된 객체의 일치하는 속성들과 바인딩합니다. 생성자의 인자 이름(명시적 또는 암시적)을 사용하여 입력된 객체의 속성과 매칭시킵니다. 이 모드는 다중 인자 생성자에 항상 사용되며, 유일한 모호한 경우는 단일 인자 생성자입니다.
DISABLED: 이 모드는 생성자를 사용하지 않아야 함을 나타내는 의사 모드입니다. 이 모드는 사용자 정의 어노테이션 인트로스펙터나 어노테이션 믹스인(예: 다른 생성자를 선택하는 경우)에서 명시적으로 비활성화하는 데 사용될 수 있습니다.
@JsonCreator 어노테이션을 사용하여 JSON 데이터를 Java 객체로 역직렬화할 때, Mode를 명시적으로 지정하지 않으면 기본값으로 DEFAULT 모드가 사용됩니다.
PROPERTIES: 이 모드는 생성자의 인자를 입력된 객체의 일치하는 속성들과 바인딩합니다. 생성자의 인자 이름(명시적 또는 암시적)을 사용하여 입력된 객체의 속성과 매칭시킵니다. 이 모드는 다중 인자 생성자에 항상 사용되며, 유일한 모호한 경우는 단일 인자 생성자입니다.
-> 우리가 직렬화&역직렬화를 하기 위해 일반적으로 사용하는 방식은 PROPERTIES
이다. 하지만 단일 인자 생성자일 때는 스프링이 DELEGATING
을 적용해야 할지, PROPERTIES
를 적용해야할 지를 헷갈려한다고 한다... 자세한 이유는 모르겠다... 쏘리
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class CartProductRequest {
@NotNull(message = "[ERROR] 상품 아이디를 입력해주세요.")
private Long productId;
}
기본 생성자가 있으면 Json parse error가 발생하지 않는다.
다만 기본생성자가 존재하므로, 필드를 final로 생성할 수 없다는 단점이 있다.
- Add
@JsonCreator
to constructor and@JsonProperty
to its arguments (to instruct Jackson how to substitute JSON items into constructor in proper order)@Getter public class CartProductRequest { @NotNull(message = "[ERROR] 상품 아이디를 입력해주세요.") private final Long productId; @JsonCreator public CartProductRequest(@JsonProperty("productId") Long productId) { this.productId = productId; } }
생성자에 @JsonCreator
를 붙이고 매개변수 앞에@JsonProperty
를 사용해서 매칭되는 json key를 명시해주면 기본생성자 없이도 사용할 수 있다.
매개변수의 이름과 json의 value명이 같으면
@JsonProperty
없이도 사용할 수 있다.