삽질일기

RequestBody 에서 enum의 속성(요소)으로 data 받아 enum 상수값으로 변환하기

키크니개발자 2022. 10. 19. 23:12

안녕하세요!

키크니 개발자 입니다. 🦒

 

문제상황

RequestBody를 사용하면서 dto 로 data를 받기 위해 Enum을 사용하였습니다.

상수값으로 data를 전달하면 바로 받을 수 있지만,

이번 상황은 상수값에 대한 속성을 JSON에 포함시켜 data를 받아와야하는 상황이었습니다.

 

Fruit enum class

@Getter
public enum Fruit {
    APPLE("사과"),
    BANANA("바나나"),
    GRAPE("포도"),
    ORANGE("오렌지");
    
    private final String name;
    
    Fruit(String name) {
        this.name = name;
    }
}

FruitController

@PostMapping("/fruit")
public String saveFruit(@RequestBody FruitDto body) {
    log.info("save fruit. body={}", body.toString());

    fruitService.saveFruit(body);

    return "OK";
}

FruitDto

@Data
public class FruitDto {

    private Fruit fruit;
    private Integer count;
}

request data

상수값의 속성으로 data를 넣고 request를 하면 json parse error인 InvalidFormatException exception이 발생하였습니다.

(request data를 "fruit" : "APPLE" 로 넣어주면 요청이 잘 됩니다.)

{
    "fruit" : "사과",
    "count" : 1
}

 

자세한 error 내용

org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
 Cannot deserialize value of type `xx.xxx.xxx.XXX` from String "사과": not one of the values accepted for Enum class:
 [APPLE, BANANA, GRAPE, ORANGE];
 nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: 
Cannot deserialize value of type `xx.xxx.xxx.XXX` from String "사과":
 not one of the values accepted for Enum class:  [APPLE, BANANA, GRAPE, ORANGE]

 

해결방법은?

처음에는 converter를 사용하면 되는줄 알았지만 @RequestParam을 사용하는 것이 아닌, 

@RequestBody를 사용하면 무용지물이 된다는 것을 알았습니다.

 

검색을 하여 찾은 결과 Jackson 2.6부터는 @JsonProperty로 enum의 각 요소를 사용하여 직렬화/역직렬화 값을 지정할 수 있는 것을 알게되었습니다.

더 자세하게 알고싶으면 클릭

 

Jackson 2.6 이상 해결방법

  • @JsonProperty를 사용합니다.
@Getter
public enum Fruit {
    @JsonPropertyp("사과")
    APPLE("사과"),
    @JsonPropertyp("바나나")
    BANANA("바나나"),
    @JsonPropertyp("포도")
    GRAPE("포도"),
    @JsonPropertyp("오렌지")
    ORANGE("오렌지");
    
    private final String name;
    
    Fruit(String name) {
        this.name = name;
    }
}

Jackson 2.6 이하 해결방법

  • @JsonCreator를 사용합니다.
  • @JsonCreator란?
    • 해당 클래스 JSON 문자열을 받아서 객체를 생성할 때 변환기를 직접 만들고자 할 때 구현합니다.
      • Jackson이 json을 deserialize해서 객체로 만들기 위해서는 객체를 생성하고 필드를 채울 수 있어야 합니다.
      • 객체를 생성하는 것은 기본 생성자를 사용하고, 필드를 채우는 것은 필드가 pulbic 이면 직접할당하고, Private 이면 setter를 사용합니다. → 하지만 객체를 캡슐화하는 것은 기본 권장 사항이기 때문에 무조건 setter를 사용한다고 보면 됩니다.
    • 즉, 생성자 + setter = @JsonCreator 라고 볼 수 있습니다.
    • 해당 어노테이션은 생성자나 팩토리 메소드 위에 붙이면 jackson이 해당 함수를 통해 객체를 생성하고 필드를 생성과 동시에 채웁니다. (setter 함수가 필요없어지게 됩니다.)
@Getter
public enum Fruit {
    APPLE("사과"),
    BANANA("바나나"),
    GRAPE("포도"),
    ORANGE("오렌지");
    
    private final String name;

	Fruit(String name) {
        this.name = name;
    }

    private static Map<String, Status> FORMAT_MAP = Stream    // (1)
    	.of(Fruit.values())
    	.collect(Collectors.toMap(s -> s.name, Function.identity()));

    @JsonCreator // (2)
    public static Fruit fromString(String fruitKeyword) {
        return Optional
            .ofNullable(FORMAT_MAP.get(fruitKeyword))
            .orElseThrow(() -> new IllegalArgumentException(fruitKeyword));
    }
}

(1) enum의 상수값의 속성과 request 값과 일치하는 것이 있을 때 Map을 만듭니다.

private static Map<String, Status> FORMAT_MAP = Stream
        .of(Status.values())
        .collect(Collectors.toMap(s -> s.formatted, Function.identity()));
  • Stream.of()로 Stream 을 생성합니다.
  • values()는 enum의 요소(속성)들을 순서대로 Enum 타입의 배열로 리턴합니다.
  • 값이 실제 요소여야 하는 일반적인 경우에 사용합니다. (Enum의 상수값을 Return 합니다.)

(2) FORMAT_MAP에서의 Key인 enum의 상수에 대한 속성값을 기준으로 value인 enum의 상수값을 return 합니다.

@JsonCreator // (2) 
public static Status fromString(String string) {
    return Optional
        .ofNullable(FORMAT_MAP.get(string))
        .orElseThrow(() -> new IllegalArgumentException(string));
}
  • Optional.ofnullable() : nul인지 아닌지 확실할 수 없는 객체를 담고 있는 Optional 객체를 생성하며, null 이 넘어올 경우 NPE를 던지지 않고 Optional.empty()와 동일하게 비어있는 Optional 객체를 얻어옵니다. (해당 객체가 Null인지 아닌지 확신할 수 없는 상황에서 사용합니다.)
  • Map.get(key) : Map 안에 있는 key에 대한 value을 가져옵니다.

 

참고로 Spring Boot에서 Jackson 버전을 확인하기 위해서는 아래를 참고해주세요.

💡 spring project 안에 External Libraries > `com.fasterxml.jackson.core`:jackson-core 버전을 확인하면 됩니다.
저는 spring boot 2.6.2를 사용하고 있어서 Jackson 버전이 2.13.1인 것으로 확인이 되었습니다.
(spring boot에서는 기본적으로 Jackson이 내장되어있습니다.)

 

References

https://stackoverflow.com/questions/31689107/deserializing-an-enum-with-jackson

https://medium.com/@lifecluee/string%EC%9D%84-%EA%B3%A0%EC%A7%91%ED%95%98%EB%8A%94-controller-%EA%B0%9C%EC%84%A0%EA%B8%B0-721cba570756

https://velog.io/@recordsbeat/JacksonObjectMapper%EB%A1%9C-Enum%EC%BB%A8%ED%8A%B8%EB%A1%A4-%ED%95%98%EA%B8%B0

https://kimsup10.wordpress.com/2019/04/02/jsoncreator%EB%8A%94-%EC%99%9C-%EC%93%B0%EB%8A%94%EA%B1%B8%EA%B9%8C/

https://kwonnam.pe.kr/wiki/java/jackson

https://javanitto.tistory.com/m/43

https://old-developer.tistory.com/m/105

반응형