본 포스트는 김영한 님의 자바 ORM 표준 JPA 프로그래밍 강의를 토대로 작성하였습니다.
JPA에서는 크게 두 가지 타입으로 나뉜다.
엔티티 타입은 쉽게 말해 @Entity가 붙은 클래스로 만든 객체를 말한다. 즉 DB와 매핑되는 클래스들을 말한다.
값 타입은 다시 3가지로 분류될 수 있는데,
다음으로 나뉠 수 있다. 이러한 값 타입들은 엔티티 내에서 하나의 필드들로 활용되는데, 하나씩 알아보자.
자바에 존재하는 기본적인 타입들이다. primitive type과 래퍼 클래스, String 등의 보통 사용하는 타입들을 말한다.
//Member Entity
class Member{
String name;
int age;
}
위 코드처럼 Member 라는 엔티티의 필드들을 구성하는 타입 중 기본 타입에 해당하는 것들을 기본값 타입이라고 한다.
사용자가 직접 정의한 타입을 말한다. (ex Period 클래스, Address 클래스 등) @Entity가 붙지 않은 사용자가 만든 클래스들이 해당한다.
예를 들어 Member Entity가 다음과 같은 필드들을 가진다고 해보자.
//Member Entity
class Member{
@Id @GeneratedValue
Long id;
String name;
LocalDateTime startDate;
LocalDateTime endDate;
String city;
String street;
String zipcode;
}
이 필드들 중 LocalDateTime 타입들은 Period 라는 클래스로 분리하고, city, street, zipcode 필드들은 Address 라는 클래스로 분리할 수 있다. 따라서
//Member Entity
class Member{
@Id @GeneratedValue
Long id;
String name;
@Embedded
Period period;
@Embedded
Address address;
}
//Period Class
@Embeddable
class Period{
LocalDateTime startDate;
LocalDateTime endDate;
}
//Address Class
@Embeddable
class Address{
String city;
String street;
String zipcode;
}
다음과 같이 나눌 수 있다. 그러나 Address, Period는 @Entity가 붙어있지는 않으므로 엔티티 객체는 아니지만 Member 엔티티에 소속되어 사용할 수 있다.
실행 결과를 보면 다음과 같이 각각 클래스를 분리했지만 Member table에는 전부 들어와 있는 것을 알 수 있다. 이처럼 임베디드 타입을 사용한다는 것은 테이블 상 나누는 것이 아닌, 객체 관점에서만 분리되는 것임을 알 수 있다.
@Embeddable
임베디드 타입 클래스에 붙이면 된다. (위 예시 코드 참조)
@Embedded
임베디드 타입 필드에 붙이면 된다. (위 예시 코드 참조)
❗️ 참고
- 임베디드 타입 클래스는 반드시 기본 생성자가 존재해야 한다.
- 한 엔티티 내에서 같은 값 타입을 사용할 시 컬럼명이 중복되게 된다.
따라서 @AttributeOverrides, @AttributeOverride를 이용해 컬럼 명 속성을 재정의 해야 한다.- 임베디드 타입의 값이 nulldlaus 매핑한 컬럼 값은 모두 null 이다.
❗️ 주의
다음처럼 임베디드 타입의 경우 객체가 전달되기 때문에 잘못하면 하나만 바꾸고 싶은데 다른 컬럼 값도 바뀔 수 있다. 따라서 애초에 set 함수를 없애 변경 자체를 막거나 꼭 필요한 경우 객체를 복사해서 집어넣어야 한다.
자바에서는 == 연산은 원시 타입만 가능하다. 객체 타입의 경우 주소 값이 다르기 때문에 값은 값들을 가지고 있다 하더라도 다르다는 결과가 나온다. 따라서 equals 메소드와 hash 메소드를 재정의해주어야 한다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return Objects.equals(getId(), member.getId()) && Objects.equals(getName(),
member.getName()) && Objects.equals(getWorkPeriod(),
member.getWorkPeriod()) && Objects.equals(getHomeAddress(), member.getHomeAddress());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getName(), getWorkPeriod(), getHomeAddress());
}
사용자가 만든 타입들을 컬렉션에 넣어서 사용하는 타입을 말한다. (ex List<Address> 등)
예를 들어
이러한 구조를 만들 때 Member에 필드로 컬렉션 값 타입을 만들어야 한다. 그러나 실제 DB에는 위 그림과 같이 따로 테이블이 만들어져 연관관계를 맺게 된다.
그러나 일반적인 엔티티 간의 연관관계와는 다르게 ID 컬럼을 가지지 않고 모든 컬럼들을 PK로 활용하게 된다.
@ElementCollection, @CollectionTable 어노테이션을 사용한다.
//Member Entity 클래스 중 일부
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
@ElementCollection
해당 필드가 컬렉션 값 타입임을 알리는 어노테이션
@CollectionTable
컬렉션 값 타입을 위한 테이블 정보를 설정하는 어노테이션
❗️참고
컬렉션 값 타입은 영속성 전이(Cascade) + 고아 객체 제거(@OrphanRemoval) 기능을 필수로 가진다고 볼 수 있다.
그러나 값 타입 컬렉션은 식별자가 따로 존재하지 않기 때문에 추적이 쉽지 않다. 또한 변경 발생 시 모든 데이터를 지웠다 다시 저장하기를 반복하여 성능에도 좋지 않다. 따라서 이렇게 힘들게 쓸 바에는 아예 엔티티로 승격시켜서 온전한 테이블과 매핑시켜 사용하도록 하는 것이 좋다.