임베디드 타입
JPA 엔티티의 속성 값으로 사용할 수 있는 타입 중 임베디드 타입이 있다.
예를 들어 Member 엔티티가 id, name, startDate, endDate, city, street, zipcode라는 속성을 갖고 있었다고 가정하자.
개발자는 startDate와 endDate는 근무기간과 관련된 속성이고 city, street, zipcode는 주소와 관련된 속성이라는 생각을 할 수 있다.
그리고 특정 논리적 개념과 관련된 속성들끼리 객체 지향적으로 관리하고 싶을 수 있다.
이 때 사용되는 것이 임베디드 타입이다.
임베디드 타입은 논리적으로 관련 있는 엔티티의 속성을 하나의 클래스에 모아둔 타입이라고 할 수 있다.
아래의 그림 처럼 임베디드 타입으로 사용될 Period, Address 클래스를 생성하고 각 클래스에 관련된 속성을 정의해둔다.
그리고 Member는 Period, Address를 속성으로 사용한다.
그 결과 Member 엔티티는 아래와 같은 구조를 갖게 된다.
임베디드 타입을 가진 엔티티는 어떤 설계의 테이블과 매핑될까?
사실 임베디드 타입은 엔티티와 테이블의 매핑에 어떠한 영향도 주지 않는다.
임베디드 타입은 그저 어플리케이션 측면에서 서로 관련성 있는 엔티티의 속성을 객체 지향적으로 다루기 위한 타입이다.
아래의 테이블 설계를 보면 임베디드 타입은 매핑에 어떠한 영향도 미치지 않으며,
임베디드 타입이 갖고있는 속성들이 그저 하나의 컬럼과 매핑되는 것을 볼 수 있다.
임베디드 타입 사용
아래의 예시 코드를 통해 임베디드 타입의 사용 방법을 알아보자.
엔티티인 Member 클래스 내부를 살펴보자.
Period, Address 타입의 속성이 정의되어 있고, @Embedded가 적용된 것을 볼 수 있다.
@Embedded는 해당 속성이 임베디드 타입이라는 것을 JPA에게 알려준다.
또 한 가지 눈여겨 볼 점은 Address 타입의 속성이 두 개라는 것과, 두 번째 Address 타입 속성에는 @AttributeOverrides를 통해 컬럼명을 재정의 해주고 있다는 것이다.
특정 임베디드 타입이 한 엔티티에서 두 번 사용되는 것은 전혀 문제가 되지 않지만, 컬럼명 중복은 주의할 필요가 있다.
현재 Member 엔티티는 city, street, zipcode라는 속성을 각각 2개씩 갖고 있는 것이라고도 볼 수 있다.
기본적으로 엔티티의 속성들은 매핑될 컬럼명을 따로 지정하지 않으면, 자신의 변수명과 같은 컬럼과 매핑된다. 그래서 두 번째 city에 대해서 컬럼명을 재정의 해주지 않으면, 2개의 city 속성이 하나의 city 컬럼과 매핑되는 오류가 발생한다.
이제 임베디드 타입으로 사용되는 Address, Period 클래스를 살펴보자.
각 클래스에 @Embeddable이 적용된 것을 볼 수 있다. 이것 또한 해당 클래스가 임베디드 타입으로 사용될 수 있는 클래스라는 것을 JPA에게 알려준다. 엔티티 내부에 @Embedded, 임베디드 타입 클래스에 @Embeddable 중 하나만 적용해도 정상 작동한다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded
private Period period;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
}) // 한 엔티티에서 동일한 임베디드타입을 여러개 사용하면 컬럼명이 중복된다. 컬럼명을 재정의 해주자.
private Address workAddress;
...
}
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// 임베디드 타입은 기본 생성자를 갖고 있어야 한다.
public Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipcode() {
return zipcode;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Address address = (Address) o;
return Objects.equals(city, address.city) && Objects.equals(street,
address.street) && Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
// 임베디드 타입은 기본 생성자를 갖고 있어야 한다.
public Period() {
}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public LocalDateTime getStartDate() {
return startDate;
}
private void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
private void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Period that = (Period) o;
return Objects.equals(getStartDate(), that.getStartDate())
&& Objects.equals(getEndDate(), that.getEndDate());
}
@Override
public int hashCode() {
return Objects.hash(getStartDate(), getEndDate());
}
}
객체 타입의 한계
객체 타입을 엔티티의 속성 타입으로 사용할 때 주의해야할 것들이 있다.
불변 객체
임베디드 타입은 객체 타입이다.
기본 타입은 = 사용 시 값 자체를 복사하지만, 객체 타입은 참조값을 대입하여 참조값을 공유하게 된다.
이렇게 참조값을 공유하게되면 데이터 변경 시에 원치 않는 부작용이 발생할 수 있다.
예를들어 Period 객체 a, b가 같은 참조값을 공유있다고 가정하자.
a의 startDate에 수정사항이 생겨서 개발자는 a.setStartDate()를 통해 값을 변경한다. 그러나 a와 b는 같은 참조값을 공유하고 있기 때문에 b의 startDate도 변경되는 불상사가 발생한다.
이러한 실수는 생각보다 빈번하게 발생할 수 있으며, 누구나 저지를 수 있다.
이것을 방지하기 위해 객체를 불변 객체로 만드는 것이 좋다.
불변 객체는 객체 생성 이후 속성 값을 변경할 수 없는 객체를 말한다. 즉 생성자로만 값을 설정할 수 있는 객체 이다.
이것을 위해서 대표적으로 2가지 방법이 있다.
첫 번째는 ***Setter() 메소드를 생성하지 않아 외부에서 값을 수정할 수 없게 하는 것이다.
두 번째는 ***Setter() 메소드를 private로 설정하여, 외부에서 호출할 수 없게 하는 것이다.
위 코드에서도 Address는 ***Setter()를 아예 정의하지 않았고, Period는 private로 설정한 것을 볼 수 있다.
객체 비교
객체 타입의 비교에는 동일성(identity) 비교와 동등성(equivalence) 비교가 있다.
동일성 비교는 ==을 통해 객체의 참조값을 비교한다.
동등성 비교는 equals()를 객체의 각 속성값들을 비교한다.
일반적으로 JPA에서는 참조값이 아닌 속성값을 비교하는 경우가 많다.
따라서 equals(), hashCode()를 적절하게 재정의하여, 해당 메소드로 비교해야한다.
위 코드에서도 Period, Address에 equals(), hashCode()가 재정의되어 있는 것을 볼 수 있다.