Programming/JPA

JPA - 값 타입 컬렉션

잇(IT) 2023. 7. 17. 21:55

- 값 타입 컬렉션

- 값 타입을 하나 이상 저장할 때 사용한다.

- 값 타입 컬렉션은 관계형 데이터 베이스에 담을 수 없다. 값만 넣을 수 있다.

- 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

- 값 타입, 엔티티를 구분해야 한다.

- 클래스 타입으로 컬렉션을 매핑하면 해당 속성들을 컬럼명으로 사용하고, String과 같이 데이터가 단일일 경우 해당 필드명을 컬럼명으로 사용하는 것이 기본이고 예외적으로 @Column(name = "XXX")를 통해 컬럼명을 지정할 수 있다.

- @ElementCollection, @CollectionTable 사용한다.

 

- 값 타입 컬렉션 사용

1. 값 타입 저장

2. 값 타입 조회 - 값 타입 컬렉션도 지연 로딩 전략 사용한다.

3. 값 타입 수정

 

- 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제 거 기능을 필수로 가진다고 볼 수 있다.


@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")//예외적으로 컬럼명을 지정할 수 있다.
private Set<String> favoriteFoods = new HashSet<>();

- Set을 통한 String 컬렉션 생성

 

@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

- List를 통한 Address 임베디드 타입 컬렉션 생성

- 위와 같이 실행하게 되면 JPA에서 컬렉션에 해당하는 두개의 테이블을 생성하는 것을 볼 수 있다.


Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));
//
            member.getFavoriteFoods().add("치킨");
            member.getFavoriteFoods().add("피자");
            member.getFavoriteFoods().add("족발");
//
            member.getAddressHistory().add(new Address("old1", "street", "10000"));
            member.getAddressHistory().add(new Address("old2", "street", "10000"));

- 컬렉션은 별도로 persist를 해주지 않아도 쿼리가 동작하였다.

- 값 타입의 생명주기는 엔티티에 있기 때문에 엔티티에 속해 있으면 엔티티의 값이 변경되거나 하면 자동으로 해당 부분의 쿼리가 자동으로 실행된다.


Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));
//
            member.getFavoriteFoods().add("치킨");
            member.getFavoriteFoods().add("피자");
            member.getFavoriteFoods().add("족발");
//
            member.getAddressHistory().add(new Address("old1", "street", "10000"));
            member.getAddressHistory().add(new Address("old2", "street", "10000"));
//
            em.persist(member);
//
            em.flush();
            em.clear();
//
            System.out.println("===============START======================");
            Member findMember = em.find(Member.class, member.getId());

- 영속성 컨텍스트를 초기화 하고 find를 실행하게 되면, member만 가져오게 된다.

- 즉, 값 타입 컬렉션은 지연 로딩이 기본값인 걸 알 수 있다.


//             조회
            List<Address> addressHistory = findMember.getAddressHistory();
            for (Address address : addressHistory) {
                System.out.println("address.getCity() = " + address.getCity());
            }

            Set<String> favoriteFoods = findMember.getFavoriteFoods();
            for (String favoriteFood : favoriteFoods) {
                System.out.println("favoriteFood = " + favoriteFood);
                }

- 컬렉션에 있는 값들을 조회하게 되면 그제서야 쿼리를 날려 데이터들을 가져오는 것을 확인 할 수 있다.


- 값 타입 수정

 

            //값 수정
            Address a = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
            //수정의 경우 완전히 교체를 해주어야 한다.

- 값 타입 수정의 경우 같은 엔티티를 통해 값을 수정해 줄 경우 종속된 다른 데이터까지 변경될 수 있기 때문에 반드시 인스턴스를 새롭게 생성하여 교체해주어야 한다.


- 컬렉션 값 타입 수정

//값 타입 음식 컬렉션 수정
            //치킨 -> 한식
            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("한식");
            //아예 값을 삭제하고 새롭게 넣어야 한다.

- 기존의 값을 delete로 삭제하고 새롭게 입력한 값을 insert로 넣어준다.


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

            //값 타입 주소 컬렉션 수정
            findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
            //equals로 실행되기 때문에 equals 오버라이드를 해야한다.
            findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

- ADDRESS 자체를 delete하는 것을 볼 수 있다.

 

- 또한 하나의 값을 생성하였는데 두번의 insert문이 실행되는 것을 확인 할 수 있다. 

* 하나의 값을 삭제하고 하나의 값을 넣었는데 insert 쿼리가 두번 나가는 것이 이상하게 보일 수 있다.


- 값 타입 컬렉션의 제약사항

1. 값 타입은 엔티티와 다르게 식별자 개념이 없다.
2. 값은 변경하면 추적이 어렵다.
3. 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
4. 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력X, 중복 저장X

 

- 즉, ADDRESS와 관련된 모든 데이터를 삭제한다.

- 또한 실무에선 결국 사용하지 않는 것이 좋다.


- 값 타입 컬렉션 대안


1. 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
2. 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
3. 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
4. EX) AddressEntity

 

 

//    @ElementCollection
//    @CollectionTable(name = "ADDRESS", joinColumns =
//    @JoinColumn(name = "MEMBER_ID"))
//    private List<Address> addressHistory = new ArrayList<>();
//    // 결국 안쓴다.
//
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();

 

package hellojpa;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Address address;

    public AddressEntity() {

    }
    
    public AddressEntity(String city, String street, String zipcode) {
        this.address = new Address(city, street, zipcode);
    }


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

- 값 타입 ADDRESS를 매핑할 ADDRESS 관련 엔티티를 새롭게 하나 생성해준다.

 

//            member.getAddressHistory().add(new Address("old1", "street", "10000"));
//            member.getAddressHistory().add(new Address("old2", "street", "10000"));

            member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
            member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));

- update 쿼리가 나가는 이유는 일대다 @OneToMany는 외래키가 다른 테이블에 있기 때문에 값을 변경해야 하기 때문이다.


- 정리

1. 엔티티 타입의 특징

1.1. 식별자 O

1.2. 생명 주기 관리

1.3. 공유

2. 값 타입의 특징

2.1. 식별자 X

2.2. 생명 주기를 엔티티에 의존

2.3. 공유하지 않는 것이 안전(복사해서 사용)

2.4. 불변 객체로 만드는 것이 안전

 

 

 

 

 

 

 

 

 

 

 

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (자바 ORM 표준 JPA 프로그래밍 - 기본편)

728x90