ModelMapper, Jackson,MessageConverter

Jackson 확장 구조

https://d2.naver.com/helloworld/0473330

이와 같이 CodeEnum을 상속하는 Enum에 대해 코드만 받아도 자동으로 enum 객체로 desialzier 해주는 커스텀 deserialer를 적용해야 한다.

역직렬화할 객체, 객체 필드에 @JsonDeserialize를 통해 deserializer 등록

첫 번째로 JSON에서 역직렬화 대상이 되는 클래스에 deserializer를 지정할 수 있다. 이 경우 JSON에서 해당 클래스로 역직렬화가 진행되는 모든 경우에 지정한 deserializer를 사용한다.

@JsonDeserialize(using = CodeEnumDeserializer.class)
public enum UserType implements CodeEnum {  
    ADMIN("00000A0"),
    USER("00000U0");
    // 생략
}

class MyRequest {  
    UserType  userType;
}

두 번째로 JSON에서 역직렬화를 진행할 deserializer를 클래스 내의 필드마다 지정할 수 있다. 이 경우 JSON에서 해당 클래스로 역직렬화가 진행되는 경우에만, 명시된 필드에 해당 deserializer를 사용한다.

class MyRequest {  
   @JsonDeserialize(using = CodeEnumDeserializer.class)
   UserType userType;
}

아주 좋은 질문입니다. 요약하면: “왜 ContextualDeserializer를 구현하지 않으면 공통 enum deserializer를 제대로 만들 수 없느냐”, “기본 생성자만 있으면 왜 _valueClass가 null이 되는가”, 그리고 “결국 애너테이션 방식(@JsonDeserialize)으로는 공통 deserializer를 못 쓰는 건가?” 를 묻는 거죠.


✅ 먼저, 질문의 핵심 요점

✔️ 당신이 하고 싶은 일

public enum UserType implements CodeEnum { ADMIN, USER }
public enum ProductType implements CodeEnum { FOOD, CLOTH }

@JsonDeserialize(using = CodeEnumDeserializer.class)
private UserType type;

@JsonDeserialize(using = CodeEnumDeserializer.class)
private ProductType type;

여기서 CodeEnumDeserializer 하나로 모든 enum 타입을 처리하고 싶다.


✅ 근데 문제 발생: _valueClass == null

왜? @JsonDeserialize(using = ...) 방식으로 Deserializer를 등록하면, Jackson은 기본 생성자를 통해 무작정 객체부터 생성합니다.

🔍 Jackson 내부 흐름 (요약)

// Jackson 내부의 기본 Deserializer 생성 흐름
Constructor<T> ctor = clazz.getDeclaredConstructor(); // 🔹 기본 생성자만 찾음
CodeEnumDeserializer deserializer = new CodeEnumDeserializer(); // ← 이 시점엔 타입 정보 없음!

즉, 이 시점에는 UserType, ProductType무슨 타입인지 모릅니다.

따라서 아래와 같은 코드에서 NullPointerException 발생:

Class<? extends Enum> enumType = (Class<? extends Enum>) this._valueClass;
Enum.valueOf(enumType, value); // 💥 enumType이 null이므로 NPE

✅ 해결책: ContextualDeserializer를 구현해야 하는 이유

ContextualDeserializer는 말 그대로 **"현재 이 deserializer가 어떤 필드에 붙었는지를 Context로부터 알아내는 인터페이스"**입니다.

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
    JavaType type = property.getType();                   // 🔹 ex: UserType.class
    Class<?> enumClass = type.getRawClass();              // 🔹 진짜 enum 타입
    return new CodeEnumDeserializer(enumClass);           // 🔹 타입 명확히 주입
}

이렇게 하면 이후 deserialize()에서 _valueClass가 null이 아님 → 안전하게 동작 가능.


✅ 풀어서 말하면 이런 흐름입니다

  1. @JsonDeserialize(using = CodeEnumDeserializer.class) 사용

  2. Jackson은 기본 생성자로 new CodeEnumDeserializer() 실행 → 이때 타입 모름

  3. Deserializer가 ContextualDeserializer를 구현했다면,

  4. createContextual(...) 호출됨 → Jackson이 해당 enum 필드가 어떤 타입인지 알려줌

  5. 그걸로 타입 정보를 주입한 새 인스턴스로 다시 반환

  6. 이후 실제 deserialize()타입이 주입된 상태에서 안전하게 동작


✅ 그럼 @JsonDeserialize 방식으로는 공통 deserializer 구현 못하나요?

❌ 아닙니다. 할 수 있어요! 단, 반드시 ContextualDeserializer직접 구현해서 타입 주입 흐름을 처리해야 합니다.

