이번 장은 사실상 @Embeddable 에 대한 주의사항이다.
때문에 @Embeddable에 대해서만 주로 정리하고, 나머지는 생략하거나 간단한 사용법만 정리한다.
요약
JPA에는 엔티티 타입과 값 타입이 존재한다.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 그 외의 것들을 뜻한다.
값 타입에는 primitive, Wrapper(Integer), embedded, collection 등이 존재한다.
값을 다룰때는 일반적으로 참조가 아닌 복제의 형태를 따른다.
JPA에서도 마찬가지다.
primitive, Wrapper(Integer)는 애초에 복제 방식으로만 동작하게 잘 처리가 돼있지만, embedded나 collection 타입은 사용자 정의 클래스나 Collection의 구현체이므로, 참조 방식으로 동작한다는 문제가 있다.
값 타입은 엔티티 타입처럼 재사용 되는 개념이 아니기 때문에, 서로 다른 두 엔티티에서 동시에 참조됐다가는 의도치 않은 수정이 발생할 위험이 존재한다.
때문에 setter 등을 모두 제거한 불변 객체로 다루어야하고, 값을 재사용하려면 deep copy를 수행해서, 절대 참조되는 일이 없도록 해야한다.
특히 Collection의 경우에는 본 객체는 물론이고, element들을 하나하나 deep copy해야한다는 사실을 잊지 말자.
Collection은 불변이면 안된다. add, remove는 해야하니까.
Collection의 element는 당연히 불변이어야한다.
임베디드 타입
앞 장에서 다루었던 @EmbeddedId가 바로 임베디드 타입의 한가지 사용 방식이다.
이 장에서는 식별자가 아닌, 일반 컬럼으로 다루는 방식을 소개한다.
아래는 @Embeddable 사용 방식이다.
@Entity
public class Member {
@Id @GeneratedValue
public Long id;
@Embedded
@AttributeOverrides(
@AttributeOverride(name="startDate", column=@Column(name="WORK_START_DATE")),
@AttributeOverride(name="endDate", column=@Column(name="WORK_END_DATE")) // 이게 없으면 start_date, end_date로 컬럼 명이 결정됐을 것
)
private Period workPeriod;
}
@EqualsAndHashcode // lombok, 필수로 달자
@AllArgsConstructor // lombok, 우리가 생성할때는 모든 값을 생성자로만 받는다
@NoArgConstructor // lombok, jpa가 쓸 기본 생성자는 있어야 함
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
}
public class Main {
psvm() {
Period period = new Period(new Date(), new Date());
Member member1 = new Member();
member1.setPeriod(period);
Member member2 = new Member();
member2.setPeriod(period.clone());
repo.save(member1);
repo.save(member2);
...
}
}
위 예제에서 Period는 Date를 멤버로 지니고 있다.
Date는 불변 클래스가 아니므로, 엄밀히 따지면 Period에 deep copy를 위한 clone 메서드를 구현하는게 옳다.
하지만, 일반적으로 Date를 가변 방식으로 사용하는 케이스가 잘 없으므로, 이정도는 그냥 넘어가도 되지 않을까 싶다.
그 외에도 @Embeddable은 엔티티 타입도 소유할 수 있는데, 이건 또 deep copy를 하면 안된다.
엔티티는 참조의 개념이기 때문이다.(심지어 프록시라면 임의로 생성할 수가 없으므로 deep copy가 불가능하다. )
deep copy는 여러 모로 골치아픈 기능이다.
가급적이면 @Embeddable 내부는 flat하게 유지해서 deep copy가 일어나지 않도록 노력하자.
값 타입의 비교
==로 비교하지 말고 equals로 비교하자.
equals와 hashcode 메서드를 참조 값 대신 내용물을 사용하도록 override 해야한다.
귀찮으니 @EqualsAndHashcode 하나 달아주자.
값 타입 컬렉션
엔티티 타입 없이도 OneToMany 엔티티처럼 다뤄주는 타입이라고 보면 된다.
무조건적으로 CascadeType.ALL, orphanRemoval = true가 달려있는 것처럼 동작한다.
@Entity
public class Member {
@Id @GeneratedValue
public Long id;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS",
joinColumns = @JoinColumn(name = "MEMBER_ID")) // 생략하면 {엔티티이름}_{컬렉션 명}로 테이블이 생성된다. 현재 케이스에서는 Member_favoriteFoods
@Column(name="FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<String>();
}
자세한 사용법은 책을 참조하자.
개인적으로는 그냥 엔티티를 만들어서 쓰는게 낫다고 느꼈다.
값 타입
이번 장은 사실상
@Embeddable
에 대한 주의사항이다. 때문에@Embeddable
에 대해서만 주로 정리하고, 나머지는 생략하거나 간단한 사용법만 정리한다.요약
JPA에는 엔티티 타입과 값 타입이 존재한다. 엔티티 타입은
@Entity
로 정의하는 객체이고, 값 타입은 그 외의 것들을 뜻한다. 값 타입에는 primitive, Wrapper(Integer), embedded, collection 등이 존재한다.값을 다룰때는 일반적으로 참조가 아닌 복제의 형태를 따른다. JPA에서도 마찬가지다. primitive, Wrapper(Integer)는 애초에 복제 방식으로만 동작하게 잘 처리가 돼있지만, embedded나 collection 타입은 사용자 정의 클래스나 Collection의 구현체이므로, 참조 방식으로 동작한다는 문제가 있다. 값 타입은 엔티티 타입처럼 재사용 되는 개념이 아니기 때문에, 서로 다른 두 엔티티에서 동시에 참조됐다가는 의도치 않은 수정이 발생할 위험이 존재한다.
때문에 setter 등을 모두 제거한 불변 객체로 다루어야하고, 값을 재사용하려면 deep copy를 수행해서, 절대 참조되는 일이 없도록 해야한다. 특히 Collection의 경우에는 본 객체는 물론이고, element들을 하나하나 deep copy해야한다는 사실을 잊지 말자.
임베디드 타입
앞 장에서 다루었던
@EmbeddedId
가 바로 임베디드 타입의 한가지 사용 방식이다. 이 장에서는 식별자가 아닌, 일반 컬럼으로 다루는 방식을 소개한다.아래는
@Embeddable
사용 방식이다.위 예제에서 Period는 Date를 멤버로 지니고 있다. Date는 불변 클래스가 아니므로, 엄밀히 따지면 Period에 deep copy를 위한 clone 메서드를 구현하는게 옳다. 하지만, 일반적으로 Date를 가변 방식으로 사용하는 케이스가 잘 없으므로, 이정도는 그냥 넘어가도 되지 않을까 싶다.
그 외에도
@Embeddable
은 엔티티 타입도 소유할 수 있는데, 이건 또 deep copy를 하면 안된다. 엔티티는 참조의 개념이기 때문이다.(심지어 프록시라면 임의로 생성할 수가 없으므로 deep copy가 불가능하다. )deep copy는 여러 모로 골치아픈 기능이다. 가급적이면
@Embeddable
내부는 flat하게 유지해서 deep copy가 일어나지 않도록 노력하자.값 타입의 비교
==로 비교하지 말고 equals로 비교하자. equals와 hashcode 메서드를 참조 값 대신 내용물을 사용하도록 override 해야한다. 귀찮으니 @EqualsAndHashcode 하나 달아주자.
값 타입 컬렉션
엔티티 타입 없이도 OneToMany 엔티티처럼 다뤄주는 타입이라고 보면 된다. 무조건적으로 CascadeType.ALL, orphanRemoval = true가 달려있는 것처럼 동작한다.
자세한 사용법은 책을 참조하자. 개인적으로는 그냥 엔티티를 만들어서 쓰는게 낫다고 느꼈다.