ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 9. 값 타입 (2)
    Back-End/JPA 2022. 6. 10. 14:06

     

    5. 값 타입 컬렉션 (Collection Value Type)

    값 타입을 하나 이상 저장하려면 컬렉션에 저장하면 된다.

    이때 2가지 어노테이션이 필요하다.

    • @ElementCollection: 값 타입 컬렉션을 사용하는 속성에 표시한다.
    • @CollectionTable: 컬렉션을 위한 추가 테이블을 매핑한다. 생략 가능하며, 기본값으로 {엔티티 이름}_{컬렉션 속성 이름} 테이블과 매핑한다.

     

    위의 다이어그램에서 favoriteFoods는 기본값 타입 컬렉션이고, addressHistory는 임베디드 타입 컬렉션이다.

    관계형 데이터베이스 테이블의 컬럼 안에 컬렉션을 포함할 수 없기 때문에, 두 가지 값 타입 컬렉션 모두 별도의 테이블을 추가하고, 추가한 테이블을 매핑해야 한다

     

    favoriteFoods 와 addressHistory 값 타입 컬렉션은 아래와 같이 구현할 수 있다.

    예제 코드)

    @Entity
    public class Member {
        ...
        @ElementCollection
        @CollectionTable(
            name = "FAVORITE_FOOD",
            joinColumns = @JoinColumn(name = "MEMBER_ID"))
        @Column(name = "FOOD_NAME") // 컬럼이 하나일 경우, 컬럼명 지정 가능
        private Set<String> favoriteFoods = new HashSet<>();
    
        @ElementCollection
        @CollectionTable(
            name = "ADDRESS",
            joinColumns = @JoinColumn(name = "MEMBER_ID"))
        private List<Address> addressHistory = new ArrayList<>();
        ...
    }

    favoriteFoods 처럼 값으로 사용되는 컬럼이 하나인 경우, @Column을 사용해서 컬럼명을 지정할 수 있다.

     

    5.1. 값 타입 컬렉션 사용

    ✔️ 등록

    Member member = new Member();
    
    // 임베디드 값 타입
    member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123"));
    
    // 기본값 타입 컬렉션
    member.getFavoriteFoods().add("짬뽕");
    member.getFavoriteFoods().add("짜장");
    member.getFavoriteFoods().add("탕수육");
    
    // 임베디드 값 타입 컬렉션
    member.getAddressHistory().add(new Address("서울", "강남", "123-123"));
    member.getAddressHistory().add(new Address("서울", "강북", "000-000"));
    
    em.persist(member);

    마지막에 member 엔티티만 영속화하면, JPA 가 member 엔티티의 값 타입도 함께 저장한다.

    실제 데이터베이스에 실행되는 INSERT SQL은 6개이고, 각각은 다음과 같다.

    • member: INSERT SQL 1번
    • member.homeAddress: 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함된다.
    • member.favoriteFoods: INSERT SQL 3번
    • member.addressHistory: INSERT SQL 2번

     

    📌  값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

     

    ✔️ 조회

    값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있고, 기본값은 LAZY 다.

    @ElementCollection(fetch = FetchType.LAZY)

     

    Member member = em.find(Member.class, 1L);
    
    Address homeAddress = member.getHomeAddress();
    
    Set<String> favoriteFoods = member.getFavoriteFoods();	// LAZY
    
    /** 
    * SQL
    * SELECT MEMBER_ID, FOOD_NAME 
    * FROM FAVORITE_FOODS
    * WHERE MEMBER_ID = 1
    */
    for (String favoriteFood : favoriteFoods) {
    	System.out.println("favoriteFood = " + favoriteFood);
    }
    
    List<Address> addressHistory = member.getAddressHistory(); // LAZY
    
    /** 
    * SQL
    * SELECT MEMBER_ID, CITY, STREET, ZIPCODE 
    * FROM ADDRESS
    * WHERE MEMBER_ID = 1
    */
    addressHistory.get(0);

    위의 예제를 실행할 때 실행되는 SELECT SQL은 다음과 같다.

    • member: 회원만 조회한다. 임베디드 값 타입인 homeAddress도 함께 조회한다. SELECT SQL을 1번 호출한다.
    • member.homeAddress: 회원을 조회할 때 같이 조회해둔다.
    • member.favoriteFoods: LAZY로 설정해서, 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
    • member.addressHistory: LAZY로 설정해서, 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.



    ✔️ 수정

    Member member = em.find(Member.class, 1L);
    
    // 임베디드 값 타입 수정
    member.setHomeAddress("새로운도시", "신도시1", "123456");
    
    // 기본값 타입 컬렉션 수정
    Set<String> favoriteFoods = member.getFavoriteFoods();
    favoriteFoods.remove("탕수육");
    favoriteFoods.add("치킨");
    
    // 임베디드 값 타입 컬렉션 수정
    List<Address> addressHistory = member.getAddressHistory();
    addressHistory.remove(new Address("서울", "기존 주소", "123-123"));
    addressHistory.add(new Address("새로운도시", "새로운 주소", "123-456"));

    임베디드 값 타입 수정

    • MEMBER 테이블만 UPDATE 한다. 사실 Member 엔티티를 수정하는 것과 같다.


    기본값 타입 컬렉션 수정

    • 탕수육을 치킨으로 변경하려면, 탕수육을 제거하고 치킨을 추가해야 한다.
    • 자바의 String 타입은 수정할 수 없다.


    임베디드 값 타입 컬렉션 수정

    • 값 타입은 불변해야 한다.
    • 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록해야 한다.
    • 참고로 값 타입은 equals, hashCode를 꼭 구현해야 한다.



    5.2. 제약사항

    엔티티에 소속된 값 타입의 경우, 값이 변경되어도 소속된 엔티티를 데이터베이스에 찾고 값을 변경하면 된다.
    그러나 값 타입 컬렉션의 경우, 보관된 값 타입들이 별도의 테이블에 보관된다. 따라서 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.

    이런 문제로 인해, JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체 있는 모든 값을 데이터베이스에 다시 저장한다.

    따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.

    참고) 일대다 관계로 설정하기

    더보기

    값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 생성하여 일대다 관계로 설정해보자.

    추가적으로 이 엔티티에 영속성 전이 + 고아 객체 제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.

    @Entity
    public class Member {
    	...
        @OneToMany(casecade = CascadeType.ALL, orphanRemoval = true)
        @JoinColumn(name = "MEMBER_ID")
        private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();
    }
    
    @Entity
    public class AddressEntity {
    	
        @Id @GeneratedValue
        private Long id;
        
        @Embedded Address address;
        ...
    }

     

    그리고 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 이로 인해 (기본 키 제약조건), 컬럼에 null을 입력할 수 없고 같은 값을 중복해서 저장할 수 없다는 제약이 있다. 

     

    6. 정리

      엔티티 타입 (Entity Type) 값 타입 (Value Type)
    식별자 식별자가 있다. 식별자가 없다.
    생명 주기 생명 주기가 있다. 생명 주기를 엔티티에 의존한다.
    공유 공유할 수 있다. 공유하지 않는 것이 안전하다.

    값 타입은 정말 값 타입이라 판단될 때만 사용해야 한다.

    식별자가 필요하고 지속해서 값을 추적하고 구분하고 변경해야 한다면, 그것은 값 타입이 아닌 엔티티다.

     

     

     

     

    728x90

    'Back-End > JPA' 카테고리의 다른 글

    9. 값 타입 (1)  (0) 2022.06.09
    8. 프록시와 연관관계 관리  (0) 2022.06.07
    7. 고급 매핑  (0) 2022.05.03
    6. 다양한 연관관계 매핑  (0) 2022.04.30
    5. 연관관계 매핑 기초  (0) 2022.04.23

    댓글

Designed by Tistory.