Spring Boot에서 JSON 데이터 타입에 AttributeConverter 활용하기

2025년 04월 19일

spring

# SpringBoot# JPA# Hibernate# JSON# AttributeConverter

📕 목차

AttributeConverter란?

Java Persistence API (JPA)에서 AttributeConverter는 엔티티 속성과 데이터베이스 컬럼 간의 변환을 정의하는 인터페이스입니다.

이 기능은 엔티티 속성을 데이터베이스에 저장하거나, 데이터베이스로부터 엔티티로 데이터를 로드할때 어떠한 방식으로 해석해야 하는지를 정의합니다.

저는 주로 복잡한 Enum 클래스 혹은 JSON 데이터를 다룰 때, 이 기능을 사용합니다.

AttributeConverter 인터페이스 살펴보기

이 인터페이스는 두 개의 타입 파라미터를 가집니다.

  • X: 엔티티 속성의 타입 (예: Map<String, Object>)
  • Y: 데이터베이스 컬럼의 타입 (예: String)
public interface AttributeConverter<X, Y> {
    Y convertToDatabaseColumn(X attribute);
    X convertToEntityAttribute(Y dbData);
}

스펙을 살펴보면 다음과 같은 메서드를 구현해야합니다.

  1. 데이터베이스 컬럼 타입(Y)을 -> 엔티티 속성타입(X)로 변환하는 convertToEntityAttribute 메서드
  2. 엔티티 속성타입(X) -> Y로 변환하는 convertToDatabaseColumn 메서드를 구현해야 합니다.

컨버터(AttributeConverter) 사용사례

Case 1. 복잡한 Enum 클래스

우리는 DB 필드에 지정된 값만 저장되도록 강제할 때 Enum 타입을 사용합니다. 보통의 Enum은 다음과 같이 정의합니다

enum class OrderStatus {
    PENDING, SHIPPED, DELIVERED, CANCELED
}

이 경우에는 보통 @Enumerated(EnumType.STRING)을 사용하여 JPA Entity 컬럼에 Enum을 매핑합니다.

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String customerName;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}

하지만 Enum이 복잡한 구조를 가질 때는 Enum의 어떤 속성으로 값을 저장하고 불러올 지 알 수 없습니다. 그래서, 우리는 AttributeConverter를 사용하여 Enum의 code와 같은 특정 필드의 값으로 저장하고 DB로부터 읽은 값을 Enum으로 변환할 수 있습니다.

예를 들어, OrderStatus가 다음과 같이 정의되어 있다고 가정해봅시다

enum class OrderStatus(val code: String, val description: String) {
    PENDING("P", "주문 대기"),
    SHIPPED("S", "배송 중"),
    DELIVERED("D", "배송 완료"),
    CANCELED("C", "주문 취소");

    companion object {
        fun fromCode(code: String): OrderStatus? {
            return values().find { it.code == code }
        }
    }
}

이 경우, OrderStatus의 code를 DB에 저장하고, 이를 다시 Enum으로 변환하는 AttributeConverter를 구현할 수 있습니다

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;

@Converter(autoApply = true)
public class OrderStatusConverter: AttributeConverter<OrderStatus, String> {

    @Override
    public String convertToDatabaseColumn(OrderStatus attribute) {
        if (attribute == null) return null
        return attribute.code
    }

    @Override
    public OrderStatus convertToEntityAttribute(String dbData) {
        if (dbData == null) return null
        return OrderStatus.fromCode(dbData)
    }
}

Case 2. 구체 클래스에 JSON 데이터 매핑

이번에는 JSON 데이터를 구체 클래스에 매핑해서 타입을 사용해서 데이터를 처리하는 방법을 살펴보겠습니다. 다음과 같은 JSON 데이터가 있다고 가정해봅시다.

{
  "baseAddress": "서울시 강남구 역삼동",
  "detailAddress": "1234호",
  "zipCode": "123-456",
  "phoneNumber": "010-1234-5678"
}

우리는 이런 데이터는 Map<String, Object> 형태로도 사용할 수 있지만, 구체적인 클래스를 만들어서 타입 안전성을 높일 수 있습니다.

public class Address {
    private String baseAddress;
    private String detailAddress;
    private String zipCode;
    private String phoneNumber;
}

Address라는 클래스를 DB에 저장하기 위해서는 JSON 문자열로 변환해야하고, DB에서 읽어올 때는 JSON 문자열을 Address 객체로 변환해야 합니다. 이때 ObjectMapper를 사용해서 처리하는 AttributeConverter를 구현해서 처리하면 됩니다.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;


@Converter(autoApply = true)
class AddressConverter : AttributeConverter<Address, String> {

    private val objectMapper = ObjectMapper().apply {
        registerModule(KotlinModule().Builder().build())
    }

    @Override
    public String convertToDatabaseColumn(Address attribute) {
        if (attribute == null) return null
        return objectMapper.writeValueAsString(attribute)
    }

    @Override
    public Address convertToEntityAttribute(String dbData) {
        if (dbData.isNullOrBlank()) return Address(
            baseAddress = "",
            detailAddress = "",
            zipCode = "",
            phoneNumber = ""
        )

        try {
            // JSON 문자열을 Address 객체로 변환
            return objectMapper.readValue<Address>(dbData)
        } catch (e: Exception) {
            throw RuntimeException("Failed to convert JSON to Address", e)
        }
    }
}

이제 AddressConverter를 사용하여 Address 객체를 DB에 저장하고, DB에서 읽어올 수 있습니다.

@Entity
@Table(name = "orders")
class Order(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    // Converter 지정
    @Convert(converter = AddressConverter::class)
    @Column(name = "address", columnDefinition = "json")
    val address: Address
)

참고 자료

© 2025, 미나리와 함께 만들었음