이 글은 JPA에서 JSON컬럼을 매핑하는 방법을 소개합니다.
Spring boot 2.7.6 환경에서 작성된 글이며 다음 라이브러리가 필요합니다. spring-boot-starter-data-jpa
, spring-boot-starter-json
(보통 web에 포함되어 있다.)
엔티티에 컬럼을 매칭할 때 JSON컬럼은 단순하게 매핑할 수가 없다. JPA
는 기본값 타입에 대한 매핑만 지원을 하며 그외의 타입은 @Converter
를 제공하여 매핑할 수 있게한다.
@Converter
에 대한 상세설명은 공식문서, baeldung 예시에서 찾아볼 수 있다.
@Converter
가 무슨 기능인지 파악을 했다면 이제 JSON컬럼을 객체로 변환해보자.
우리는 다음과 같은 테이블을 엔티티로 변환해야한다.
CREATE TABLE product (
product_id int,
product_property json,
product_prices json -- JSON 배열
)
우선 AttributeConverter
을 상속하여 Converter
객체를 작성해주어야한다.
@Converter
public class JsonConverter<T> implements AttributeConverter<T, String> { // 1
protected final ObjectMapper objectMapper;
public JsonConverter() {
objectMapper = new ObjectMapper(); // 2
}
@Override
public String convertToDatabaseColumn(T attribute) {
if (ObjectUtils.isEmpty(attribute)) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute); // 3
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public T convertToEntityAttribute(String dbData) {
if (StringUtils.hasText(dbData)) {
Class<?> aClass =
GenericTypeResolver.resolveTypeArgument(getClass(), JsonConverter.class); // 4
try {
return (T) objectMapper.readValue(dbData, aClass); // 5
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return null;
}
}
Converter
사용방식이다. AttributeConverter<X,Y>
의 제네릭 X는 엔티티 속성으로 변환될 타입, 제네릭 Y는 컬럼 데이터 타입으로 변환될 타입이다. 여기서 X는 상속 시 정의될 속성의 타입 제네릭으로 선언하고, Y는 String
으로 선언했다.ObjectMapper()
다른 방식으로 생성하거나, 다른 라이브러리를 사용해도 좋다.writeValueAsString
을 사용해 변환해주었다.GenericTypeResolver.resolveTypeArgument
을 사용하였다. 다른 방법이 있다면 그 방법을 사용하면된다. 목적은 엔티티 속성의 타입을 얻는 것이다.readValue
를 사용하여 문자열을 엔티티 속성 타입으로 변환해주었다.이제 이렇게 정의한 Converter
클래스는 다음과 같이 사용할 수 있다.
public class ProductProperty {
private LocalTime sellingStartTime;
private LocalTime sellingEndTime;
private Integer stock;
// getters and setters
// override equals and hashcode
public static class ProductPropertyConverter extends JsonConverter<ProductProperty> {} // 1
}
엔티티 속성에서 사용될 클래스를 정의해준다.
1. Converter
클래스 선언을 해주었다. JsonConverter<T>
를 상속하였고 제네릭에 매핑할 클래스인 ProductProperty
를 선언했다.
⚠️ 주의
꼭equals
와hashcode
를 재정의해주어야 한다. JPA에서 변경감지 시 영속성 컨텍스트에 있는 엔티티와 수정한 엔티티를 비교할 때 해당 속성 객체의equals
,hashcode
를 사용하여 동등성을 비교하는데 재정의하지 않을 시 두 엔티티의 인스턴스가 다르므로 무조건update
쿼리가 실행된다. 이를 방지하기 위해 꼭equals
와hashcode
를 재정의하자.
엔티티 내에선 다음과 같이 작성한다.
@Entity
public class Product {
@Id @GeneratedValue
private Long productId;
@Convert(converter = ProductProperty.ProductPropertyConverter.class)
private ProductProperty productProperty;
...
}
대부분 JSON 객체안에 배열을 선언하여 잘 사용될 일은 없지만 그래도 사용방법을 소개해보도록 한다.
객체때와 마찬가지로 AttributeConverter
을 상속하여 Converter
객체를 작성한다.
@Converter
public class JsonArrayConverter<T> implements AttributeConverter<List<T>, String> { // 1
private final TypeReference<List<T>> typeReference; // 2
public JsonArrayConverter(TypeReference<List<T>> typeReference) {
this.typeReference = typeReference;
}
@Override
public String convertToDatabaseColumn(List<T> attribute) {
if (attribute == null || attribute.isEmpty()) {
return null;
}
return objectMapper.writeValueAsString(attribute);
}
@Override
public List<T> convertToEntityAttribute(String dbData) {
if (StringUtils.hasText(dbData)) {
return objectMapper.readValue(dbData, typeReference);
}
return null;
}
}
List
를 사용하였다.jackson
라이브러리의 Typereference
변수를 JsonArrayConverter
생성 시 초기화 해주어야 한다.이제 이렇게 정의한 Converter
클래스는 다음과 같이 사용할 수 있다.
public class ProductPrices {
private Long price;
// getters and setters
// override equals and hashcode
public static class ProductPricesConverter extends JsonArrayConverter<ProductPrices> {
public ProductPricesConverter() {
super(new TypeReference<>() {}); // 1
}
}
}
엔티티 속성에서 사용될 클래스를 정의해준다.
1. 현재 클래스의 TypeReference
를 생성하여 상위 클래스에 넘겨준다.
엔티티 내에선 다음과 같이 작성한다.
@Entity
public class Product {
@Id @GeneratedValue
private Long productId;
@Convert(converter = ProductProperty.ProductPropertyConverter.class)
private ProductProperty productProperty;
@Convert(converter = ProductPrices.ProductPricesConverter.class)
private List<ProductPrices> productPrices;
...
}
Converter
를 사용하면 같은 데이터 타입이더라도 다양한 객체로 매핑할 수 있습니다.
예시의 전체 소스코드는 깃헙에서 확인할 수 있습니다.