elevne's Study Note
JPA - Embedded / Collection Value Type 본문
Embedded Type
JPA 에서는 새로운 값 타입을 직접 정의해 엔티티 내에서 사용할 수 있는데, 이를 임베디드 타입이라고 한다. (임베디드 타입도 int, String 처럼 값 타입이다)
@Entity
@Data
public class User {
@Id @GeneratedValue @Column(name = "USER_ID")
private Long id;
@Embedded
Period workPeriod;
@Embedded
Address address;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType.DATE)
Date endDate;
}
@Embeddable
@Data
public class Address {
@Column(name = "city")
private String city;
private String street;
private String zipcode;
}
위와 같이 객체지향적으로 Address, Period 오브젝트를 만들어 엔티티에 넣어주면 엔티티가 더욱 의미있고 응집력 있게 변한다. 이 값 타입들은 재사용할 수 있고 응집도도 아주 높다. 또, 해당 클래스 내에 특별한 메소드를 만들어 사용해볼 수 있을 것이다. 이러한 임베디드 타입을 사용하기 위해서는 @Embeddable (값 타입을 정의하는 곳에 표시), @Embedded (값 타입을 사용하는 곳에 표시) 2 가지 애노테이션을 사용한다. 임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수도 있다.
만약 임베디드 타입에 정의한 매핑정보를 재정의하고자 한다면 엔티티에 @AttributeOverrides 를 사용하면 된다. 예를 들어 회원에게 주소가 하나 더 필요하다면 아래와 같이 작성해볼 수 있다.
@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"))
})
@Embedded
Address address;
엔티티를 영속화할 때 임베디드 타입이 null 이라면 매핑한 컬럼 값은 모두 null 이 된다.
임베디드 타입 값을 여러 엔티티에서 공유하면 위험할 수 있다고 한다. 예를 들어 2 개의 User 엔티티가 참조하는 Address 오브젝트를 사용하면, 공유참조 문제가 생겨 의도한대로 값이 변경되지 않을 수 있다. 공유참조로 인해 발생하는 버그는 찾기도 어렵다. 이렇게 뭔가 수정했는데 전혀 예상치 못한 곳에서 문제가 발생하는 것을 부작용이라고 한다. 이를 막기 위해서는 값을 복사해서 사용하면 된다.
@Test
public void sideEffectTest() {
User user1 = new User();
Address address = new Address();
address.setCity("SEOUL");
user1.setAddress(address);
Address newAddress = address.clone();
newAddress.setCity("INCHEON");
User user2 = new User();
user2.setAddress(newAddress);
}
값 타입은 부작용 걱정 없이 사용할 수 있어야 한다. 부작용이 일어나면 값 타입이라 할 수 없다. 객체를 불변으로 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변 객체로 설계한다. 불변 객체를 구현하는 다양한 방법이 있지만, 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않으면 된다. (불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다)
값 타입 컬렉션
값 타입을 하나 이상 저장하면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 애노테이션을 사용하면 된다.
@Entity
public class User2 {
@Id @GeneratedValue @Column(name = "USER_ID")
private Long id;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS", joinColumns = @JoinColumn(name = "USER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "USER_ID"))
private List<Address> addressesHistory = new ArrayList<>();
}
위 favoriteFoods 는 기본값 타입인 String 을 컬렉션으로 가진다. 이것은 데이터베이스 테이블로 매핑해야 하는데, 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없다. 그래서 별도의 테이블을 추가하고 @CollectionTable 을 사용해서 추가한 테이블을 매핑해야 한다. 그리고 favoriteFoods 처럼 값으로 사용되는 컬럼이 하나면 @Column 을 사용해서 컬럼명을 지정할 수 있다. addressHistory 는 임베디드 타입인 Address 를 컬렉션으로 가진다. 이것도 마찬가지로 별도의 테이블을 사용해야 한다. (테이블 매핑정보는 @AttributeOverrides 를 사용해 재정의할 수 있다)
엔티티는 식별자가 잇으므로 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경할 수 있지만, 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음으로 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다. 이러한 문제로 JPA 구현체들은 값 타입 컬렉션에 변경사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다. 따라서 실무에서는 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해야 한다. 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 데이터베이스 기본 키 제약조건으로 인해 컬럼에 null 을 입력할 수 없고, 같은 값을 중복하여 저장할 수 없는 제약도 있다.
이러한 문제들을 해결하려면 값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 만들어 일대다 관계로 설정한다. 추가로 영속성 전이 + 고아객체 제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
Reference:
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA 프로그래밍' 카테고리의 다른 글
JPA - Criteria (0) | 2023.07.14 |
---|---|
JPA - JPQL (0) | 2023.07.13 |
JPA - Proxy, Cascade, Orphan (0) | 2023.07.11 |
JPA 고급매핑 (0) | 2023.07.09 |
JPA N:M 관계 (0) | 2023.07.06 |