01. 값 타입 컬렉션
값 타입 컬렉션
값 타입을 컬렉션에 담아서 쓰는 것을 말한다. 값 타입을 하나 이상 저장할 때 사용한다.
Member 엔티티의 favoriteFoods 변수, addressHistory 변수는 값 타입을 컬렉션으로 갖고있다.
관계형 데이터베이스는 기본적으로 컬렉션을 테이블 내에 담을 수 있는 구조가 없다.@ElementCollection
, @CollectionTable
을 사용한다. @ElementCollection
은 컬렉션 객체임을 JPA에게 알려주는 어노테이션이다. @CollectionTable
의 name 속성으로 매핑한 테이블명을 작성한다. @CollectionTable
의 joinColumns 속성은 FK를 지정하는 것이다. favoriteFoods에는 예외적으로 @Column
을 지정했다.
작성한 코드는 다음과 같다. 추가로 각 클래스에는 기본생성자, getter 및 setter를 작성한다. 코드가 길어질까봐 해당 부분은 생략했다.
import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
@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")
private List<Address> addressHistory = new ArrayList<>();
}
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
실행 결과는 다음과 같다. Member 테이블에 city, street, zipcode 필드가 나타난다.
FAVORITE_FOOD 테이블이 생성된다.
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID")
@Column(name = "FOOD_NAME")
private Set favoriteFoods = new HashSet<>();
ADDRESS 테이블이 생성된다.
@ElementCollection
@CollectionTable(name = "ADDRESS")
private List<Address> addressHistory = new ArrayList<>();
02. 값 타입 컬렉션 사용 - 저장
저장 예시 코드는 다음과 같다. JpaMain에 다음과 같이 작성한다.
try {
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.setHomeAddress(new Address("homeCity", "street", "10000")); // 로 인한 결과
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
최종적으로, 기대했던 대로 값이 잘 들어간 것을 볼 수 있다.
값 타입 컬렉션을 따로 persist하지 않고 member만 persist 했는데도 저장된다. 값 타입은 스스로 lifecycle이 없다. lifecycle이 member에 의존한다.
03. 값 타입 컬렉션 사용 - 조회
조회 예시 코드는 다음과 같다. JpaMain에 다음과 같이 작성한다.
Member 클래스에서는 Team을 제거하여 다음과 같이 작성한다. 추가로 기본 생성자, 생성자, getter, setter를 작성한다.
import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@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_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS")
private List<Address> addressHistory = new ArrayList<>();
}
JpaMain에서는 다음과 같이 작성한다.
try {
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("=====================");
Member findMember = em.find(Member.class, member.getId());
tx.commit();
}
실행 결과로 Member만 가져오는 것을 볼 수 있다. 컬렉션들은 지연로딩임을 알 수 있다.
JpaMain의 마지막에 다음 코드들을 추가하면
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
조회를 할 때에야 쿼리문이 나가는 것을 볼 수 있다.
@ElementCollection
의 fetch 속성은 기본으로 LAZY이다.
04. 값 타입 컬렉션 사용 - 수정
수정 예시 코드는 다음과 같다.findMember.getHomeAddress().setCity("newCity");
와 같이 코드를 작성해도 된다고 생각하지만, 이렇게 작성하면 안된다. 값 타입은 immutable 해야하기 때문이다. Address 자체를 새로 갈아껴야 한다.
// oldCity -> newCity
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
String은 통째로 갈아껴야 한다. update 자체를 하면 안된다.
// 치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
컬렉션만 변경을 해도 실제 DB에 쿼리가 나가면서 무엇이 변경되었는지 알고 JPA가 알아서 바꿔준다.
핵심은 값 타입 컬렉션이 Member에 의존관계를 갖는다는 것이다.
기본적으로 참조를 equals로 찾기 때문에 Address에 equals와 hashCode가 구현되어 있어야 한다.
import javax.persistence.Embeddable;
import java.util.Objects;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// 생성자, getter/setter, equals, hashCode
}
조금 더 자세히 보자. JpaMain에 다음과 같이 작성하자.
try {
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("=====================");
Member findMember = em.find(Member.class, member.getId());
Address a = findMember.getHomeAddress();
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
tx.commit();
}
delete 쿼리로 ADDRESS 테이블에 있는 모든 데이터들을 지우고 (테이블을 완전히 갈아낀다.) insert 쿼리가 2번 나간다. (old2와 newCity에 대해)
실행 결과는 다음과 같다. 원하는대로 잘 나오는 것을 볼 수 있다.
참고로 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
05. 값 타입 컬렉션의 제약사항
값 타입 컬렉션의 제약사항
값 타입은 엔티티와 다르게 식별자 개념이 없다.
값은 변경하면 추적이 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. (null 입력이 불가능하게, 중복 저장이 불가능하게)
식별자만 기본키로 쓰게 된다면 값 타입이 아니라 Entity가 된다.
기본 자료구조인 Collection 하위의 인터페이스들을 사용할 수 있다. (Set, List와 같은..)
06. 값 타입 컬렉션 대안
값 타입 컬렉션 대안
실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용한다.
영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용한다.
예시 코드는 다음과 같다. AddressEntity를 생성한다.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
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 클래스에서 이 부분을 제거하고
@ElementCollection
@CollectionTable(name = "ADDRESS")
private List<Address> addressHistory = new ArrayList<>();
다음과 같이 작성하여 값 타입으로 매핑하는게 아니라 Entity로 매핑한다. 기본 생성자, getter와 setter도 작성한다. (일대다 단방향이라고 가정한다.)
이렇게 했을 때 값 타입으로 사용했을 때 보다 활용 범위가 넓다. 실무에서 최적화 하기에도 좋다.
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List addressHistory = new ArrayList<>();
JpaMain을 다음과 같이 고치고
try {
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 AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("=====================");
Member findMember = em.find(Member.class, member.getId());
tx.commit();
}
실행 결과를 보면, address 에 ID가 부여된 것을 확인할 수 있다. MEMBER_ID는 외래키이다.
실무에서는 이 방법을 많이 쓴다.
07. 정리
엔티티 타입의 특징
- 식별자가 존재한다.
- 생명 주기를 관리할 수 있다.
- 공유가 가능하다.
값 타입의 특징
- 식별자가 없다.
- 생명 주기를 엔티티에 의존한다.
- 공유하지 않는 것이 안전하다. (복사해서 사용하면 된다.)
- 불변 객체로 만드는 것이 안전하다.
- 식별자가 필요하고 지속해서 값을 추적, 변경해야 하면 값 타입이 아니라 엔티티로 사용해야 한다.
'JPA' 카테고리의 다른 글
객체지향 쿼리 언어1 - 프로젝션 (SELECT) (0) | 2024.01.18 |
---|---|
객체지향 쿼리 언어1 - 기본 문법과 쿼리 API (0) | 2024.01.18 |
값 타입의 비교 (0) | 2024.01.16 |
값 타입과 불변 객체 (2) | 2024.01.15 |
임베디드 타입 (0) | 2024.01.15 |