💡 못 하는 게 아니라, 기본 생성자만 쓰면 무조건 실패한다는 뜻입니다.


✅ 만약 enum 타입이 고정이라면?

이건 아주 간단합니다:

public class UserTypeDeserializer extends StdDeserializer<UserType> {
    public UserTypeDeserializer() { super(UserType.class); }

    @Override
    public UserType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String code = p.getText();
        return UserType.valueOf(code);
    }
}
  • 타입이 고정이라면 생성자에 미리 타입을 넣으면 되므로,

  • ContextualDeserializer도 필요 없습니다.


✅ 정리 요약

상황
해결 방법
필요한가?

enum 타입이 고정 (UserType 전용)

생성자에 UserType.class 직접 주입

enum 타입이 다양 (CodeEnum 인터페이스 공유)

ContextualDeserializer 구현해서 타입 주입

✅ 필수

기본 생성자만 사용하면?

타입 정보가 없으므로 _valueClass == null → NPE 발생

❌ 위험


✅ 진짜 공통 deserializer를 만들고 싶다면?

반드시 이렇게 구현해야 안전합니다:

public class CodeEnumDeserializer extends StdDeserializer<Enum<? extends CodeEnum>>
        implements ContextualDeserializer {

    private final Class<? extends Enum<?>> enumType;

    public CodeEnumDeserializer() {
        this.enumType = null; // 초기에는 null
    }

    public CodeEnumDeserializer(Class<?> enumType) {
        super(enumType);
        this.enumType = (Class<? extends Enum<?>>) enumType;
    }

    @Override
    public Enum<? extends CodeEnum> deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        String code = p.getText();

        for (Enum<?> constant : enumType.getEnumConstants()) {
            CodeEnum e = (CodeEnum) constant;
            if (e.getCode().equals(code)) {
                return (Enum<? extends CodeEnum>) e;
            }
        }

        throw new IllegalArgumentException("Unknown code: " + code);
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        Class<?> enumClass = property.getType().getRawClass();
        return new CodeEnumDeserializer(enumClass);
    }
}

필요하시면 이 구조를 Spring 전체에 전역 등록하는 방식 (SimpleModule, ObjectMapper) 도 같이 설명드릴게요!

훌륭한 질문입니다! ModelMapper는 DTO ↔ Entity 간의 명시적 매핑을 위해 사용되는 라이브러리이고, Jackson은 HTTP 요청/응답 본문 ↔ Java 객체 간의 직렬화/역직렬화를 담당합니다.


🔍 질문 요지

ModelMapper.map(...)처럼 Jackson도 내부에서 "기본 생성자 → setter로 세팅" 흐름을 쓰는데, 그 과정을 명시적으로 코드로 작성한다면 어떻게 될까?


ModelMapper 스타일

public ProductDto add(ProductDto productDto) {
    Product product = modelMapper.map(productDto, Product.class);
    Product savedProduct = repository.save(product);
    ProductDto result = modelMapper.map(savedProduct, ProductDto.class);
    return result;
}

Jackson이 자동으로 하는 일을 수동으로 구현하면?

예를 들어 다음과 같은 JSON 요청을 받는다고 가정해봅시다:

{
  "name": "치킨",
  "price": 12000
}

이를 DTO로 바꾸는 것은 일반적으로 다음과 같이 Spring이 자동으로 해줍니다:

@PostMapping("/products")
public ResponseEntity<ProductDto> createProduct(@RequestBody ProductDto productDto) {
    ...
}

하지만 이 자동 역직렬화 과정을 수동으로 구현하면 다음과 같이 할 수 있어요.


✅ Jackson 수동 역직렬화 + 직렬화 예제

@PostMapping("/products")
public ResponseEntity<String> createProduct(HttpServletRequest request) throws IOException {
    // 1. 요청 바디에서 JSON 직접 읽기
    String json = request.getReader().lines().collect(Collectors.joining());

    // 2. ObjectMapper로 JSON → ProductDto
    ObjectMapper objectMapper = new ObjectMapper();
    ProductDto productDto = objectMapper.readValue(json, ProductDto.class);

    // 3. 로직 처리
    Product product = new Product();
    product.setName(productDto.getName());
    product.setPrice(productDto.getPrice());

    Product saved = repository.save(product);

    // 4. 다시 DTO로 변환
    ProductDto responseDto = new ProductDto();
    responseDto.setId(saved.getId());
    responseDto.setName(saved.getName());
    responseDto.setPrice(saved.getPrice());

    // 5. ObjectMapper로 DTO → JSON 직렬화
    String responseJson = objectMapper.writeValueAsString(responseDto);

    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(responseJson);
}

