@ElementCollection 와 @ManyToMany

크리링·2023년 2월 14일
0

오늘의 문제

목록 보기
1/9
post-thumbnail

도메인을 설계하는 작업 중에 내가 하고 싶었던 것은 Entity 안에 컬럼을 Collection 으로 관리하고 싶었다. 그래서 @ElementCollection 어노테이션을 사용하였고, 이로 해결이 안되어서 @ManyToMany를 사용하려고 했다. 실무에서는 @ManyToMany를 사용하지 않도록 권고하는 말이 많았어서 그 이유와 해결 방법에 대해서 적어보려고 한다.

첫번째로 도메인을 설계하는데 Entity 안에 컬럼을 List 형식으로 만들고 싶었다.

그렇게 @ElementCollection 어노테이션을 사용하게 되었다.

@ElementCollection

@ElementCollection은 RDB에서 컬렉션과 같은 형태의 데이터를 컬럼에 저장할 수 없기 때문에, 별도의 테이블을 생성하여 컬렉션을 관리한다.

이때 값 타입 컬렉션은 개념적으로 보면 1대 N관계입니다.

@ElementCollection@CollectionTable과 함께 사용합니다.

@CollectionTable은 값 타입 컬렉션을 매핑할 테이블에 대한 정보를 지정하는 역할을 수행합니다.

ex)

@Entity
public class StudyGroup {
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    // Basic type
    @ElementCollection(fetch = FetchType.LAZY)
    @Column(name = "TOPIC_NAME")//String인 경우에 한해서 예외적으로 허용, 이외 타입은 @AttributeOverride를 사용해서 테이블 속성을 재정의한다.
    private Set<String> topicTags = new HashSet<String>();
 
    // Embedded type
    @ElementCollection
    @CollectionTable(name="study_group_member", joinColumns = @JoinColumn(name= "study_group_id", referencedColumnName = "id"))
    private Set<StudyGroupMember> members = new HashSet<StudyGroupMember>();
}

@Embeddable
public class StudyGroupMember {
    private UUID memberId;
    private Boolean isOwner;
}
  • 값 타입 컬렉션은 조회시 지연로딩 전략을 사용합니다.
  • 값 타입은 그 생명주기를 부모 엔티티에 의해 관리된다. 영속성 전이(CASCADE ALL) + 고아 객체 제거 기능을 필수로 가집니다.

주의

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

@OneToMany와의 차이

@ElementCollection

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

@OneToMany

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

출처 및 참고 : JPA @ElementCollection
[JPA] 필드와 컬럼 매핑 - @ElementCollection (값 타입 컬렉션 매핑), @CollectionTable






컬럼을 자료형으로 만든다고 해결되지는 않았다.
차분하게 엔티티와의 관계도를 다이어그램 형식으로 그려보니 ManyToMany 관계였다.
하지만 이전에도 책들에서 ManyToMany는 실무에서 사용하는 것을 권장하지 않는다고 본 적이 있었다. 이번에 그 이유와 다른 방법으로 해결을 해보려고 한다.

@ManyToMany를 사용하면 안되는 이유

다대다 관계의 경우 그대로 사용하지 못하고 반드시 정규화를 통해 중간 테이블을 만들어줘야 한다.

JPA에서는 @ManyToMany를 통해 연관관계를 매핑할 경우 하이버네이트가 위와 같은 중간 테이블을 알아서 만들어서 처리해준다.

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToMany
    @JoinTable(name = "새로 만들어줄 중간 테이블 이름")
    private List<Product> products = new ArrayList<>();
}
.
.
.
@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();

중간 테이블을 만들고 PK, FK 쌍을 알아서 매핑해주는 것 까지는 괜찮은데, 실무 레벨에서는 이러한 테이블 매핑에 필요한 필수적인 정보들 외에도 중간 테이블이 가져야하는 여러가지 칼럼들이 있을 수 있다. (ex.변경 시간과 같은 정보)

하이버네이트에 의해 생성된 중간 테이블은 관계 설정에 필수적으로 필요한 정보들만 담겨있을 뿐 이러한 비즈니스 로직상 필요한 정보들은 담기지 않는다.

따라서 실무 단계에서는 @ManyToMany를 절대 사용하지 말아야 한다.

다대다 관계를 사용하고 싶은 경우라면 중간 테이블에 대한 클래스를 직접 만들어서 @ManyToOne과 @OneToMany의 조합을 만들어서 사용해야 한다.

@ManyToOne + @OneToMany

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}
.
.
.
@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}
.
.
.
@Entity
public class MemberProduct {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
}

관계도는 다음과 같다.

중간 테이블을 하나의 엔티티 개념으로 사용하면 (MemberProduct -> Order) 이러한 형태를 가질 수 있게 된다. (ORDER_ID는 Generated Value로 주어진 비즈니스적 의미를 갖지 않는 값)

출처 및 참고 : @ManyToMany를 사용하면 안되는 이유
[JPA] @ManyToMany, 다대다[N:M] 관계






확실히 왜 DDD가 중요한지 느낀 하루였다. 생각보다 쉽다고 생각했던 자료형을 컬럼으로 받는 데에서 시작되어서 @ManyToOne과 @OneToMany의 쓰임으로 도메인 설계가 굉장히 중요하고, 공을 들여야 후에 문제를 발견하고 돌아올 일이 적다는 것을 느낀다.
후에 @ElementCollection은 @OneToMany로 수정하였다. 그 이유로는 첫번째로는 CASCADE ALL 부분이 후에 부모 Entity 삭제시 재생성 해주어야하는 번거로움이었고, 두번째로는 @OneToMany 의 조인 방식이 내가 필요한 부분에 적합하다고 생각이 되었기 때문이다.



0개의 댓글