이번 포스팅에서는 AttributeConverter
와 jackson
라이브러리를 사용해 엔티티의 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 Type | Method and Description |
---|---|
Y | convertToDatabaseColumn(X attribute): Converts the value stored in the entity attribute into the data representation to be stored in the database. |
X | convertToEntityAttribute(Y dbData): Converts the data stored in the database column into the value to be stored in the entity attribute. |
정리하자면, Y 타입의 DB 컬럼과 X라는 엔티티(클래스) 간 상호 변환 기능을 제공한다.
두 개의 메소드를 오버라이딩해서 원하는 대로 구현을 할 수 있다.
Json string에 담긴 정보가 각각 필드로 매핑될 수 있도록 엔티티와 필드를 생성해준다.
이 때, Json의 key 이름이 엔티티의 필드 이름과 일치해야 한다. 그래야만 converter가 자동으로 인식하여 매핑을 성공적으로 할 수 있다.
엔티티의 멤버 변수 중 매핑 객체에 @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
을 사용하였다.
참고로, objectMapper
에 setSerializationInclusion(JsonInclude.Include.NON_NULL)
옵션을 적용하지 않으면, 매핑 시 빈 필드도 NULL이라는 값을 가지도록 작동한다. 즉, {"fieldA": "value", "fieldB": "value"}
가 아니라, {"fieldA: "value", "fieldB": "value", "fieldC": NULL}
과 같이 저장이 된다는 것이다. 이를 피하기 위해서는 NON_NULL
옵션을 적용해야 한다.