🔍 이 흐름을 단계별로 보면:

단계
Jackson 내부에서 자동으로 하는 작업
위 예시에서 수동으로 구현한 부분

1. JSON 읽기

HTTP 메시지 바디 추출

request.getReader()

2. 역직렬화

JSON → DTO (@RequestBody)

objectMapper.readValue()

3. 비즈니스 로직

사용자가 직접 작성

repository.save(...)

4. DTO 작성

return ProductDto 자동 처리

직접 DTO 생성 및 세팅

5. 직렬화

DTO → JSON (@ResponseBody)

objectMapper.writeValueAsString()


✅ 핵심 요약

항목
ModelMapper
Jackson (HttpMessageConverter)

목적

객체 ↔ 객체 (DTO ↔ Entity)

JSON ↔ 객체 (직렬화/역직렬화)

생성 흐름

기본 생성자 + setter

기본 생성자 + setter

자동 사용 위치

서비스/컨트롤러 내부

컨트롤러 @RequestBody, @ResponseBody

수동 구현 방식

map(A, B)

readValue(json, Class), writeValueAsString(obj)


좋습니다! 이제 이어서 Jackson의 동작 원리, @JsonCreator, @JsonProperty, 커스텀 직렬화/역직렬화 등 실무에서 자주 쓰이는 확장 기법까지 단계별로 설명드릴게요.


✅ 1. Jackson의 기본 동작 방식

Jackson의 ObjectMapper는 다음과 같은 순서로 객체를 생성하고 값을 주입합니다:

🔸 역직렬화 (JSON → Java 객체)

  1. 기본 생성자(no-arg constructor) 호출

  2. JSON 키에 해당하는 setter 또는 public 필드에 값을 주입

public class ProductDto {
    private String name;
    private int price;

    public ProductDto() {} // 기본 생성자 필수

    public void setName(String name) { this.name = name; }
    public void setPrice(int price) { this.price = price; }
}

✅ 2. @JsonCreator + 생성자 기반 매핑

불변 객체(immutable object)나 final 필드만 있는 객체를 매핑하려면 setter 없이도 생성자 기반으로 매핑해야 합니다.

public class ProductDto {
    private final String name;
    private final int price;

    @JsonCreator
    public ProductDto(@JsonProperty("name") String name,
                      @JsonProperty("price") int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() { return name; }
    public int getPrice() { return price; }
}

💡 이 경우 Jackson은 ProductDto를 생성할 때 이 생성자를 사용하며, setter가 없어도 역직렬화됩니다.


✅ 3. 커스텀 직렬화: JsonSerializer

🔸 특정 객체를 JSON으로 변환할 때 포맷을 바꾸고 싶다면?

public class PriceSerializer extends JsonSerializer<Integer> {
    @Override
    public void serialize(Integer price, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(price + "원");
    }
}
public class ProductDto {
    private String name;

    @JsonSerialize(using = PriceSerializer.class)
    private int price;

    // ...
}

결과:

{
  "name": "치킨",
  "price": "12000원"
}

✅ 4. 커스텀 역직렬화: JsonDeserializer

🔸 문자열 "12000원" → 숫자 12000으로 역직렬화

public class PriceDeserializer extends JsonDeserializer<Integer> {
    @Override
    public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getText().replace("원", "");
        return Integer.parseInt(value);
    }
}
public class ProductDto {
    private String name;

    @JsonDeserialize(using = PriceDeserializer.class)
    private int price;

    // ...
}

입력 JSON:

{
  "name": "치킨",
  "price": "12000원"
}

✅ 5. 기타 유용한 Jackson 애노테이션

애노테이션
설명

@JsonIgnore

해당 필드는 JSON에서 무시됨 (직렬화/역직렬화 모두)

@JsonInclude

null/빈값 제외 조건 지정

@JsonProperty("jsonName")

필드명 매핑 커스터마이징

@JsonFormat

날짜 포맷 등 형식 지정

@JsonView

조건에 따라 보이는 필드 조절


✅ 6. ObjectMapper 커스터마이징

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  • 불필요한 null 필드 제거

  • 모르는 필드 있어도 실패하지 않도록 설정


✅ 마무리 요약

주제
핵심 내용

기본 동작

기본 생성자 + setter/public 필드

불변 객체 대응

@JsonCreator + @JsonProperty 생성자 사용

포맷 커스터마이징

JsonSerializer, JsonDeserializer 구현 후 애노테이션 등록

고급 설정

ObjectMapper 커스터마이징으로 유연한 처리 가능

ModelMapper와 차이

ModelMapper는 객체 ↔ 객체 변환용, Jackson은 JSON ↔ 객체 변환용


Last updated