도메인을 설계하는 작업 중에 내가 하고 싶었던 것은 Entity 안에 컬럼을 Collection 으로 관리하고 싶었다. 그래서 @ElementCollection 어노테이션을 사용하였고, 이로 해결이 안되어서 @ManyToMany를 사용하려고 했다. 실무에서는 @ManyToMany를 사용하지 않도록 권고하는 말이 많았어서 그 이유와 해결 방법에 대해서 적어보려고 한다.
첫번째로 도메인을 설계하는데 Entity 안에 컬럼을 List 형식으로 만들고 싶었다.
그렇게 @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;
}
@ElementCollection
@OneToMany
출처 및 참고 : JPA @ElementCollection
[JPA] 필드와 컬럼 매핑 - @ElementCollection (값 타입 컬렉션 매핑), @CollectionTable
컬럼을 자료형으로 만든다고 해결되지는 않았다.
차분하게 엔티티와의 관계도를 다이어그램 형식으로 그려보니 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의 조합을 만들어서 사용해야 한다.
@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 의 조인 방식이 내가 필요한 부분에 적합하다고 생각이 되었기 때문이다.