값 타입 컬렉션 매핑 : @ElementCollection, @CollectionTable

밀크야살빼자·2024년 5월 13일
0

아이돔에서 팀캘린더의 참여자들 관리하기 위해 별도의 테이블로 분리하지 않고, @ElementCollection@CollectionTable을 사용하여 관리하는 것으로 설계했습니다. 하지만 이러한 선택은 무지의 상태에서 이루어졌기 때문에, 서비스를 유지하면서 성능을 향상시키는 데에 많은 제약사항을 마주 하게되었습니다. 따라서 @ElementCollection@CollectionTable가 개념과 장단점과 제가 마주한 문제에 대해서 이야기하려고 합니다.

값 타입 컬렉션

값 타입을 컬렉션에 담아서 사용하는 것을 말합니다. 테이블은 자료구조(컬렉션)을 가질 수 없습니다. 그렇기 때문에 별도의 테이블을 구성해야 합니다. 그러나 값타입 테이블은 본인이 속한 엔티티에 ‘종속’ 됩니다. 엔티티와 엔티티의 종속은 영속성 전이와 고아객체 제거로 구현되지만 값타입 컬렉션은 자동으로 종속설정이 이루어집니다.

@ElementCollection, @CollectionTable

@ElementCollection

값 타입 컬렉션을 매핑할 때 사용합니다.

값 타입 컬렉션 객체임을 JPA가 알 수 있게 해주는 어노테이션입니다.

엔티티가 아닌 값 타입, 임베디드 타입에 대한 테이블을 생성하고 1:M 관계로 매핑됩니다.

이때, @Entity가 아닌 Basic Type이나 Embeddable Class로 정의된 컬렉션을 테이블로 생성하며 1:M 관계로 다룹니다.

@CollectionTable

@ElementCollection 과 함께 사용됩니다.

값 타입 컬렉션을 매핑할 테이블에 대한 역할을 지정하는데 사용합니다.(테이블의 이름과 조인 정보를 적어주어야 합니다.)

만약 이를 생략한다면 기본값을 이용하여 매핑합니다.

기본값 : {엔티티이름}_{컬렉션 필드 이름}

@Entity
public class TeamCalendar {
	
	@Id
	@GeneratedValue(strategy = GenerationTypy.IDENTITY)
	private Long id;
	
	@Embedded
	private Participants participants;
}


@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Participants {
	
	@ElementCollection(fetch = FetchType.LAZY)
	// String인 경우에 한해서 예외적으로 허용, 이외 타입은 @AttributeOverride를 사용해서 테이블 속성을 재정의한다.
	@Column(name = "PARTICIPANT")
	private Set<String> participants = new HashSet<>();

	//Embedded type
	@ElementCollection
	@CollectionTable(name = "participant", joinColumns = @JoinColumn(name = "team_calendar_id"))
	private Set<Participant> participants = new HashSet<>();

	public Participants(final Set<Participant> participants) {
		this.participants = participants;
	}
}

@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = "memberId")
public class Participant implements Comparable<Participant> {

	@Column(nullable = false)
	private Long memberId;

}

특징

  • 자신의 라이프 사이클을 가지지 않습니다.
  • 영속성 전이(Cascade)와 고아 객체 제거 기능을 필수로 가진 것과 비슷하다고 볼 수 있습니다.
  • 기본적으로 @ElementCollection이 Lazy로 설정되어 있어 지연로딩으로 작동합니다.
  • 식별자 개념이 없습니다.
  • 변경 감지를 못합니다.

제약 사항

  • 식별자 개념이 없어서 값을 변경하면 추적이 어렵습니다.
  • 값 타입 컬렉션에 변경 사항(저장, 삭제)이 발생하면, 소유하는 엔티티와 연관된 모든 데이터를 삭제하고, 현재 남아있는 값을 모두 다시 저장합니다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 합니다. (null 입력X, 중복 저장X)

우리는 SQL 쿼리가 삭제할 때 delete 쿼리가 1개 insert 쿼리가 1개 나가는 것을 기대합니다. 그러나 값 타입 컬렉션은 주인 엔티티와 연관된 모든 데이터를 삭제하고 다시 저장합니다. 따라서 주인 엔티티와 관련된 엔티티의 데이터가 5개가 있다면, delete 쿼리가 5개가 나가고, 주인 엔티티가 삭제됩니다. 즉 쿼리가 6개가 나갑니다.

대안

값 타입 컬렉션을 사용하는 것 대신에 엔티티를 사용하여 1:M 관계를 맺는 것 입니다.

  • 1:M 엔티티를 만들고, 엔티티에서 값 타입을 사용합니다.
  • cascade와 고아 객체 제거를 설정해서 값 타입 컬렉션처럼 사용할 수 있습니다.

@ElementCollection과 @OneToMany의 차이

@ElementCollection

  • 연관된 부모 Entity 하나에만 연관되어 관리됩니다.(부모 Entity와 독립적으로 사용이 불가능합니다.)
  • 항상 부모와 함께 저장되고 삭제되므로 cascade 옵션은 제공하지 않습니다.(cascade = ALL와 같다고 볼 수 있습니다.)
  • 부모 Entity Id와 추가 컬럼(basic or embedded 타입)으로 구성됩니다.
  • 기본적으로 식별자 개념이 없으므로 컬렉션 값 변경 시, 전체 삭제 후 새로 추가합니다.

@OneToMany / @ManyToMany

  • 다른 Entity에 의해 관리될 수도 있습니다.
  • join table이나 컬럼은 보통 ID만으로 연관을 맺습니다.

내가 마주한 문제점

아이돔에서 팀 캘린더마다 참여하는 참여자들을 등록하는 요구사항이 있었습니다. 팀 일정에 참여하는 멤버들을 등록하는 요구사항이 있었습니다. 그러나 회원이 팀에서 나가거나 탈퇴할 경우, 해당 회원이 등록되어 있는 팀 캘린더 참여자 목록에서 삭제되어야 했습니다. 불행하게도, 미숙한 상태에서 이 부분을 설계하면서 값 타입 컬렉션을 사용하게 되었습니다. 그 결과, 회원이 참여한 팀 일정의 수만큼 삭제 쿼리가 발생하였고, 벌크 연산 또한 할 수 없었습니다. 이에 대응하기 위해 엔티티로 변경하는 방법을 고려했지만, 그에 따른 코스트가 크고 이 부분이 자주 발생하지 않기 때문에 그대로 유지하기로 결정했습니다.


- 참고 자료

profile
기록기록기록기록기록

0개의 댓글