beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

9장 정리 #27

Open beadss opened 5 years ago

beadss commented 5 years ago

값 타입

이번 장은 사실상 @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 사용 방식이다.

@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>();
}

자세한 사용법은 책을 참조하자. 개인적으로는 그냥 엔티티를 만들어서 쓰는게 낫다고 느꼈다.