본문 바로가기
Develop Study/Spring 2025. 3. 13.

(25.03.13) Java Spring JPA @Converter & AttributeConverter

Labeling Project를 진행하면서 JSON  형태의 데이터를 계속다루고 있고,

좀더 유연한 자료의 저장을 위해서 MySQL 에서 활용하지만, 다양한 Entity에서 String 타입 또는 Json 타입으로 저장하기 위해서

Service 단에서 같은 ObjectMapper로 변환하는 비효율적인 코드를 작성했다. 하지만,

Converter를 사용하면서 좀더 효율적으로 사용할 것으로 판단했고

Row Data 당 사용자가 활용한 Label 리스트 타입을 Json 타입으로 저장하는데 활용할 수 있었다.


 

@Converter

@Entity
@Table(name = "samples")
public class Sample extends Timestamp {
		...
    @Column(columnDefinition = "JSON")
    @Convert(converter = ListToJsonConverter.class)
    private List<String> labels;
    ...
  • Java Spring JPA 에서 Entity 필드의 타입과 DB 에서의 저장 타입의 변환을 수행하기 위해서 사용
    • 위에서는 List<String> 타입을 Json 형태로 저장하기 위해서 사용
  • AttributeConverter<X, Y> 인터페이스를 구현한 클래스를 미리 지정해서 사용할 수 있음
    • 위에서는 ListToJsonConverter 클래스가 구현을 하고 있음
  • 타입을 변환한다는 목적을 가지고 있기 때문에, 어떤 특정 템플릿을 적용하기 위한 형식을 변환하는데는 적합하지 않음
    • 해당 목적으로는 Service 에서 작업을 할 수 있도록 해야함

AttributeConverter

// List <-> Json 간 컨버터
@Converter // @Converter Annotation으로 지정
public class ListToJsonConverter implements AttributeConverter<List<String>, String> {

    private static final ObjectMapper objectMapper = new ObjectMapper();
		
    @Override
    public String convertToDatabaseColumn(List<String> attribute) {
        try {
            return objectMapper.writeValueAsString(attribute);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("리스트를 JSON으로 변환하는데 실패", e);
        }
    }

    @Override
    public List<String> convertToEntityAttribute(String dbData) {
        try {
            return objectMapper.readValue(dbData, List.class);
        } catch (IOException e) {
            throw new IllegalArgumentException("JSON을 리스트로 변환하는데 실패", e);
        }
    }
}

  • JPA Entity 필드 값 → DB 컬럼 값으로 변환하는 기능을 제공하는 인터페이스
    • List<String>, Map<String, Object>, Enum 등등 JPA에서 그냥 타입 자체가 지원이 안되는 경우에 사용
      • LocalDateTime은 지원하지 않으나, @EnableJpaAuditing 이 @SpringBootApplication 클래스에 지정이 되어있다면 사용을 할 필요가 없을 수 있음
      • @Enumerated(EnumType.STRING) 역시 String으로 자동변환을 담당하기 때문에 굳이 사용할 필요가 없을 수 있음 , @Converter는 어떤 옵션을 추가하거나 추가 작업이 있을 경우에 사용을 할 수 있음
    • 따라서, 경우에 따라 각각의 구현 클래스, Converter 클래스를 미리 만들어야함
  • General 로 어떤 값 끼리 변환할 것인지 명시
  • X → Y 변환(convertToDatabaseColumn)과 Y → X 변환(convertToEntityAttribute) 을 정의해서 JPA 가 처리할 수 있도록 함
    • @Override를 통해서 지정
    • DB에 save 전 / Read 후 자동으로 변환이 작동

기능

  • 자동 변환
    • JPA 가 자동으로 DB로 부터 데이터를 저장 & Read(조회, Retrieve) 할 때마다 자동으로 변환
  • 효율적인 사용
    • 미리 Converter클래스가 있다면 필요에 따라 여러 Entity / 여러 컬럼에 적용이 가능
  • 불필요 로직 제거
    • Service 에서 따로 불필요한 변환 로직을 길게 작성하지 않아도 알아서 DB와 매핑이 가능

예) Map ↔ JSON 간 Converter

@Converter
public class MapToJsonConverter implements AttributeConverter<Map<String, Object>, String> {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Map<String, Object> attribute) {
        if (attribute == null) {
            return null;
        }
        try {
            return objectMapper.writeValueAsString(attribute);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Error converting Map to JSON", e);
        }
    }

    @Override
    public Map<String, Object> convertToEntityAttribute(String dbData) {
        if (dbData == null) {
            return null;
        }
        try {
            return objectMapper.readValue(dbData, Map.class);
        } catch (IOException e) {
            throw new IllegalArgumentException("Error converting JSON to Map", e);
        }
    }
}

  • 위 처럼 만든다면 여러개의 필드를 Service 단에서 만든다음에 JasonObject 로 만들고 toString() 으로 변환해서 넣지 않아도 Map 자체를 save 하기만 하면 됨

심화) Complex Object ↔ JSON 변환

  • 객체 (클래스) 가 여러개의 필드를 가지고 있을 경우, 또는 다른 객체를 포함한 Complex Object ↔ JSON 변환
public class Company {
    private String companyName;
    private List<Employee> employees;

    ...
}

public class Employee {
    private String employeeName;
    private Address address;

    ...
}

public class Address {
    private String street;
    private String city;

    ...
}

  • 위의 Company와 같은 Complex Objext 를 JSON 으로 DB에 넣을 수 있음
@Converter
public class CompanyToJsonConverter implements AttributeConverter<Company, String> {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Company attribute) {
        try {
            return objectMapper.writeValueAsString(attribute);
        } catch (Exception e) {
            throw new IllegalArgumentException("Error converting Company to JSON", e);
        }
    }

    @Override
    public Company convertToEntityAttribute(String dbData) {
        try {
            return objectMapper.readValue(dbData, Company.class); 
        } catch (Exception e) {
            throw new IllegalArgumentException("Error converting JSON to Company", e);
        }
    }
}

  • ObjectMapper로 인해 자연스럽게 만들 수 있음
{
  "companyName": "TechCorp",
  "employees": [
    {
      "employeeName": "John Doe",
      "address": {
        "street": "123 Main St",
        "city": "Anytown"
      }
    },
    {
      "employeeName": "Jane Smith",
      "address": {
        "street": "456 Oak St",
        "city": "Othertown"
      }
    }
  ]
}
  • 위의 JSON 자체가 컬럼의 어트리뷰트로 저장
@Entity
public class CompanyEntity {

    @Id
    private Long id;

    @Convert(converter = NestedComplexObjectConverter.class)
    @Column(columnDefinition = "LONGTEXT")
    private Company company;

    ...
}

  • @Column(columnDefinition = "JSON") 으로 지정을 하면 MySQL이 알아서 JSON 타입으로 저장

Spring Data Jpa 와 MySQL 자료 타입을 변환하는 것은 어느정도 한계가 있다.

따라서 필요에 따라서 Converter를 활용해서 비효율적인 Service 단의 코드가 늘어지는 걸 방지할 수 있을 것이다.