JPA의 데이터 타입은 크게 엔티티 타입, 값 타입으로 나누어집니다.
이 중 엔티티 타입은 그동안 우리가 계속 다뤘던 @Entity가 붙는 객체입니다. 엔티티에 대한 내용은 해당 포스트에서 확인하실 수 있습니다. 여기서는 값 타입에 대한 내용을 다루려고 하기 때문에 엔티티 타입은 따로 이야기하지 않겠습니다.
값 타입은 다시 기본값 타입, 임베디드 타입, 컬렉션 값 타입으로 나뉩니다.
이 중에서 기본값 타입은 자바 언어가 제공하는바 언어가 제공하는 기본형 타입, Wrapper 클래스, String을 말하기 때문에 여기서는 따로 설명하지 않겠습니다.
또한 컬렉션 타입도 자바의 컬렉션을 이용하려 하나 이상의 값 타입을 지정할 때 사용하는 것이므로 해당 포스트를 참조하시면 됩니다.
임베디드 타입은 기본형 타입 외에 프로그래머가 직접 정의해서 사용하는 타입을 의미합니다.
다음과 같은 학생 객체를 정의했습니다.
@Entity
public class Student {
@Id
@GenerateValue
private Long sId;
//소속 학과
private String department;
private Integer year;
//주소
private String city;
private String street;
}
위 학생 객체는 소속 학과와 주소에 대해 너무 상세한 정보(필드)들을 가지고 있는 상태입니다. 소속 학과와 주소를 각 타입으로 정의하면 객체가 좀 더 깔끔해지고 재사용성도 올라가겠죠?
이때 사용하는 것이 임베디드 타입 정의입니다. 사실 새로운 개념은 아니고 예전에 복합 키 매핑을 이야기하면서 복합 키를 위한 새로운 식별자 클래스를 정의했었죠? 그때 등장한 @Embedded가 바로 임베디드 타입을 복합 키 식별자로 사용했던 것 입니다.
임베디드 타입을 정의하기 위해서는 다음 두 가지 어노테이션을 이용합니다.
또한 반드시 기본 생성자를 작성해야 합니다.
그러면 소속 학과와 주소에 대한 임베디드 타입을 정의해보겠습니다.
@Embeddable
public class Major {
private String department;
private Integer year;
//기본 생성자 필수
}
@Embeddable
public class Address {
private String city;
private String street;
//기본 생성자 필수
}
각 임베디드 타입 내에서 메소드,
@Column을 정의할 수도 있습니다.
위와 같이 정의한 타입을 Student에 적용시키면 다음과 같습니다.
@Entity
public class Student {
@Id
@GenerateValue
private Long sId;
//소속 학과
@Embedded
private Major major;
//주소
@Embedded
private Address address;
}
임베디드 타입에NULL값이 오면 매핑된 컬럼 값들도 NULL이 옵니다.예를들어 major에 null이 들어오면 department, year 모두 null이 됩니다.
임베디드 타입에 정의한 매핑 정보를 재정의 하기 위해 @AttributeOverride를 사용합니다.
다음과 같이 복수 전공이 추가 되는 경우에 그냥 추가하면 Major 컬럼명이 중복되어 오류가 발생하기 때문에 @AttributeOverride를 사용해서 매핑 정보를 재정의합니다.
@Entity
public class Student {
@Id
@GenerateValue
private Long sId;
@Embedded
private Major mainMajor;
@Embedded
@AttributeOverrides({
@AttributeOverride(
name = "department",
column = @Column(name = "minor_department")
),
@AttributeOverride(
name = "year",
column = @Column(name = "minor_year")
)
})
private Major minorMajor;
}
임베디드 값 타입은 엔티티들이 공유하게 되면 안됩니다.
학생 A가 "컴퓨터공학"으로 입학했습니다. 그 후 학생 B가 입학해서
major.department에 "물리학" 값을 넣었더니 학생 A의 department도 "물리학"으로 변경되면 원하지 않은 동작이 발생하게 됩니다.
자바 기본형은 값을 복사하지만 임베디드 타입과 같은 값 타입은 객체 타입이기 때문에 참조 값을 사용하여 값을 조작하게 됩니다.
MyObject a = new MyObject("a");
MyObject b = a.clone();
b.setDate("b");
//위 코드의 실행 결과는 a.data = "b", b.date = "b"
위와 같은 문제점을 해결하기 위해 값 타입을 설계할 때는 불변 객체 Immutable Object로 설계하는 것이 권장됩니다.
불변 객체를 만드는 방법들에는 다음과 같은 방법들이 있습니다.
final 클래스 정의private final 필드setter 메소드 사용 금지 (생성자에서만 필드 초기화)record 사용
record는 정의하기만 해도 불변 객체를 생성할 수 있습니다.
나머지 final 클래스, private final 필드, setter 메소드 금지는 세 방식을 상황에 따라 조합하면서 불변 객체를 정의하게 됩니다. 각 방식을 단독으로 사용하는 경우는 불변 객체임을 보장할 수 없습니다.
record는 Java 14에서 추가된 문법으로 자세한 내용은 이 포스트를 참조해주세요.
또한 값 타입인
String, Wrapper 클래스는 불변 객체입니다.
값 타입은 값 자체가 아닌 참조하는 주소를 통해 동등성 비교를 수행하므로 equals()를 사용해서 비교를 수행해야합니다.
따라서 임베디드 타입같은 타입을 정의할 때는 equals(), hashCode()를 함께 재정의하는 것이 좋습니다.
컬렉션에 값 타입을 저장하고자 하는 경우 @ElementCollection, @CollectionTable을 사용합니다.
@Entity
public class Student {
@Id
@GenerateValue
private Long sId;
//소속 학과
@Embedded
private Major major;
//주소
@ElementCollection
@CollectionTable(
name = "address",
joinColumns = @JoinColumn(name = "s_id")
)
private List<Address> addressList = new ArrayList<>();
}
값 타입 컬렉션에 저장된 값들은 별도의 테이블을 갖습니다. 문제는 값 타입은 식별자를 갖지 않기 때문에 값 타입 변경이 발생하면 원본 값 타입을 찾기가 어려워집니다.
그래서 JPA(구현체)는 값 타입 컬렉션에 변경 사항이 생기면 값 타입 컬렉션이 매핑된 테이블의 모든 데이터를 삭제하고 다시 저장하는 과정을 거치게 됩니다.
이러한 추가 과정이 발생하기 때문에 데이터가 많아질 것으로 예상이 된다면 값 타입 대신 새로운 엔티티를 생성하고 일대다 연관관계를 지정하는 것이 좋습니다.