본문 바로가기

기술 블로그

Java ObjectMapper (Feat. RedisTemplate)

안녕하세요.

이번 포스팅에서는 Java의 ObjectMapper에 대해서 분석한 내용을 공유하고자 합니다.

 

ObjectMapper는 객체의 직렬화 및 역직렬화를 위해 사용되는 클래스입니다.

Redis에 객체 저장을 시도하다가 LocalDateTime 직렬화에 실패하는 문제를 만나 ObjectMapper의 registerModule과 activateDefaultTyping 메소드를 활용하여 문제를 해결한 경험도 있고, ObjectMapper를 자주 사용하고 있기 때문에

해당 기술이 어떻게 동작하기에 객체들을 자동으로 직렬화 및 역직렬화하는지 확인하고자 했습니다.

 

1. ObjectMapper의 동작 흐름

ObjectMapper 클래스의 writeValueAsBytes 직렬화 메소드를 정리해보면, 아래와 같이 진행됩니다.

public byte[] writeValueAsBytes(Object value) throws JsonProcessingException {
    ByteArrayBuilder bb = new ByteArrayBuilder(br);
    this._writeValueAndClose(this.createGenerator((OutputStream)bb, JsonEncoding.UTF8), value);
    byte[] result = bb.toByteArray();
    return result;
 }
    
protected final void _writeValueAndClose(JsonGenerator g, Object value) throws IOException {
    this._serializerProvider(cfg).serializeValue(g, value);
}

protected DefaultSerializerProvider _serializerProvider(SerializationConfig config) {
    return this._serializerProvider.createInstance(config, this._serializerFactory);
}

 

ObjectMapper는 SerializerProvider를 필드로 갖고, 해당 필드를 활용하여 객체를 직렬화 합니다.

 

ObjectMapper는 객체 생성 시점에 기본적으로 DefaultSerializerProvider.Impl 클래스를 매개변수로 갖고, 객체가 직렬화될 때 마다 SerializationConfig와 SerializerFactory를 활용하여 SerializerProvider 객체를 생성하므로 thread-safe하게 동작함을 소스에서 확인할 수 있었습니다.

    public ObjectMapper(JsonFactory jf, DefaultSerializerProvider sp, DefaultDeserializationContext dc) {
    	// ... 생략
        this._serializerProvider = (DefaultSerializerProvider)(sp == null ? new DefaultSerializerProvider.Impl() : sp);
        this._serializerFactory = BeanSerializerFactory.instance;
    }

 

 

DefaultSerializerProvider의 serializeValue 메소드는 직렬화하고자 하는 객체의 Class 정보를 활용하여 실제 직렬화를 담당할 JsonSerializer를 찾고 있었습니다.

public void serializeValue(JsonGenerator gen, Object value) throws IOException {
    JsonSerializer<Object> ser = this.findTypedValueSerializer(cls, true, (BeanProperty)null);
    this._serialize(gen, value, ser);
}

 

findTypedValueSerializer는 캐시에 저장된 Serializer를 찾거나, 캐싱된 것이 없다면 SerializerFactory를 이용하여 Serializer를 새롭게 생성합니다.

 

2. SerializerFactory 

ObjectMapper의 객체 직렬화 메소드가 실행될 때 마다 SerializerFactory를 활용해서 SerializerProvider를 생성합니다. 그렇다면 SerializerFactory는 어느 시점에 업데이트가 되는지 확인이 필요했습니다. 

 

ObjectMapper의 registerModule 메소드를 통해 그 단서를 찾아갈 수 있었습니다.

    public ObjectMapper registerModule(Module module) {
       Iterator var4 = module.getDependencies().iterator();
       while(var4.hasNext()) {
           Module dep = (Module)var4.next();
           this.registerModule(dep);
       }
       
       module.setupModule(new Module.SetupContext() {
           public void addSerializers(Serializers s) {
               ObjectMapper.this._serializerFactory = ObjectMapper.this._serializerFactory.withAdditionalSerializers(s);
           }

           public void addKeySerializers(Serializers s) {
               ObjectMapper.this._serializerFactory = ObjectMapper.this._serializerFactory.withAdditionalKeySerializers(s);
           }
       });
       return this;
    }

 

registerModule 메소드는 Module 객체의 setupModule 메소드를 실행하고, ObjectMapper 클래스 내 익명 클래스로 Module.SetupContext 인터페이스를 구현하고 있음을 확인하였습니다.

 

위와 같은 디자인 패턴에 의해 Module 추상 클래스를 상속한 JavaTimeModule이 ObjectMapper에 등록되면, JavaTimeModule에 등록된 Serializer가 ObjectMapper의 SerializerFactory에 등록되는 것을 알 수 있었습니다.

 

RedisTemplate을 통해 저장하고자 하는 객체에 LocalDatimeTime이 있을 때 직렬화에 실패하는 오류가 있었고, 이는 ObjectMapper의 SerializerFactory 필드에 저장된 Serializer와 관련이 있음을 알 수 있었습니다.

 

 

