포스팅 할 내용은 ERD를 설계하는 과정에서 발생한 컬렉션 타입 엔티티 이슈입니다!
머나먼 옛날 첫번째 프로젝트를 진행할 당시 ERD, 즉 엔티티 설계만 해도 굉장히 큰 산이었고, 코드 한글자 한글자 헤맸던 경험이 있습니다. (물론 언제나 엔티티 설계라는 것은 굉장히 꼼꼼해야 하고, 중요한 단계입니다!)
지금 보면 다양한 해결책과 방법들이 보이지만, 당시에는 폭풍 검색 해가며 하나씩 해결했던 기억이 납니다.
그 과정 같이 구경 하실래요?
회고 시작해보겠습니다!
원활한 이해를 위해 엔티티 설명을 조금하고 시작하겠습니다.
이슈가 되는 엔티티 관계는 User
와 Category
엔티티입니다. 이때 Category
는 Enum 타입입니다.
유저의 경우 여러개의 카테고리를 가질 수 있습니다.
@Entity
@Getter
@NoArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
//...생략
@Getter
public enum Category {
JAVA("Java", 3.2),
PYTHON("Python", 3),
NETWORK("네트워크", 2.6),
ALGORITHM("알고리즘", 2.7),
AI("인공지능", 2.7),
DATABASE("데이터베이스", 2.6),
//...생략
private final String name;
private final double score;
Category(String name, double score) {
this.name = name;
this.score = score;
}
한명의 유저가 여래개의 카테고리를 가져야하는 요구사항이 있었기에, 저는 자연스럽게 여러 요소를 담을 수 있는 컬렉션 타입을 떠올리게 되었습니다.
아! 그럼 연관관계 설정 시 유저가 가지는 카테고리를 List 컬렉션으로 설정해주면 되지 않을까?
하고 바로 시도해봤습니다.
@Entity
@Getter
@NoArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
//...생략
private List<Category> categories = new ArrayList<>();
실행결과 바로 에러가 발생했는데요 (사실 실행 전부터 느낌이 쎄했습니다)
따라서 결론적으로 두가지 방법이 존재했는데요
하나씩 알아보도록 하겠습니다.
JPA에서 데이터의 타입은 엔티티 타입과 값 타입으로 분류할 수 있습니다.
이때 값 타입은 자바의 primitive 타입이나 객체를 의미합니다.
값 타입의 분류는 크게 3가지로 나눌 수 있는데, 아래와 같습니다.
이러한 값 타입을 여러개 저장하고자 할 때 제가 생각했던 것처럼 자바의 컬렉션을 사용하게 되는데요
이때 @ElementCollection 어노테이션을 사용하여 이러한 값 타입 컬렉션을 사용할 수 있게됩니다.
JPA가 @ElementCollection 을 통해 컬렉션 객체임을 알 수 있도록 하고, 별도의 테이블 생성 후 일대다 관계로 다루게 됩니다.
@Entity
@Getter
@NoArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
//...생략
@Elementcollection
private List<Category> categories = new ArrayList<>();
언뜻보면 @OneToMany를 사용하는 것과 비슷해보이지만 큰 차이가 있습니다.
@ElementCollection을 사용한 값 타입 컬렉션은 자신만의 생명주기를 가지지 않습니다.
즉, 부모 엔티티의 수명주기와 동일하게 움직이는 것이죠. (cascade 옵션을 제공하지 않습니다)
식별자 개념이 없습니다.
컬렉션 값을 변경하는 경우, 전체 삭제 후에 새로 추가해야하는 번거로움과 추적이 어렵다는 제약사항이 있습니다.
이러한 제약사항을 보완하는 대안으로 두번째 방법이 있습니다.
별도의 식별자를 가지고, 컬럼으로 Category
를 가지는 일대다 관계의 엔티티를 새로 만드는 것입니다.
아래와 같이 CategoryUser
엔티티를 새로 생성합니다.
@Entity
@Getter
@NoArgsConstructor
public class CategoryUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_user_id")
private Long id;
@Enumerated(EnumType.STRING)
private Category category;
이 후 유저와 일대다 연관관계를 설정해주고,
cascade = all
과 orphanRemoval = true
로 설정해주면 @ElementCollection 의 제약사항을 해결하고 컬렉션 타입을 사용할 수 있게됩니다.
정리해보자면 위의 두가지 방법 모두 별도의 테이블을 생성하여 컬렉션 타입을 저장해야 합니다.
큰 차이점은 식별자의 유무이고, 식별자가 없는 경우 관리가 번거롭고, 추적이 어렵기 때문에
별도의 식별자를 가지는 새로운 엔티티를 생성 후 일대다 연관관계로 풀어내는 방법을 추천합니다.
한 번 적용해 볼까요?
위에서 말한 것처럼 새로운 CategoryUser
엔티티를 만들어줍니다.
@Entity
@Getter
@NoArgsConstructor
public class CategoryUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_user_id")
private Long id;
@Enumerated(EnumType.STRING)
private Category category;
이후 일대다 연관관계를 User
와 CategoryUser
에 설정해줍니다.
@Entity
@Getter
@NoArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
//...생략
// 추가
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CategoryUser> categoryUsers = new ArrayList<>();
@Entity
@Getter
@NoArgsConstructor
public class CategoryUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_user_id")
private Long id;
@Enumerated(EnumType.STRING)
private Category category;
//연관관계 추가
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
각각의 장단점이 존재하니 무조건적인 방법은 없습니다.
오히려 단순한 값 타입이라면 @ElementCollection이 좋은 방법일 수 있습니다.
중요한 단계 중 하나인 DB 설계이니 꼼꼼히 고민하고, 적절한 방법 사용을 권장합니다!!
읽어주셔서 감사합니다😜
References
enum을 list로 어떻게 받는지 궁금합니다. - 인프런