Spring에서의 직렬화

hongo·2023년 5월 21일
2

ObjectMapper

Spring은 ObjectMapper라는 클래스를 사용해 Json값을 Spring의 자바 객체로 변환한다.

spring-boot-starter-web을 gradle에 의존성으로 추가하면 jackson이라는 라이브러리도 함께 가져오는데, jackson안에 ObjectMapper가 존재한다.

직렬화 (Object -> Json)

ObjectMapperwriteValue() 메서드를 통해 object를 json으로 직렬화할 수 있다.

getter 또는 setter를 선언해주어야 한다.

내부적으로 getXXX, setXXX형태의 메서드를 찾아, json의 key이름을 할당한다.

ex)

name필드의 getter인 getName() 메서드가 있을 때

  • getName이라는 이름을 파싱해 Name을 얻는다.
  • Name의 앞글자를 소문자로 만든 name을 json key이름으로 할당한다.

역직렬화 (Json -> Object)

ObjectMapperreadValue() 메서드를 통해 json을 object로 역직렬화 할 수 있다.

스프링 웹에서는 @RequestBody를 사용하면, readValue()를 수행해준다.

기본생성자getter 또는 setter를 선언해주어야한다.

ObjectMapper는 리플렉션을 사용해 기본생성자와 getter, setter를 참고한다.

리플렉션?

  • 리플렉션은 JVM이 클래스 로더를 통해 읽어온 클래스 정보를 JVM 메모리에 저장하는 것을 의미한다.
  • 리플렉션을 사용하면 생성자, 메소드, 필드 등 클래스에 대한 정보를 아주 자세히 알아낼 수 있다.
  • 리플렉션을 사용하면 접근 제어자와 무관하게 클래스의 필드나 메소드도 가져와서 호출할 수 있다.

단, 리플렉션은 생성자의 인자에 대한 정보는 알아낼 수 없다. 따라서 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 리플렉션을 사용해 객체를 생성할 수 없다. ObjectMapper는 리플렉션을 사용해 역직렬화를 하려면 기본생성자를 선언해주어야한다.

기본생성자가 무조건 필요하다고?

@Getter
@RequiredArgsConstructor
public class ProductRequest {
    private final String name;
    
    private final Integer price;
}

잘 되는데요...?

Spring Boot로 미션을 하다보면 기본생성자없이도 직렬화&역직렬화가 잘 되는 것을 볼 수 있었다.

분명 기본생성자가 있어야 역직렬화를 할 수 있다고 들었는데, 어떻게 정상적으로 동작하는 걸까?

ParameterNamesModule

'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'
    }
}
  • Object를 역직렬화할 수 있는 생성자가 없다고 예외 발생
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)

JSON parse error : 필드가 하나밖에 없는 DTO

장바구니 미션에서 CartProductRequest를 사용해 장바구니에 저장할 Product의 ID를 입력받았다.

CartProductRequestproductId라는 필드만 가지고 있다.

@Getter
@AllArgsConstructor
public class CartProductRequest {
    @NotNull(message = "[ERROR] 상품 아이디를 입력해주세요.")
    private final Long productId;
}

JSON parse error

해당 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 of cart.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로 생성할 수 없다는 단점이 있다.

@JsonCreator와 @JsonProperty

  1. 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없이도 사용할 수 있다.

0개의 댓글