java compiler 매개변수 이름을 유지할 때 발생하는 deserializer 문제

2023, Jul 11    

Error

Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: 
Input mismatch reading Enum `domain.enums.BusinessChannelType`: 
properties-based `@JsonCreator` ([method domain.enums.BusinessChannelType#of(java.lang.String)]) 
expects JSON Object (JsonToken.START_OBJECT), got JsonToken.VALUE_STRING
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 5, column: 29] (through reference chain:)
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1741)
	at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:137)
	at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:392)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
	at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:392)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)

Cause

  • 컴파일러에게 매개변수 이름을 사용하기 위해 compilerArgs옵션 추가된것이 역직렬화 문제 발생시키게 된 것
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <encoding>${project.build.sourceEncoding}</encoding>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

-parameters 옵션?

  • 컴파일러에게 메서드의 매개변수 이름을 유지하도록 하는 것으로, 이 옵션을 추가하면 Java Reflection을 사용하여 메서드 매개변수 이름을 추출할 수 있다. 그래서 클래스 파일에 var~로 시작하는 것 대신 java파일의 변수명으로 알 수 있게 된다.
  • 이는 주로 런타임에서 리플렉션을 사용하는 프레임워크인 Spring Framework에서 매개변수 이름에 기반한 작업을 수행할 때 유용하다.

parameters옵션과 역직렬화 문제

  • 근데 이 옵션을 사용하면 Jackson 또는 다른 JSONParser에서 Enum 클래스를 역직렬화할 때 문제가 발생하게 된다.
  • JSON 파서는 Enum 값을 식별하기 위해 Enum 상수의 이름을 사용하는 경우가 많은데, -parameters 옵션을 사용하면 컴파일러가 메서드 매개변수의 이름을 변경하므로, JSON 파서는 예상한 Enum 상수 이름과 다른 이름을 발견하게 되고, 이로 인해 “JSON parse error: Input mismatch reading Enum”과 같은 오류가 발생하는 것

Solve

Try1. @JsonProperty 사용

  • 해당 enum 클래스에서 @JsonCreator 어노테이션을 사용할 때 @JsonProperty 어노테이션과 함께 선언하도록 한다.

      public enum MyEnum {
          @JsonProperty("VALUE_ONE")
          VALUE1,
        
          @JsonProperty("VALUE_TWO")
          VALUE2;
        
          @JsonCreator
          public static MyEnum fromJson(String value) {
              // 역직렬화 로직
          }
      }
    

Try2. @lombok.experimental.NonFinal 사용

  • Lombok 라이브러리를 사용하면 @lombok.experimental.NonFinal 어노테이션을 Enum 클래스에 추가하여 메서드 매개변수 이름을 유지할 수 있다고 한다. (해보지 않음)

      import lombok.experimental.NonFinal;
        
      public enum MyEnum {
          @NonFinal
          VALUE1,
        
          @NonFinal
          VALUE2;
            
          // 역직렬화 로직
      }
    

Try3. JsonCreator.Mode.*DELEGATING* 사용

  • @JsonCreator 어노테이션을 사용하여 Enum 클래스의 생성자에 (mode = JsonCreator.Mode.DELEGATING) 옵션을 추가하면 해당 Enum 클래스를 역직렬화할 때 발생하는 문제를 해결할 수 있다.
  • 이 모드는 생성자 매개변수의 순서와 관계없이 매개변수 이름을 기반으로 역직렬화를 수행한다.

      @Getter
      @AllArgsConstructor
      public enum BusinessChannelType implements CamelableEnum {
          A_TALK("a.talk"),
          B_TALK("b.talk")
          ;
        
          private final String value;
        
          @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
          public static BusinessChannelType of(String value) {
              return BusinessChannelType.valueOf(CamelcaseUtil.toSnakeName(value));
          }
        
          @JsonValue
          @Override
          public String camelName() {
              return CamelcaseUtil.toCamelName(name());
          }
      }