[헤이동동 #04] Json 필드 파싱

Jiwoo Kim·2020년 12월 2일
0
post-thumbnail

☕헤이동동 : 생협 음료 원격 주문 서비스

이번 포스팅에서는 AttributeConverterjackson 라이브러리를 사용해 엔티티의 Json String을 값을 객체로 파싱한 과정에 대해 설명한다.


개요

필요성

사용자가 주문한 menus_in_orders 테이블에는 custom_options라는 필드가 있다.

이 필드는 json string (varchar) 형태 그대로 DB에 저장되기 때문에 엔티티와 매핑하기 위해서는 별도의 작업이 필요하다. 처음 해보는 작업이기 때문에 구글링으로 방법을 알아 보았고, 최종적으로 AttributeConverter 인터페이스를 활용하여 구현에 성공하였다.

AttributeConverter?

자바 공식 레퍼런스 문서에 따르면 AttributeConverter 인터페이스는 다음과 같다.

javax.persistence
Interface AttributeConverter<X,Y>

Type Parameters:

  • X - the type of the entity attribute
  • Y - the type of the database column

    public interface AttributeConverter<X,Y>
    : A class that implements this interface can be used to convert entity attribute state into database column representation and back again. Note that the X and Y types may be the same Java type.
Modifier and TypeMethod and Description
YconvertToDatabaseColumn(X attribute): Converts the value stored in the entity attribute into the data representation to be stored in the database.
XconvertToEntityAttribute(Y dbData): Converts the data stored in the database column into the value to be stored in the entity attribute.

정리하자면, Y 타입의 DB 컬럼과 X라는 엔티티(클래스) 간 상호 변환 기능을 제공한다.
두 개의 메소드를 오버라이딩해서 원하는 대로 구현을 할 수 있다.


구현

엔티티 정의

  1. Json string에 담긴 정보가 각각 필드로 매핑될 수 있도록 엔티티와 필드를 생성해준다.
    이 때, Json의 key 이름이 엔티티의 필드 이름과 일치해야 한다. 그래야만 converter가 자동으로 인식하여 매핑을 성공적으로 할 수 있다.

  2. 엔티티의 멤버 변수 중 매핑 객체에 @Convert를 붙여 구현한 converter가 작동할 수 있도록 명시한다.

CustomOption

{"shotAmericano": 1, "vanilla": true}와 같은 json string을 CustomOption 객체로 변환하기 위해 모든 옵션을 필드로 넣었다.

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomOption {

    private Integer shotAmericano;
    private Integer shotLatte;
    private Boolean milk;
    private Boolean mint;
    private Boolean condensedMilk;
    private Boolean chocolate;
    private Boolean caramel;
    private Boolean vanilla;
    private Boolean soyMilk;
}

Option

Option 엔티티는 CustomOption 객체를 멤버 변수로 갖는다. 그 객체에 @Convert를 붙여 구현한 CustomOptionConverter가 작동하도록 하였다.

따라서 Option 엔티티를 JPA가 로드할 때 custom_options 컬럼은 자동으로 CustomOptionConverter에게 넘겨지고, 그 결과로 CustomOption 객체가 생성된다.

@Getter
@NoArgsConstructor
@Embeddable
@ToString
public class Option {

    @Embedded
    private BasicOption basicOption;

    @Column(name = "custom_options")
    @Convert(converter = CustomOptionConverter.class)
    private CustomOption customOption;

    @Builder
    public Option(BasicOption basicOption, CustomOption customOption) {
        Assert.notNull(basicOption, "BasicOption must not be null");

        this.basicOption = basicOption;
        this.customOption = customOption;
    }
}

CustomOptionConverter 구현

AttributeConverter 인터페이스를 구현한 CustomOptionConverter 클래스 전체 코드는 아래와 같다.

public class CustomOptionConverter implements AttributeConverter<CustomOption, String> {

    private static final ObjectMapper objectMapper = new ObjectMapper()
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);

    @Override
    public String convertToDatabaseColumn(CustomOption customOption) {
        if (customOption == null)
            return null;

        try {
            return objectMapper.writeValueAsString(customOption);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public CustomOption convertToEntityAttribute(String jsonStr) {
        if (jsonStr == null)
            return null;
        if (jsonStr.isEmpty())
            return null;

        try {
            return objectMapper.readValue(jsonStr, CustomOption.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

앞서 살펴 보았던 대로, 메소드 두 개를 오버라이딩하여 CustomOption 객체로 변환할 수 있도록 구현하였다. 대부분의 코드는 읽는 대로 직관적으로 이해할 수 있지만, ObjectMapper에 대한 설명이 필요할 것 같아서 설명을 추가했다.

ObjectMapper

여기서 objectMapper는 json과 객체 간 변환을 실제로 실행하는 객체다. converter파싱 단계를 적용하기 위한 인터페이스였고, 실제 변환별도의 라이브러리를 활용해야 하는 것이다. 헤이동동은 jackson 라이브러리를 사용하여 ObjectMapper를 통해 변환 작업을 정의했다.

Gson이나 다른 json 라이브러리를 사용할 수도 있지만, 참고한 블로그에 따르면 스프링 부트 안에서의 json 파싱은 jackson을 사용하는 것이 더 편리하다고 한다. 근거가 충분하지 않았던지라 이 말을 100% 신뢰하는 것은 아니지만, 우선 Gson을 사용했던 경험이 있었기 때문에 새로운 라이브러리를 사용해보고자 jackson을 사용하였다.

참고로, objectMappersetSerializationInclusion(JsonInclude.Include.NON_NULL) 옵션을 적용하지 않으면, 매핑 시 빈 필드도 NULL이라는 값을 가지도록 작동한다. 즉, {"fieldA": "value", "fieldB": "value"}가 아니라, {"fieldA: "value", "fieldB": "value", "fieldC": NULL}과 같이 저장이 된다는 것이다. 이를 피하기 위해서는 NON_NULL 옵션을 적용해야 한다.


전체 코드는 Github에서 확인하실 수 있습니다.

0개의 댓글