값 타입2

2025. 2. 28. 16:56JPA

◎ 값 타입 컬렉션

-> 값 타입을 컬렉션에 담아서 사용하는 방식으로 값 타입을 하나 이상 저장할 때 사용한다.

-> 데이터베이스에는 같은 테이블에 컬렉션을 저장할 수 없다. 따라서 컬렉션을 저장하기 위한 별도의 테이블을 생성해서 저장해야 한다.

-> 다음은 값 타입 저장 예제다.

@Entity
public class Member extends BaseEntity{

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

    @Column(name = "name")
    private String username;


    // Period
    @Embedded
    private Period workPeriod;

    // Address
    @Embedded
    private Address homeAddress;

	// 값 타입 컬렉션
    @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<>();
// 값타입 컬렉션 저장
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);

tx.commit();

- 결과

- member만 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());

// 컬렉션 값 타입은 지연로딩으로 아래 코드에서 컬렉션 테이블에 있는 실제 값을 가져올 때 select 쿼리가 나간다.
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);
}

tx.commit();

- 결과

====================== START ==================
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.createdBy,
        m1_0.createdDate,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.lastModifiedBy,
        m1_0.lastModifiedDate,
        t1_0.TEAM_ID,
        t1_0.createdBy,
        t1_0.createdDate,
        t1_0.lastModifiedBy,
        t1_0.lastModifiedDate,
        t1_0.name,
        m1_0.name,
        m1_0.endDate,
        m1_0.startDate 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?
Hibernate: 
    select
        ah1_0.MEMBER_ID,
        ah1_0.city,
        ah1_0.street,
        ah1_0.zipcode 
    from
        ADDRESS ah1_0 
    where
        ah1_0.MEMBER_ID=?
address.getCity() = old1
address.getCity() = old2
Hibernate: 
    select
        ff1_0.MEMBER_ID,
        ff1_0.FOOD_NAME 
    from
        FAVORITE_FOOD ff1_0 
    where
        ff1_0.MEMBER_ID=?
favoriteFood = 치킨
favoriteFood = 햄버거
favoriteFood = 피자

- 값 타입 컬렉션은 기본적으로 지연 로딩이기 때문에 컬렉션 내의 값을 조회할 때 select 쿼리가 나간다.

 

-> 다음은 값 타입 수정 예제다.

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());

            // homeCity -> newCity
//            findMember.getHomeAddress().setCity("newCity"); // -> 값 타입은 불변해야 한다.
            Address a = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));

            // 값타입 컬렉션 수정 (치킨 -> 한식)
            // 기존에 있는 값을 지우고 수정하고자 하는 값을 새로 추가헤야 한다.
            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("한식");

            // 주소 변경(old1 -> new1)
            findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
            findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

- 컬렉션 값을 수정하려면 기존 값을 지우고 수정하고자 하는 값을 새로 저장해야 한다.

- 이 방식대로 실행하면 값을 수정할 때 update 쿼리가 아니라 delete를 해서 기존 값을 지우고 insert를 통해 수정 값을 저장하도록 한다.

Hibernate: 
    /* one-shot delete for hellojpa.Member.addressHistory */delete 
    from
        ADDRESS 
    where
        MEMBER_ID=?
Hibernate: 
    /* insert for
        hellojpa.Member.addressHistory */insert 
    into
        ADDRESS (MEMBER_ID, city, street, zipcode) 
    values
        (?, ?, ?, ?)
Hibernate: 
    /* insert for
        hellojpa.Member.addressHistory */insert 
    into
        ADDRESS (MEMBER_ID, city, street, zipcode) 
    values
        (?, ?, ?, ?)
Hibernate: 
    /* delete for hellojpa.Member.favoriteFoods */delete 
    from
        FAVORITE_FOOD 
    where
        MEMBER_ID=? 
        and FOOD_NAME=?
Hibernate: 
    /* insert for
        hellojpa.Member.favoriteFoods */insert 
    into
        FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) 
    values
        (?, ?)

 

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

 

◎ 값 타입 대안 

-> 값 타입 컬렉션 대신 엔티티를 만들어 일대다 관계를 형성하여 값 타입을 사용한다.

@Entity
public class Member extends BaseEntity{

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

    @Column(name = "name")
    private String username;


    // Period
    @Embedded
    private Period workPeriod;

    // Address
    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    // 값 타입 컬렉션 대안
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
// 컬렉션 값 타입 대안
findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "10000"));
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "10000"));

- 결과

- update 쿼리가 나간다.

 

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

-> 값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 이 값을 변경하면 추적하기 어렵다.

-> 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

-> 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어 기본 키를 구성해야 한다.(null 입력x, 중복 저장x)

 

★ 참고

자바 ORM 표준 JPA 프로그래밍 - 기본편

'JPA' 카테고리의 다른 글

값 타입1  (0) 2025.02.27
영속성 전이 CASCADE & 고아 객체  (0) 2025.02.27
즉시 로딩 & 지연 로딩  (0) 2025.02.26
JPA 프록시  (0) 2025.02.22
@PrePersist  (0) 2023.05.17