[JPA] Trouble Shooting : @CollectionTable & @ElementCollection 멸망기

Ceing·2024년 11월 21일

JPA

목록 보기
8/15
post-thumbnail

개요 : @CollectionTable , @ElementCollection 도입한 계기

  • 현재 진행하는 웹 프로젝트에서 각 분류에 따른 체크박스를 구현하고자 함

  • 한 식당이 각 분류에 따른 여러 데이터들을 가지므로 일대다 관계를 생각함

  • @OneToMany & @ManyToOne으로 엔티티로서 각 분류에 대한 테이블을 운영해도 되지만, 테이블이 많아질 것을 고려하고, 해당 데이터들은 변경될 일이 없는 단순 문자열 데이터들이기 때문에 @CollectionTable@ElementCollection을 활용하고자 함


엔티티 설계


@Entity
@Getter @Setter(AccessLevel.PRIVATE)
public class Restaurant{

	@Id @GeneratedValue
    private Long id;
    
    //생략
    
	@ElementCollection
	@CollectionTable(name="contain_food_type" , joinColumns = @JoinColumn(name="restaurant_id"))
    private Set<String> containFoodTypes = new HashSet<>();

	//생략
}
  • 컬렉션은 위 사진 분류 중 포함 음식에 해당됨
  • 체크박스로부터 체크된 데이터들이 해당 컬렉션에 바인딩되도록 할 것
  • 컬렉션을 흔히 사용하는 List가 아닌 Set인 이유에 대해선 바로 뒤에서 설명할 것임

※ @CollectionTable 어노테이션 설명

  • name : DB와 매핑될 테이블 이름
  • joinColumns: 매핑할 외래키 이름 , JPA는 지정된 외래 키가 주인 엔티티(Restaurant)의 PK 따르게 해줌

@ElementCollection & @CollectionTable 통해 매핑되는 테이블 및 칼럼 설명

그림 설명

  • JPA는 containFoodTypes 컬렉션에 들어오는 각각의 값을 따로 지정한 이름의 테이블의 칼럼에 매핑하여줌
  • 복합 키 중 RESTAURANT_ID는 PK이자 FK로 식당과 연쇄적으로 동작됨
  • 즉 모든 칼럼이 복합 키가 되어 동작되니 모든 칼럼이 중복되면 안됨
  • 예를 들어 {1 , "생선포함"} , {1 , "생선포함"} 이렇게 데이터가 들어오면 PRIMARY KEY의 UNIQUE 제약 조건에 위배되므로 중복된 데이터가 들어오지 않도록 해줘야됨

spring.jpa.hibernate.ddl-auto=create에 따른 자동 생성 DDL

create table contain_food_type(
	restaurant_id bigint not null,
    contain_food_types varchar(255),  
);
  • 기본적으로 application.properties에서 spring.jpa.hibernate.ddl-auto=create를 써서 자동으로 위와 같은 DDL 생성해줌

  • 하지만 운영 시에는 해당 설정이 모든 데이터를 지우고 새로 테이블을 생성해서 굉장히 위험하므로 해당 설정을 끄고 개발자가 직접 DDL을 작성해줘야함

  • 따라서 개발자는 위의 형식과 일치하는 DDL로 테이블을 생성해줘야됨


★ 개발자가 직접 작성해줘야할 DDL

create table contain_food_type(
	restaurant_id bigint not null,
    contain_food_types varchar(255) not null,
    primary key(restaurant_id , contain_food_types),
    foreign key(restaurant_id) references restaurant(id)
    
);
  • 이때 JPA는 restaurant_idcontain_food_types를 복합 키로 인식하므로 개발자가 직접 복합 키로 지정하고 restaurant_id 같은 경우는 FK로 설정하는 게 좋음

  • 직접 PK와 FK를 지정하지 않는다고 문제는 안되지만, JPA는 기본적으로 그렇게 알고 동작하기 때문에 지정해주는 게 좋음


1. restaurant_id 칼럼

  • 주인 테이블의 PK에 대한 외래 키이자 컬렉션 테이블의 기본 키로 주인 테이블과 동일한 PK를 공유하게 됨(식별 관계)
  • 즉 Long으로 id를 지정했으니 bigint 타입이 자동으로 지정되게 됨
  • 그에 따라 주인 테이블과 동일한 생명주기에 놓이게 되므로 자동으로 주인테이블에 cascade = CascadeType.ALL , orphanRemoval = true가 설정되는 것

2. contain_food_types 칼럼

  • 주인 엔티티에서 리스트(containFoodTypes)에 할당했던 값들이 해당 칼럼에 저장되게 됨
  • 다른 칼럼들과 마찬가지로 @Column(name= "")으로 칼럼명을 지정하지 않았으니 기본 리스트의 이름인 containFoodTypes에 DB에서 자주 쓰이는 snake_case의 문법이 자동 적용되어 conain_food_types 이름으로 매핑되게 됨

DDL을 찾지 못할 경우 발생하는 오류

JPA가 컬렉션 테이블로 지정한 테이블을 찾지 못할 경우org.hibernate.exception.SQLGrammarException를 발생시키므로 정확히 테이블을 매핑해줘야함


발생되는 쿼리 및 문제

select * from restaurant r join contain_food_type c on r.id = c.restaurant_id;
  • 위와 같은 컬렉션 테이블 조인 쿼리를 JPA가 알아서 어노테이션을 감지하여 해당 조인 쿼리를 자동으로 작성해서 DB에 flush해주긴 함
  • 문제는 DataGrip같은 DB 툴에서 쿼리 테스트를 할 경우 일대다 테이블이 DB에 저장된 것이므로 직접 컬렉션 테이블을 조인해줘야 가져올 수 있음

결론 및 문제

  • 말로만 @CollectionTable과 @ElementCollection을 쓰지 말라고 공부했지 직접 체감해보진 못했기 때문에 프로젝트에 한번 적용해보았다.

  • 하지만 직접 JPA가 만들어주는 DDL과 네이밍컨벤션과 칼럼 타입 복합 키로 PK와 칼럼을 하나로 묶어 기본 키로 지정해줘야하는 것까지 똑같이 DDL을 만들어줘야하는 번거로움이 있었다.

  • 생각해보면 @CollectionTable & @ElementCollection의 취지는 개발자가 편하도록 JPA가 자동으로 주인 테이블의 생명주기와 일치하는 테이블을 별도로 만들어주는 것인데 자동 ddl 설정을 끌 경우 개발자가 별도로 @CollectionTable을 통해 만들어질 DDL을 직접 만들어줘야 하는 것이므로 차라리 일대다 관계의 테이블을 따로 생성해서 운영하는 것이 나을 것 같다는 생각을 하였다.

  • 또 중복을 방지해야한다는 생각을 매번 해주면서 컬렉션을 정의 시 Set으로 일일이 설계하는 것도 개발자에겐 번거롭고 실수로 List로 정의하고 운영 시에 추후에 불러올 Side Effect가 어마무시하다.

  • 굳이 JPA가 인식해주는 형식을 맞춰서 DDL도 개발자가 직접 만들어야할 뿐더러 , 해당 어노테이션을 쓸 경우 수정 삭제도 해주면 성능 상 좋지 않아지고 튜닝하기 어려워지므로 처음부터 번거롭더라도 아싸리 일대다 관계의 테이블을 만들어서 그럴 걱정 없게 설계하자 !

profile
이유에 대해 끊임없이 생각하고 고민하는 개발자

0개의 댓글