크게 엔티티타입과 값 타입으로 나뉜다.
1) 엔티티 타입
1) 기본값 타입
2) 임베디드 타입
3) 컬렉션 값 타입
새로운 값 타입을 직접 정의할 수 있다.
주로 기본값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
ex. 회원 엔티티는 이름, 근무기간, 집 주소를 가진다.
@Embeddable: 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
기본 생성자 필수!
의미있는 메소드를 만들 수 있다.
임베디드 타입(값 타입에 포함되는 타입)을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
예로 들면
package jpabook;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
@Entity
public class Member{
@Id
@GeneratedValue //생략하면 AUTO
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@Embedded
//기간
private Period workPeriod;
//주소
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
}
[문제 원인] 컬럼 명이 중복되기 때문에 나타나는 문제
[사용법] @AttributeOverrides, 하나면 @AttributeOverride
를 사용해서
컬러 명 속성을 재정의하면 된다.
값 타입 공유 객체
임베디드 타입은 같은 값 타입을 여러 객체에서 공유하면 다양한 Side Effect(부작용)가 발생할 수 있기 때문에 위험하다. 만약 서로 다른 회원이 같은 객체를 참조할 경우 다른 회원의 정보도 변경될 수 있고, 이러한 버그는 찾아내기 어렵기 때문에 굉장히 주의해야 한다.
Address address = new Address("Old City", "Street", "12345");
member1.setAddress(address);
em.persist(member1);
member2.setAddress(address);
em.persist(member2);
member1.getAddress().setCity("New City"); // Member1의 주소를 변경하고 싶음
tx.commit(); // UPDATE 쿼리가 2번 발생해 member1과 2의 주소가 모두 변경
값 타입 복사
만약 같은 값 타입을 사용하고 싶다면 값(인스턴스)를 복사해 사용해야 한다.
Address address = new Address("Old City", "Street", "12345");
member1.setAddress(address);
em.persist(member1);
Address copyedAddress = new Address(address.getCity(), address.getStree(), address.getZipcode());
// Address copyedAddress = address.clone(); clone()메소드를 구현해 복사해도 된다!
member2.setAddress(copyedAddress); // 복사된 주소로 저장한다.
em.persist(member2);
member1.getAddress().setCity("New City"); // member1의 주소만 변경된다.
자바는 객체에 값을 대입하면 항상 참조값을 전달하기 때문에 같은 인스턴스를 조회하게 되는데, 값을 복사해 사용하면 공유 객체를 사용해 발생하는 문제점을 피할 수 있다.
값을 복사해 사용하면 복사를 하지 않고 원본 객체를 넘기는 것을 컴파일 단계에서 막을 수 없는데, 이를 해결하는 방법은 객체의 값을 수정하지 못하는 불변 객체(Immutable Object)로 설계하면 된다. 같은 객체를 참조하더라도 값을 수정할 수 없기 때문에 부작용이 발생하지 않는다.
불변 객체를 만들기 위해서는 생성자로만 값을 설정하고, setter를 만들지 않으면 된다. 래퍼 클래스와 String은 자바가 제공하는 대표적인 불변 객체이다.
값 타입 비교
자바가 제공하는 객체의 비교는 2가지가 존재
동일성(Identity) : 인스턴스의 참조 값을 비교하는 것. ==를 사용
동등성(Equivalence) : 인스턴스의 값을 비교하는 것. equals()를 사용
Address a = new Address("City", "Street", "12345");
Address b = new Address("City", "Street", "12345");
만약 a == b로 동일성을 비교하면 둘은 다른 인스턴스이므로 false가 반환된다. 때문에 값 타입을 비교하려면 equals()를 재정의해 동등성 비교를 해야 한다. 또한, equals()를 재정의하면 hashCode()도 함께 재정의하는 것이 좋다.
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollction, @CollectionTable
애노테이션을 사용하면 된다.
@Entity
public class Member {
...
@ElementCollection
@CollectionTable(
name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") // 컬럼명 지정
private Set<String> favoriteFoods = new HashSet<>();
...
}
데이터베이스 테이블은 컬럼 안에 컬렉션을 담을 수 없고, 값만 보관할 수 있다. 때문에 별도의 테이블을 추가해야 하는데, @CollectionTable을 사용해 추가한 테이블을 매핑해야 한다. 만약 @CollctionTable을 생략하면 기본 값을 사용해서 매핑하게 된다.
{엔티티 이름}_{컬렉션 이름}
Member의 addressHistory => MEMBER_ADDRESSHISTORY
매핑되는 테이블의 컬럼이 하나라면 @Column을 사용해 컬럼명을 지정할 수 있다. 또한, 테이블의 매핑 정보를 변경하고 싶다면 @AttributeOverride를 통한 재정의도 가능하다.
// INSERT 1
Member member = new Member();
Address address = new Address("city", "street", "12345");
member.setHomeAddress(address); // 임베디드 값이기 때문에 회원을 저장할 때 함께 저장
member.getFavoriteFoods().add("피자"); // INSERT 2
member.getFavoriteFoods().add("치킨"); // INSERT 3
member.getFavoriteFoods().add("콜라"); // INSERT 4
member.getAddressHistory().add(new Address("old city1", "street1", "10001")); // INSERT 5
member.getAddressHistory().add(new Address("old city2", "street2", "10002")); // INSERT 6
em.persist(member);
tx.commit();
em.persist(member) //총 6개의 INSERT 쿼리가 실행된다.
값 타입들은 Member에 소속된 값으로 라이프 사이클이 Member에 의존된다. 때문에 영속성 전이(cascade)와 고아 객체 제거(orphan remove)를 설정한 것과 동일하다.
컬렉션 값 타입도 fetch 전략을 사용할 수 있는데 기본적으로 LAZY 전략을 사용한다. @ElementCollection(fetch = FetchType.LAZY)
때문에 Member를 조회하면 Embedded값인 Address는 함께 조회되지만 컬렉션들은 아직 조회되지 않은 상태이고, 실제 사용할 때 SELECT 쿼리를 통해 가져오게 된다.
Member member = em.find(Member.class, id); // SELETE 1
Address address = member.getAddress(); // 이미 값을 가지고 있어 새로운 쿼리 발생❌
// SELECT 2
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
// 로직
}
// SELECT 3
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
}
// 1.
member.setAddress(new Address("New City", "New Street", "54321"));
// 2.
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("치킨");
favoriteFoods.add("뿌링클");
// 3.
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("old city1", "street1", "10001"));
addressHistory.add(new Address("city", "street", "12345"));
임베디드 값 타입 수정
address는 MEMBER 테이블과 매핑했기 때문에 MEMBER 테이블만 UPDATE
기본 값 타입 수정
String은 불변 객체이기 때문에 삭제하고 새로운 객체를 넣어야 한다.
컬렉션의 값만 변경해도 JPA가 변경사항을 알고 UPDATE를 실행
컬렉션 값 타입 수정
equals()와 hashCode()가 반드시 구현되어 있어야 한다.
불변 객체이기 때문에 기존에 있던 주소를 삭제하고 새로운 주소를 등록
제약 사항
엔티티는 식별자가 있어 값 변경시에 저장된 원본 데이터를 쉽게 찾아서 변경이 가능하다. 하지만, 값 타입은 식별자가 없어 값을 변경하면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.
컬렉션 값 타입의 값들은 별도의 테이블에 보관된다. 때문에 보관된 값이 변경되면 데이터베이스에 있는 원본을 찾기 어렵다.
JPA에서는 이러한 문제를 해결하기 위해 컬렉션 값 타입에 변경 사항이 발생하면 엔티티와 연관된 모든 데이터를 삭제하고 현재 컬렉션에 있는 모든 값을 데이터베이스에 다시 저장한다.
실무에서는 정말 단순한 경우가 아니라면 일대다 관계를 사용하는 것이 좋다! 일대다 관계를 위한 엔티티를 생성하고, 여기서 값타입을 사용하는 방법으로 해결할 수가 있다. 추가적으로 영속성 전이(cascade)와 고아 객체 제거(orphan remove)를 사용하면 값 타입과 유사하게 사용이 가능하다.
@Entity
public class Member {
...
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<Address> addressHistory = new ArrayList<>();
...
}
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
...
}