[JPA] 값 타입
JPA 스터디를 진행하며 작성한 글입니다.
기본값 타입
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
}
Member에서 String, int가 값 타입이다. 값 타입은 식별자 값도 없고 생명주기도 회원 엔티티에 의존하며, 회원 엔티티 인스턴스를 제거하면 값 타입도 제거된다. 그리고 값 타입은 절대 공유하면 안 된다.
임베디드 타입(복합 값 타입)
새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라고 한다. 주용한 것은 직접 정의한 임베디드 타입도 int, String처럼 값 타입이다.
@Entity
public class Member {
@Id
@GeneratedValue
priate Long id;
private String name;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
public boolean isWork(Date date){
...
}
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력만 떨어뜨린다. 대신에 Period, Address와 같은 타입이 있다면 코드가 더 명확해질 것이다. @Embeddable와 @Embedded를 사용해 임베디드 타입으로 만들 수 있다. 임베디드 타입을 사용하면 재사용이 가능하고 응집도가 높아진다. 또한 Period.isWork()처럼 의미 있는 메소드를 만들 수 있다.
참고로 임베디드 타입은 기본 생성자가 필수이다. 또한, 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명 주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML로 표현하면 컴포지션 관계가 된다.
임베디드 타입과 테이블 매핑
임베디드 타입은 데이터베이스 테이블에 아래와 같이 매핑된다.
임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하며, 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
임베디드 타입과 연관관계
임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.
위 그림의 임베디드 타입과 연관관계를 코드로 나타내면 다음과 같다.
@Entity
public class Member {
@Embedded
private Address address;
@Embedded
private PhoneNumber phoneNumber;
}
@Embeddable
public class Address {
private String street;
private String city;
private String state;
@Embedded
private Zipcode zipcode;
}
@Embeddable
public class Zipcode {
private String zip;
private String plusFour;
}
@Embeddable
public class PhoneNumber {
private String areaCode;
private String localNumber;
@ManyToOne
PhoneServiceprovider provider;
}
@Entity
public class PhoneServiceProvider {
@Id
private String name;
}
@AttributeOverride: 속성 재정의
임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다.
@Entity
public class Member {
@Id
@GeneratedValue
priate Long id;
private String name;
@Embedded
private Address homeAddress;
@Embedded
private Address companyAddress;
}
위 예제를 보면 집 주소에 회사 주소를 하나 더 추가했다. 문제는 테이블에 매핑하는 컬럼명이 중복되는 것이다. 이때 @AttributeOverride를 사용해서 매핑정보를 재정의하면 된다.
@Embedded
@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"))
})
private Address companyAddress;
다면 @AttributeOverride를 너무 많이 사용하면 엔티티 코드가 지저분해진다. 다행히도 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.
임베디드 타입과 null
임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.
값 타입과 불변 객체
값 타입 공유 참조
임베디드 타입 같은 값 타입은 어러 엔티티에서 공유하면 위험하다.
위 상황을 코드로 나타내면 다음과 같다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); // 회원 1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
회원2에 새로운 주소를 할당하려고 회원1의 주소를 그대로 참조해서 사용했다. 이 코드를 실행하면 회원1과 회원2 모두 주소가 NewCity로 변경된다. 회원1과 회원2가 같은 인스턴스를 참조하기 때문이다. 이러한 부작용을 막으려면 값을 복사해서 사용하면 된다.
값 타입 복사
값 타입의 실제 인스턴스인 값을 공유하는 대신 값을 복사해서 사용해야 한다.
위 상황을 코드로 나타내면 다음과 같다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
clone() 메소드는 자신을 복사해서 반환하도록 구현했다. 위 예제를 보면 회원1의 주소 인스턴스를 복사해서 사용한다. 이 코드를 실행하면 의도한 대로 회원2의 주소만 NewCity로 변경된다.
불변 객체
값 타입은 부작용 걱정 없이 사용할 수 있어야 한다. 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
불변 객체를 구현하는 다양한 방법이 있지만 가장 간단한 방법은 setter를 만들지 않는 것이다.
@Getter
@Embeddable
public class Address {
private String city;
protected Address() {} // JPA에서 기본 생성자는 필수다.
//생성자로 초기 값을 설정한다.
public Address(String city) {
this.city = city;
}
}
Address는 이제 불변 객체다. 값을 수정할 수 없으므로 공유해도 부작용이 발생하지 않는다. 만약 값을 수정해야 하면 새로운 객체를 생성해서 사용해야 한다.
정리하자면, 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
값 타입 비교
- 동일성 비교: 인스턴스의 참조 값을 비교하며, == 사용
- 동등성 비교: 인스턴스의 값을 비교하며, equals() 사용
값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
favoriteFoods는 기본 값 타입인 String을 컬렉션으로 가진다. 이것을 데이터베이스 테이블로 매핑해야 하는데, 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없다. 따라서 아래 그림처럼 별도의 테이블을 추가하고 @CollectionTable를 사용해서 추가한 테이블을 매핑해야 한다. addressHistory도 마찬가지다.
참고로 favoriteFoods처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다.
값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(Orpahn Remove) 기능을 가지고 있다. 또한 값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있는데 LAZY가 기본이다.
값 타입 컬렉션의 제약사항
JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스 다시 저장한다.
따라서 실무에서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 일대다 관계를 고려해야 한다.
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
참고
- 자바 ORM 표준 JPA 프로그래밍 - 김영한
'개발 > JPA' 카테고리의 다른 글
[JPA] N+1 문제 (0) | 2022.01.08 |
---|---|
[JPA] 프록시와 연관관계 정리 (0) | 2022.01.08 |
[JPA] 고급 매핑 (0) | 2021.12.21 |
[JPA] 다양한 연관관계 매핑 (0) | 2021.12.14 |
[JPA] 연관관계 매핑 기초 (0) | 2021.12.14 |
댓글