JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다. 엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없다.
- 기본값 타입
> 자바 기본 타입 (ex. int, double)
> 래퍼 클래스 (ex. Integer)
- 임베디드 타입 (복합 값 타입) - JPA에서 사용자가 직접 정의한 값 타입
- 컬렉션 값 타입
9.1 기본값 타입
- 식별자 값이 없고 생명주기도 엔티티에 의존한다.
- 값 타입은 공유하면 안된다.
9.2 임베디드 타입(복합 값 타입)
- 새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라 한다. 임베디드 타입도 int, String 처럼 값 타입이다.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 임베디드 타입은 기본 생성자가 필수
- 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게(fine-graned) 매핑하는 것이 가능하다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
- 임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride 사용
@Embedded
@AttributeOvveride({
@AttribueOverride(name="city", column=@Column(name = "COMPANY_CITY")),...
)}
Address companyAddress;
- 임베디드 타입이 null이면 매핑한 컬럼 값이 모두 null이 된다.
9.3 값 타입과 불변 객체
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. 아래 코드에서 회원2의 주소만 변경되길 기대했지만 회원1의 주소도 변경되어 버린다. 공유 참조로 인해 발생하는 버거는 찾아내기 어렵다. 이런 부작용을 막으려면 값을 복사해서 사용하면 된다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeADDRESS();
address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용
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);
- 이처럼 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이라는 것이다. 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다. 객체의 공유 참조는 피할 수 없다. 따라서 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다. ex. Address 객체의 setCity() 같은 수정자 메소드를 모두 제거
Address a = new Address("Old");
Address b = a.clone(); // 항상 복사해서 넘겨야 한다.
// Address b = a; // 이렇게 참조만 넘기면 부작용이 발생할 수 있다.
b.setCity("New");
- 값 타입은 될 수 있으면 불변 객체로 만들어서 값을 수정할 수 없게 하는 것이 좋다.
9.4 값 타입의 비교
- 동일성 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성 비교: 인스턴스의 값을 비교, equlas() 사용
- 값 타입의 equals() 메소드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현한다.
- 자바에서 equlas()를 재정의하면 hashCode()도 재정의하는 것이 안전하다. 그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)이 정상 동작하지 않는다.
9.5 값 타입 컬렉션
- 값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.
@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"))
@Column(name="FOOD_NAME")
private Set<Address> addressHistory = new HashSet<Address>();
- 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(ORPAHN REMOVE) 기능을 필수로 가진다고 볼 수 있다.
- 값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있는데 LAZY가 기본이다.
- 엔티티는 식별자가 있으므로 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경할 수 있다. 반면에 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기는 어렵다.
- 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관된다. 따라서 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다. 이로 인해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다. 따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
9.6 정리
- 엔티티 타입의 특징
> 식별자(@id)가 있다.
: 엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
> 생명 주기가 있다.
: 생성하고, 영속화하고, 소멸하는 생명 주기가 있다.
: em.persist(entity)로 영속화한다. em.remove(entity)로 제거한다.
> 공유할 수 있다.
: 참조 값을 공유할 수 있다. 이것을 공유 참조라 한다.
: 예를 들어 회원 엔티티가 있다면 다른 엔티티에서 얼마든지 회원 엔티티를 참조할 수 있다.
- 값 타입의 특징
> 식별자가 없다.
> 생명 주기를 엔티티에 의존한다.
: 스스로 생명주기를 가지지 않고 엔티티에 의존한다. 의존하는 엔티티를 제거하면 같이 제거된다.
> 공유하지 않는 것이 안전하다.
: 엔티티 타입과는 다르게 공유하지 않는 것이 안전하다. 대신에 값을 복사해서 사용해야 한다.
: 오직 하나의 주인만이 관리해야 한다.
: 불변(immutable) 객체로 만드는 것이 안전하다.
'JPA > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
Chap.10 객체지향 쿼리 언어 - 2 (0) | 2021.07.05 |
---|---|
Chap.10 객체지향 쿼리 언어 - 1 (0) | 2021.06.19 |
Chap.8 프록시와 연관관계 관리 (0) | 2021.06.03 |
Chap.7 고급 매핑 (0) | 2021.05.25 |
Chap.6 다양한 연관관계 매핑 (0) | 2021.05.23 |