JPA 컬렉션 타입 엔티티

일상 회고록·2024년 2월 5일
0
post-thumbnail
post-custom-banner
안녕하세요!

포스팅 할 내용은 ERD를 설계하는 과정에서 발생한 컬렉션 타입 엔티티 이슈입니다!

머나먼 옛날 첫번째 프로젝트를 진행할 당시 ERD, 즉 엔티티 설계만 해도 굉장히 큰 산이었고, 코드 한글자 한글자 헤맸던 경험이 있습니다. (물론 언제나 엔티티 설계라는 것은 굉장히 꼼꼼해야 하고, 중요한 단계입니다!)

지금 보면 다양한 해결책과 방법들이 보이지만, 당시에는 폭풍 검색 해가며 하나씩 해결했던 기억이 납니다.

그 과정 같이 구경 하실래요?


회고 시작해보겠습니다!

0. 엔티티 상황

원활한 이해를 위해 엔티티 설명을 조금하고 시작하겠습니다.

이슈가 되는 엔티티 관계는 UserCategory 엔티티입니다. 이때 Category는 Enum 타입입니다.

유저의 경우 여러개의 카테고리를 가질 수 있습니다.

  • User
@Entity
@Getter
@NoArgsConstructor
public class User {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    //...생략
  • Category
@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;
    }

1. 이슈

한명의 유저가 여래개의 카테고리를 가져야하는 요구사항이 있었기에, 저는 자연스럽게 여러 요소를 담을 수 있는 컬렉션 타입을 떠올리게 되었습니다.

아! 그럼 연관관계 설정 시 유저가 가지는 카테고리를 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<>();

실행결과 바로 에러가 발생했는데요 (사실 실행 전부터 느낌이 쎄했습니다)


Enum 타입을 리스트로 가질 수 있는 방법을 구글링 한 결과 RDB의 경우 컬렉션 타입의 저장을 지원하지 않는다고 합니다.

따라서 결론적으로 두가지 방법이 존재했는데요

  • @ElementCollection 사용
  • 별도의 식별자와 Category 컬럼을 가지는 엔티티(테이블)을 새로 생성 후 유저와 연관관계 설정

하나씩 알아보도록 하겠습니다.

1-1. @ElementCollection

JPA에서 데이터의 타입은 엔티티 타입과 값 타입으로 분류할 수 있습니다.

이때 값 타입은 자바의 primitive 타입이나 객체를 의미합니다.

값 타입의 분류는 크게 3가지로 나눌 수 있는데, 아래와 같습니다.

  • 기본 값 타입
    • 자바 기본 타입 (int, double)
    • 래퍼 클래스 (Integer, Long)
    • String
  • 임베디드 타입
  • 컬렉션 값 타입

이러한 값 타입을 여러개 저장하고자 할 때 제가 생각했던 것처럼 자바의 컬렉션을 사용하게 되는데요

이때 @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 옵션을 제공하지 않습니다)

  • 식별자 개념이 없습니다.

    컬렉션 값을 변경하는 경우, 전체 삭제 후에 새로 추가해야하는 번거로움과 추적이 어렵다는 제약사항이 있습니다.

이러한 제약사항을 보완하는 대안으로 두번째 방법이 있습니다.

1-2. 엔티티 생성

별도의 식별자를 가지고, 컬럼으로 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 = allorphanRemoval = true로 설정해주면 @ElementCollection 의 제약사항을 해결하고 컬렉션 타입을 사용할 수 있게됩니다.

1-3. 정리

정리해보자면 위의 두가지 방법 모두 별도의 테이블을 생성하여 컬렉션 타입을 저장해야 합니다.

큰 차이점은 식별자의 유무이고, 식별자가 없는 경우 관리가 번거롭고, 추적이 어렵기 때문에

별도의 식별자를 가지는 새로운 엔티티를 생성 후 일대다 연관관계로 풀어내는 방법을 추천합니다.

한 번 적용해 볼까요?

2. 적용

위에서 말한 것처럼 새로운 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;

이후 일대다 연관관계를 UserCategoryUser에 설정해줍니다.

  • User
    @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<>();
  • 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;
    
    		//연관관계 추가
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id")
        private User user;
    

이렇게 해서 유저가 여러 카테고리를 가지는 컬렉션 타입 요구사항을 해결해보았습니다.

각각의 장단점이 존재하니 무조건적인 방법은 없습니다.

오히려 단순한 값 타입이라면 @ElementCollection이 좋은 방법일 수 있습니다.

중요한 단계 중 하나인 DB 설계이니 꼼꼼히 고민하고, 적절한 방법 사용을 권장합니다!!

읽어주셔서 감사합니다😜



References

enum을 list로 어떻게 받는지 궁금합니다. - 인프런

[JPA] 값 타입 컬렉션 : @ElementCollection, @CollectionTable

JPA @ElementCollection

profile
하고 싶은 것들이 많은 개발자입니다
post-custom-banner

0개의 댓글