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
_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를 구현해야 하는 이유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이 아님 → 안전하게 동작 가능.
✅ 풀어서 말하면 이런 흐름입니다
@JsonDeserialize(using = CodeEnumDeserializer.class)사용Jackson은 기본 생성자로
new CodeEnumDeserializer()실행 → 이때 타입 모름Deserializer가
ContextualDeserializer를 구현했다면,createContextual(...)호출됨 → Jackson이 해당 enum 필드가 어떤 타입인지 알려줌그걸로 타입 정보를 주입한 새 인스턴스로 다시 반환
이후 실제
deserialize()는 타입이 주입된 상태에서 안전하게 동작
✅ 그럼 @JsonDeserialize 방식으로는 공통 deserializer 구현 못하나요?
@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 스타일
ModelMapper 스타일✅ Jackson이 자동으로 하는 일을 수동으로 구현하면?
Jackson이 자동으로 하는 일을 수동으로 구현하면?예를 들어 다음과 같은 JSON 요청을 받는다고 가정해봅시다:
이를 DTO로 바꾸는 것은 일반적으로 다음과 같이 Spring이 자동으로 해줍니다:
하지만 이 자동 역직렬화 과정을 수동으로 구현하면 다음과 같이 할 수 있어요.
✅ 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()
✅ 핵심 요약
목적
객체 ↔ 객체 (DTO ↔ Entity)
JSON ↔ 객체 (직렬화/역직렬화)
생성 흐름
기본 생성자 + setter
기본 생성자 + setter
자동 사용 위치
서비스/컨트롤러 내부
컨트롤러 @RequestBody, @ResponseBody
수동 구현 방식
map(A, B)
readValue(json, Class), writeValueAsString(obj)
좋습니다! 이제 이어서 Jackson의 동작 원리, @JsonCreator, @JsonProperty, 커스텀 직렬화/역직렬화 등 실무에서 자주 쓰이는 확장 기법까지 단계별로 설명드릴게요.
✅ 1. Jackson의 기본 동작 방식
Jackson의 ObjectMapper는 다음과 같은 순서로 객체를 생성하고 값을 주입합니다:
🔸 역직렬화 (JSON → Java 객체)
기본 생성자(no-arg constructor) 호출
JSON 키에 해당하는 setter 또는 public 필드에 값을 주입
✅ 2. @JsonCreator + 생성자 기반 매핑
@JsonCreator + 생성자 기반 매핑불변 객체(immutable object)나 final 필드만 있는 객체를 매핑하려면
setter 없이도 생성자 기반으로 매핑해야 합니다.
💡 이 경우 Jackson은
ProductDto를 생성할 때 이 생성자를 사용하며, setter가 없어도 역직렬화됩니다.
✅ 3. 커스텀 직렬화: JsonSerializer
JsonSerializer🔸 특정 객체를 JSON으로 변환할 때 포맷을 바꾸고 싶다면?
결과:
✅ 4. 커스텀 역직렬화: JsonDeserializer
JsonDeserializer🔸 문자열 "12000원" → 숫자 12000으로 역직렬화
"12000원" → 숫자 12000으로 역직렬화입력 JSON:
✅ 5. 기타 유용한 Jackson 애노테이션
@JsonIgnore
해당 필드는 JSON에서 무시됨 (직렬화/역직렬화 모두)
@JsonInclude
null/빈값 제외 조건 지정
@JsonProperty("jsonName")
필드명 매핑 커스터마이징
@JsonFormat
날짜 포맷 등 형식 지정
@JsonView
조건에 따라 보이는 필드 조절
✅ 6. ObjectMapper 커스터마이징
불필요한 null 필드 제거
모르는 필드 있어도 실패하지 않도록 설정
✅ 마무리 요약
기본 동작
기본 생성자 + setter/public 필드
불변 객체 대응
@JsonCreator + @JsonProperty 생성자 사용
포맷 커스터마이징
JsonSerializer, JsonDeserializer 구현 후 애노테이션 등록
고급 설정
ObjectMapper 커스터마이징으로 유연한 처리 가능
ModelMapper와 차이
ModelMapper는 객체 ↔ 객체 변환용, Jackson은 JSON ↔ 객체 변환용
Last updated