nu_s

[JPA] 값 타입 컬렉션 🐶 본문

JPA

[JPA] 값 타입 컬렉션 🐶

woochii 2023. 10. 25. 17:59
728x90
반응형

값 타입 컬렉션

 

  • 값 타입을 컬렉션에 담아서 쓰는 것
  • RDB는 기본적으로 컬렉션을 담을 수 있는 구조가 없다.
  • 그래서 컬렉션을 별도의 테이블로 뽑아서 관리한다.

 

Member

@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Address homeAddress;

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

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

    //Getter ,Setter...
}

 

이렇게 실행하면

FAVORITE_FOODS와 ADDRESS 테이블이 생성되었다.

 

  • 값 타입을 하나 이상 저장할 때 사용한다.
  • @ElementCollection, @CollectionTable을 사용한다.
  • 데이터베이스는 컬렉션 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

 

값 타입 저장

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

member.getFavoriteFood().add("치킨");
member.getFavoriteFood().add("족발");
member.getFavoriteFood().add("피자");

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

em.persist(member);

  • 값 타입 컬렉션을 따로 persist()하지 않았는데 저장이 된 것을 볼 수 있다.
    • 값 타입 컬렉션도 생명주기를 엔티티에 의존하기 때문이다.
    • 참고 : 값 타입 컬렉션은 영속성 전이(CASCADE) + 고아 객체 제거 기능을 필수로 가진다는 것을 알 수 있다.

 

값 타입 조회

// 값 타입 저장
...

em.flush();
em.clear();

// 값 타입 조회
System.out.println("============================");
Member findMember = em.find(Member.class, member.getId());

 

실행 결과

============================
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.city as city2_3_0_,
        member0_.street as street3_3_0_,
        member0_.zipcode as zipcode4_3_0_,
        member0_.TEAM_ID as TEAM_ID6_3_0_,
        member0_.USERNAME as USERNAME5_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
  • Member를 조회했을 때
    • city, street, zipcode를 보았을 때 임베디드 타입의 Address는 즉시 로딩이다. (이게 더 효율적)
    • 컬렉션들은 지연 로딩이다. (FetchType fetch() default LAZY)

 

값 타입 수정

// 값 타입 저장
...

// 값 타입 조회
...

// 값 타입 수정
// homeCity -> newCity

//findMember.getHomeAddress().setCity("newCity"); // 부작용 발생

// 새로운 인스턴스로 갈아껴야 한다.
Address adderss = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", address.getStreet(), address.getZipcode()));

// 컬렉션 수정
// 치킨 -> 한식
findMember.getFavoriteFood().remove("치킨");
findMember.getFavoriteFood().add("한식");

// 주소 old1 -> new1
// 값을 비교해서 제거하기 때문에 equals(), hashcode() 필수
findMember.getAddressHistory().remove(new Address("old1", "street", "zipcode"));
findMember.getAddressHistory().add(new Address("new1", "street", "zipcode"));

 

addressHistory를 수정하는 쿼리를 보면

Hibernate:
    /* delete collection hellojpa.Member.addressHistory */ delete
        from
            ADDRESS
        where
            MEMBER_ID=?
Hibernate:
    /* insert collection row hellojpa.Member.addressHistory */ insert
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode)
        values
            (?, ?, ?, ?)
Hibernate:
    /* insert collection row hellojpa.Member.addressHistory */ insert
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode)
        values
            (?, ?, ?, ?)
  • UPDATE 쿼리가 아닌 DELETE, INSERT 쿼리가 나가는 것을 볼 수 있다.
  • ADDRESS 테이블 전체를 DELETE하고 최종 컬렉션에 남아있는 "old2"와 "new1"을 INSERT

 

값 타입 컬렉션의 제약사항

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

 

값 타입 컬렉션 대안책

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

AddressEntity

@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)
    }

    //Getter, Setter...
}

 

Member

@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Address homeAddress;

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

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

    //Getter ,Setter...
}

 

JpaMain

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

member.getFavoriteFood().add("치킨");
member.getFavoriteFood().add("족발");
member.getFavoriteFood().add("피자");

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

em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

 

 

정리

  • 엔티티 타입의 특징
    • 식별자 O
    • 생명 주기 관리
    • 공유
  • 값 타입의 특징
    • 식별자 X
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전 (복사해서 사용)
    • 불변 객체로 만드는 것이 안전

 

값 타입은 정말 값 타입이라 판단될 때만 사용한다.
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다.
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.

 


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

728x90
반응형