Close
Close full mode
logo만렙 개발자 키우기

(5) 값 타입 컬렉션

Git RepositoryEdit on Github
Last update: a year ago by nowwaterReading time: 2 min

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.

값 타입 컬렉션

import java.util.ArrayList;
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") // FAVORITE_FOODS 테이블에 값으로 사용되는 컬럼이 FOOD_NAME 하나 뿐이라서 @Column을 사용해 컬럼명을 지정할 수 있다.
private Set<String> favoriteFoods = new HashSet<String>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private Set<Address> addressHistory = new ArrayList<Address>();
//...
}
@Embeddable
public class Address {
@Column
private String city;
private String street;
private String zipcode;
//...
}

image

데이터베이스 테이블로 매핑 시 컬럼 안에 컬렉션을 포함할 수 없다. 따라서 별도의 테이블을 추가하고 @CollectionTable를 사용해서 추가한 테이블을 매핑해야 한다.

@CollectionTable를 생략하면 기본값을 사용해서 매핑한다.

기본값 : {엔티티 이름}_{컬렉션 속성 이름}

ex) Member 엔티티의 addressHistoryMember_addressHistory 테이블과 매핑한다.

테이블 매핑정보는 @AttributeOverride를 사용해서 재정의할 수 있다.


9.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

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

따라서 em.persist(member) 한 번 호출로 총 6번의 INSERT SQL을 실행한다. (영속성 컨텍스트 플러시할 때 SQL을 전달)

값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 필수로 갖는다.


값 타입 컬렉션도조회 시 패치 전략을 선택할 수 있는데, LAZY가 기본이다.

조회

// 1. member : 회원만 조회. 이때 임베디드 값 타입인 homeAddress도 함께 조회 => SELECT SQL 1번 호출
Member member = em.find(Member.class, 1L);
// 2. member.homeAddress
Address homeAddress = member.getHomeAddress();
// 3. member.favoriteFoods : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출
Set<String> favoriteFoods = member.getFavoriteFoods(); // LAZY
for(String favoriteFood : favoriteFoods){
System.out.println("favoriteFood = " + favoriteFood);
}
// 4. member.addressHistory : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출
List<Address> addressHistory = member.getAddressHistory(); // LAZY
addressHistory.get(0);

수정

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

1. 임베디드 값 타입 수정

  • homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다

    • 사실 Member 엔티티를 수정하는 것과 같다.

2. 기본값 타입 컬렉션 수정

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

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

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

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

엔티티는 식별자가 있어서 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경할 수 있다.

반면 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로, 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기가 어렵다.

특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 된다.

문제는 값 타입 컬렉션이다. 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관되기 때문에 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다.

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

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


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

그리고 추가적으로 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.

import java.util.ArrayList;
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
Address address;
...
}
public class Member {
...
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();
}
🚀 JPA — Previous
(4) 값 타입의 비교
Next — 🚀 JPA
(6) 정리