728x90

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

 

조회를 할 때에야 쿼리문이 나가는 것을 볼 수 있다.

 

 

@ElementCollectionfetch 속성은 기본으로 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. 정리

 

 

 

엔티티 타입의 특징

  • 식별자가 존재한다.
  • 생명 주기를 관리할 수 있다.
  • 공유가 가능하다.

 

 

값 타입의 특징

  • 식별자가 없다.
  • 생명 주기를 엔티티에 의존한다.
  • 공유하지 않는 것이 안전하다. (복사해서 사용하면 된다.)
  • 불변 객체로 만드는 것이 안전하다.
  • 식별자가 필요하고 지속해서 값을 추적, 변경해야 하면 값 타입이 아니라 엔티티로 사용해야 한다.
728x90

'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