3. objectMapper.activateDefaultTyping

Jackson은 객체를 직렬화할 때 Class 타입 정보를 Json에 기본적으로 포함하지 않습니다. 하지만, 직렬화되는 객체가 다형성을 이용하고 있는 경우, Json에 그 타입을 명확히 지정해야 역직렬화가 가능합니다.

그러므로, 객체가 다형성을 활용하고 있는 상황에서의 정상적인 직렬화 및 역직렬화를 위해서 activateDefaultTyping 메소드의 활용이 필요하다고 합니다.

 

아래와 같이 사용할 수 있습니다.

    objectMapper.activateDefaultTyping(
            BasicPolymorphicTypeValidator.builder()
            .allowIfSubType(Object.class)
            .build(),
            ObjectMapper.DefaultTyping.NON_FINAL
    );

 

해당 메소드의 매개변수는 아래와 같습니다.

  • PolymorphicTypeValidator : 타입 정보를 허용하거나 제한하기 위해 사용함
    • Ex1. Parent.class를 상속하고 있는 클래스만 역직렬화 되도록 제한할 수 있습니다.
    • Ex2. "com.example.xxx" 패키지 내 클래스만 역직렬화 되도록 제한할 수 있습니다.
  • ObjectMapper.DefaultTyping : JSON 객체 내 타입 정보를 활용하여 객체를 역직렬화하므로 DefaultTyping을 통해 타입 정보를 JSON 내 어디에 추가힐지를 결정함 
    • NON_CONCRETE_AND_ARRAYS : 추상 클래스, 인터페이스, 배열이 BaseType인 경우에 타입 정보를 추가함
    • OBJECT_AND_NON_CONCRETE : 배열을 포함한 모든 객체에 타입 정보를 추가함
    • NON_FINAL : final이 아닌 모든 클래스에 타입 정보를 추가함
    • EVERYTHING : primitive 타입을 포함한 모든 클래스에 타입 정보를 추가함
  • JsonTypeInfo.As : 타입 정보를 Json에 추가하는 방법 결정
    • PROPERTY : JSON 객체에 "@class" 속성을 추가(기본 값)
    • WRAPPER_OBJECT : 타입 정보를 JSON 객체로 래핑
    • WRAPPER_ARRAY : 타입 정보를 JSON 배열로 래핑

JsonTypeInfo.As에 따른 직렬화 결과는 아래와 같습니다.

// property
{
    "@class": "MyType",
    "field1": "xxx",
    "field2": "yyy"
}

// wrapper_object
{
  "MyType": {
    "field1": "xxx",
    "field2": "yyy" 
    }
}

// wrapper_array
[
  "MyType": {
    "fiedl1": "xxx",
    "field2": "yyy"
    }
 ]
]

 

예를 들어, Redis의 Serializer 중 GenericJackson2JsonRedisSerializer 클래스는 모든 객체를 Object.class로 직렬화 및 역직렬화를 하므로 activateDefaultTyping 메소드를 통해 Object.class를 상속하고 있는 모든 클래스가 직렬화/역직렬화 되도록 설정하는 등의 작업이 필요합니다.

 

 

4. @JsonTypeInfo

ObjectMapper의 activateDefaultTyping으로 직렬화/역직렬화 방식 기본 설정을 할 수 있지만, @JsonTypeInfo 어노테이션을 통해서 클래스 별로 직렬화/역직렬화 방식을 다르게 할 수 있습니다.

 

활용 예시는 아래와 같습니다.

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
class MyType {}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
abstract class Animal {}

 

  • @JsonTypeInfo
    • use 속성 : JSON에 타입정보를 어떻게 저장할지를 결정함
      • JsonTypeInfo.Id.CLASS : 패키지명을 포함한 클래스 이름을 저장함
      • JsonTypeInfo.Id.MINIMAL_CLASS : 패키지명을 제외한 클래스 이름을 저장함
      • JsonTypeInfo.Id.NAME : 사용자가 직접 정의한 타입 이름을 저장함
      • JsonTypeInfo.Id.NONE : 타입 정보를 포함하지 않음
    • include 속성 : JSON에 타입정보를 추가하는 방식을 결정함
      • JsonTypeInfo.As.PROPERTY
      • JsonTypeInfo.As.WRAPPER_OBJECT
      • JsonTypeInfo.As.WRAPPER_ARRAY
      • JsonTypeInfo.As.EXISTING_PROPERTY : 클래스 내의 기존 필드를 그대로 속성으로 사용(@JsonSubTypes에서 활용함). 

'기술 블로그' 카테고리의 다른 글

@Transactional Deep Dive  (1) 2025.02.19
Java Annotation  (2) 2025.02.18
스프링 Kafka Consumer Deep Dive - 1  (1) 2025.02.05
Spring Security에서의 Filter  (0) 2024.12.17
필터와 인터셉터란?  (0) 2024.12.11