본 글은 김영한님의 <자바 ORM 표준 JPA 프로그래밍>을 읽고 공부한 내용을 정리한 글입니다.
- 기본값 타입
- 임베디드 타입
- 값 타입과 불변 객체
- 값 타입의 비교
- 값 타입 컬렉션
JPA의 데이터 타입을 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다.
엔티티 타입
값 타입
@Entity
public class Member { // 엔티티 타입
@Id @GeneratedValue
private Long id;
private String name; // 값 타입
private int age; // 값 타입
새로운 값 타입을 직접 정의해서 사용한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
@Embedded Period workPeriod; // 임베디드 타입
}
@Embeddable
public class Period {
@Temporal(TeporalType.DATE) java.util.Date startDate;
...
}
임베디드 타입 어노테이션
@Embeddable
: 값 타입을 정의하는 곳에 표시@Embedded
: 값 타입을 사용하는 곳에 표시
임베디드 타입은 엔티티의 값이기 때문에 속한 엔티티의 테이블에 매핑한다.
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.
- 엔티티를 참조한다 - 공유될 수 있으므로
- 값 타입을 포함한다 - 특정 주인에 소속되고 논리적인 개념상 공유되지 않으므로
@Entity
public class Member {
@Embedded Address address; // 임베디드 타입 포함
@Embedded PhoneNumber phoneNumber; // 임베디드 타입 포함
}
@Embeddable
public class Address {
String street;
String city;
@Embeddable Zipcode zipcode; // 임베디드 타입 포함
...
}
@Embeddable
public class Zipcode {
String zip;
...
}
@Embeddable
public class PhoneNumber {
@ManyToOne PhoneServiceProvider provider // 엔티티 참조
...
}
@Entity
public class PhoneServiceProvider {
@Id String name;
...
}
@AttributeOverride
: 임베디드 타입에 정의한 매핑 정보를 재정의한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded Address companyAddress; // 주소 추가
}
테이블에 매핑하는 컬럼명이 중복된다.
⇒ @AttributeOverride
를 이용해서 매핑 정보를 재정의한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column=@Column(name = "COMPANY_CITY")),
@AttributeOverride(name="street", column=@Column(name = "COMPANY_STREET")),
@AttributeOverride(name="zipcode", column=@Column(name = "COMPANY_ZIPCODE"))
})
Address companyAddress; // 주소 추가
}
@AttributeOverride
는 엔티티에 설정해야 한다.
❓ 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.❓
임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이다.
임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 안 된다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용 -> 회원1의 주소도 변경
member2.setHomeAddress(address);
이러한 부작용을 막으려면 값을 복사해서 사용해야 한다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
임베디드 타입은 자바의 기본 타입이 아니라 객체 타입이다.
기본 타입 - 값을 복사해서 전달
객체 타입 - 참조 값을 전달
Address a = new Address("OldCity")
Address b = a.clone(); // 항상 복사해서 넘겨야 한다.
// Address b = a // 이렇게 참조만 넘기면 공유 참조의 부작용이 발생한다.
b.setCity("NewCity");
객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있다.
하지만 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다.
객체의 공유 참조는 피할 수 없다.
⇒ 객체의 값을 수정하지 못하게 막는다.
ex) setter와 같은 수정자 메소드를 제거한다.
부작용을 원천 차단하기 위해서 값 타입은 되도록 불변 객체로 설계해야 한다.
불변 객체의 값은 조회 ⭕️, 수정 ❌
불변 객체도 결국 객체이다.
⇒ 인스턴스의 참조 값 공유를 피할 수 없다.
⇒ 하지만 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않는다.
불변 객체 만들기!
생성자로만 값을 설정하고 수정자를 만들지 않는다.
만약 값을 수정해야한다면 새로운 객체를 생성해서 사용해야 한다.
int a = 10;
int b = 10;
Address a = new Address("가", "나", "다");
Address b = new Address("가", "나", "다");
- 동일성 비교 : 인스턴스의 참조 값을 비교, == 사용
- 동등성 비교 : 인스턴스의 값을 비교, equals() 사용
값 타입을 비교할 때는 a.equals(b)
를 이용해서 동등성 비교를 해야 한다.
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection
, @CollectionTable
을 사용한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<String>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Address>();
}
@ElementCollection
: 값 타입 컬렉션@CollectionTable
: 컬렉션 테이블 매핑
관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없기 때문에 별도의 테이블을 추가해야 한다.
Member member = new Member();
// 임베디드 값 타입
member.setHomeAddress(new Address("ㄱ","ㄴ","ㄷ"));
// 기본 값 타입 컬렉션
member.getFavoriteFoods().add("r");
member.getFavoriteFoods().add("s");
// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("1"));
member.getAddressHistory().add(new Address("2"));
em.persist(member);
값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수로 가진다.
Member member = em.find(Member.class, 1L);
// 임베디드 값 타입
member.setHomeAddress(new Address("ㄱ","ㄴ","ㄷ"));
// 기본 값 타입 컬렉션
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove(r); // String 타입을 수정할 수 없다.
favoriteFoods().add("s");
// 임베디드 값 타입 컬렉션
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("1")); // 값 타입은 불변하므로 삭제 후 새로 등록한다.
addressHistory.add(new Address("2"));
값 타입 컬렉션에 보관된 값들은 별도의 테이블에 보관되므로 이 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.
이 문제를 해결하기 위해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면
따라서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 새로운 엔티티를 만들어서 일대다 관계로 설정한다.