[2024.04.09] MySQL 데이터로 List 넣기(AttributeConverter)

아스라이지새는달·2024년 6월 19일
2

화반 프로젝트

목록 보기
1/5
post-thumbnail

굉장히 오랜만에 올리는 포스트이다. 3월 말서부터 새로운 프로젝트를 시작해 약 2달간 진행하였고 며칠 전 완전히 종료되었다. 한동안은 해당 프로젝트를 하면서 알게 되고 해결한 것들에 대해 포스팅 해보려 한다.
우선 오늘 포스팅할 내용은 프로젝트 초반 DB설계를 할 때 겪었던 일로 MySQL 데이터로 List를 넣고 싶어 구현했던 방법에 관한 내용이다.

🏞️ 구현 배경

위의 ERD와 같이 recipe_file 테이블은 recipe 테이블의 primary key인 recipe_no를 foreign key로 갖는다. 원래 기획대로라면 recipe 테이블과 recipe_file 테이블은 One to Many의 관계를 가지게 되는데 추후 사진 부분을 구현할 때 번거로움이 있을 거 같아 recipe_file_oname 컬럼과 recipe_file_sname 컬럼을 List로 관리하는 건 어떨까? 라고 생각하였다. 즉 이렇게 되면 One to Many가 아닌 One to One 관계를 갖게 된다. 다만 문제가 하나 있는데 MySQL에서는 List에 맞는 데이터 타입이 없다는 것이다. 이를 해결한 방법은 아래에서 소개하도록 하겠다.


🔨 해결 방법

해결 방법으로 사용한 것은 AttributeConverter, @Converter, @Convert이다.

AttributeConveter

AttributeConverter는 인터페이스로 엔티티의 값을 원하는 형태로 DB에 저장하거나 DB에 저장된 값을 원하는 형태로 엔티티 필드에 바인딩할 때 사용한다.

package jakarta.persistence;

public interface AttributeConverter<X, Y> {
    Y convertToDatabaseColumn(X var1);

    X convertToEntityAttribute(Y var1);
}

AttributeConveter에는 두 가지 메서드가 있다.

  • convertToDatabaseColumn : Entity의 값을 DB에 저장할 때 사용
  • convertToEntityAttribute : DB에 저장된 값을 Entity 필드에 바인딩할 때 사용

@Converter

이 AttributeConverter를 구현하는 구현체를 만들고 위의 두 메소드를 Override하면 된다. 또한 이 구현체에 @Converter 어노테이션을 달아주면 Entity의 값과 DB의 값을 원하는 형태로 변환할 수 있다.

아래 코드는 프로젝트에서 사용한 코드이다.

// common/StringListConverter.java

@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {

    private final ObjectMapper mapper = new ObjectMapper();


    @Override
    public String convertToDatabaseColumn(List<String> entityData) {
        try {
            return mapper.writeValueAsString(entityData);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<String> convertToEntityAttribute(String dbData) {
        try {
            return mapper.readValue(dbData, List.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

DB에 값을 저장할 때는 convertToDatabaseColumn 메소드를 오버라이드하여 ObjectMapper의 writeValueAsString를 통해 Java 객체를 String으로 직렬화(Serialization) 하였고
DB에서 값을 읽어올 때는 convertToEntityAttribute 메소드를 오버라이드하여 ObjectMapper의 readValue를 통해 String을 Java 객체로 역직렬화(Deserialization) 하였다.

@Convert

이 Custom Converter를 적용할 엔티티의 필드에 @Convert 어노테이션을 이용해 적용시켜주면 된다.
아래 코드와 같이 @Convert 어노테이션을 적어주고 사용할 converter를 명시해주면 된다.

필자는 recipe_file_oname 컬럼과 recipe_file_sname 컬럼뿐만 아니라 레시피 재료(recipe_stuff) 컬럼 또한 엔티티에서는 List<String> 타입으로 사용하고, DB에는 String 타입으로 저장하고 싶어 해당 컬럼에도 적용하였다.

// domain/Recipe.java

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Recipe {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "recipe_no")
    private Long recipeNo;

	.
    .
    .

    @Column(name = "recipe_stuff")
    @Convert(converter = StringListConverter.class)
    private List<String> recipeStuff;

    @ManyToOne
    @JoinColumn(name = "user_no")
    private User user;

    @OneToMany(mappedBy = "recipe", cascade = CascadeType.REMOVE)
    private List<RecipeFile> recipeFiles;

    .
    .
    .
    
}
//RecipeFile.java

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RecipeFile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "recipe_file_no")
    private Long recipeFileNo;

    @Column(name = "recipe_file_oname", columnDefinition = "TEXT")
    @Convert(converter = StringListConverter.class)
    private List<String> recipeFileOname;

    @Column(name = "recipe_file_sname", columnDefinition = "TEXT")
    @Convert(converter = StringListConverter.class)
    private List<String> recipeFileSname;

    @ManyToOne
    @JoinColumn(name = "recipe_no")
    private Recipe recipe;

}

🧪 테스트

Postman을 통해 데이터를 넣어보고 조회하여 원하는대로 변환이 이루어지는지 확인해본다.

데이터 삽입(올바른 요청)

recipe_stuff, recipe_file_oname, recipe_file_sname에 List<String> 형태로 된 데이터를 작성하여 넣어준다.

DB를 확인해보면 다음과 같이 String으로 변환되어 recipe_stuff는 varchar(255) 타입으로, recipe_file_oname과 recipe_file_sname은 Entity 필드에서 지정한 TEXT 타입으로 데이터가 들어간 것을 볼 수 있다.

데이터 삽입(잘못된 요청)

이번엔 recipe_stuff에 List<String> 형태가 아닌 String 형태로 데이터를 넣어본다.

에러가 발생한 것을 볼 수 있고 IntelliJ를 확인해보면 다음과 같은 에러 메시지가 찍힌 것을 볼 수 있다.

Exception [Err_Msg]: JSON parse error: Cannot construct instance of `java.util.ArrayList` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('당근')
Exception [Err_Where]: org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406)
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `java.util.ArrayList` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('당근')]

데이터 조회

위에서 생성한 레시피를 조회하면 어떻게 조회되는지도 확인해본다.

List 형태로 조회되는 것을 확인할 수 있다.


🔍 Reference

https://khdscor.tistory.com/39

https://velog.io/@juice/SpringBoot-%EC%97%94%ED%8B%B0%ED%8B%B0%EC%97%90-MapStringListString%ED%83%80%EC%9E%85-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0

https://rachel0115.tistory.com/entry/JPA-JPA-AttributeConverter%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EA%B0%92%EC%9D%84-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0

https://effortguy.tistory.com/483

https://velog.io/@dnrwhddk1/Spring-Converter-%EC%82%AC%EC%9A%A9

https://velog.io/@best1370/SpringBoot%EC%9D%98-Converter

https://velog.io/@zooneon/Java-ObjectMapper%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-JSON-%ED%8C%8C%EC%8B%B1%ED%95%98%EA%B8%B0#json-%E2%86%92-java-object

profile
웹 백엔드 개발자가 되는 그날까지

0개의 댓글