엔티티 타입(Entity Type)과 값 타입(Value Type)의 특징
- 엔티티 타입(Entity Type)
- 식별자(@Id)가 있다. 그리고 식별자로 구별할 수 있다
- 생명 주기가 있다. (생성 - 영속 - 소멸)
- 공유할 수 있다. 다른 엔티티에서 얼마든지 해당 엔티티를 참조할 수 있다.
- 값 타입(Value Type)
- 임베디드 타입
- 식별자가 없다.
- 생명주기를 엔티티에 의존한다. 즉 의존하는 엔티티가 제거되면 같이 제거된다.
- 공유하지 않는 것이 안전하다. 오직 하나의 주인만이 관리해야하며, 불변객체로 만드는 것이 안전하다.
임베디드 타입(복합 값 타입)
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded Period workPeriod; // 근무기간
@Embedded Address homeAddress; // 주소
@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"))
})
Address companyAddress;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE) Date startDate;
@Temporal(TemporalType.DATE) Date endDate;
}
@Embeddable
public class Address {
@Column(name="city")
private String city;
private String street;
private String zipcode;
}
- 임베디드 타입(=컴포넌트)은 기본 생성자가 필수 (보통 class를 만들면 선언하지 않아도 기본생성자가 컴파일하는 시점에 자동으로 만들어짐)
- 새로 정의한 값 타입들은 재사용할 수도 있고, 응집도도 아주 높다.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- @AttributeOverride: 매핑정보를 재정의 (임베디드 타입을 여러번 사용할 때 사용. 보통 같은 임베디드를 여러번 사용하는 일은 많지 않다.)
값 타입과 불변 객체
값 타입은 단순하고 안전하게 다루어야 한다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity");
member2.setHomeAddress(address);
공유 참조의 문제점이 발생하는 예제코드이다. 회원1의 주소를 참조해서 사용하여 회원2에 도시 정보를 "NewCity"로 변경하려고했으나, 같은 참조객체를 바라보고 있기때문에, 회원1도 "NewCity"로 변경이 되고 말았다.(자바의 기본타입이 아닌 객체 타입이기 때문에 발생하는 문제) 따라서 이 문제를 해결하기 위해서는 값 타입을 복사해서 사용해야 한다.
Address newAddress = address.clone(); // 객체 복사 (새로운 참조 주소값이 나옴)
(!) 자바의 기본 타입은 대입을 했을 경우에는 값을 복사하여 넘기고, 위와같이 객체를 대입했을 때는 참조를 넘긴다. 값타입을 다룰때는 이러한 특징을 유의해야한다.
- 불변객체
: 한 번 만들면 절대 변경할 수 없는 객체를 불변 객체라 한다. 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 차단할 수 있다. 따라서 값타입은 조회는 가능하지만, 수정은 할 수 없는 불변 객체로 설계하는것이 좋다.
값 타입 컬렉션
- 값 타입을 컬렉션에 담아서 쓴다. (연관관계 매핑에서 엔티티를 컬렉션으로 사용하는 것이 아니라 값 타입을 컬렉션에 쓰는 것)
- 값 타입 컬렉션은 값 타입을 하나 이상 저장할 때 사용한다.
- 컬렉션은 1:N의 개념이기 때문에 DB는 컬렉션을 하나의 테이블에 담을 수 없다. 따라서 별도의 테이블이 필요하다.
@Entity
public class Member {
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
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 {
@Column
private String city;
private String street;
private String zipcode;
}
public static main(String args[]) {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street1", "10001"));
member.getAddressHistory().add(new Address("old2", "street2", "10002"));
em.persist(member);
}
=> 값 타입 컬렉션으로 FAVORITE_FOOD와 ADDRESS 테이블이 생성되는 모습이다.
- 값 타입 컬렉션 조회시에는 지연 로딩을 기본적으로 사용
@ElementCollection(fetch = FetchType.LAZY)
값 타입 수정
Member member = em.find(Member.class, 1L);
// 1. 임베디드 값 타입 수정
// @Embeddable 되어있는 값 타입을 수정했지만 사실상 Member 테이블만 update한다.
member.setHomeAddress(new Address("새로운도시", "신도시1","123456");
// 2. 기본값 타입 컬렉션 수정
// String 타입은 수정 불가능, 탕수육을 삭제하고 치킨을 추가해주어야 함
Set<String> favoriteFoods = member.getFavoriteFoods();
member.getFavoriteFoods().remove("탕수육");
member.getFavoriteFoods().add("치킨");
// 3. 임베디드 값 타입 컬렉션 수정
// 값 타입은 불변해야한다. 따라서 기존 주소를 제거하고 새로 주소를 추가한다.
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울","기존주소", "123-123"));
addressHistory.add(new Address("새로운도시","새로운주소","000-000"));
값 타임 컬렉션의 제약사항
- 값타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경이 된다면, 추적이 어렵다.
-값타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관된다.
따라서 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본데이터를 찾기 어렵다는 문제가 있다.
(엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면된다.)
때문에, 값타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
=> 값타입 컬렉션을 매핑하는 테이블은 모든 칼럼을 묶어서 기본키를 구성해야함 (NOT NULL, 중복저장 비허용)
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아객체(orphan)제거를 사용해서 값 타입 컬렉션처럼 사용하자.
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
Address address;
}
......
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();
참고자료
- 서적 - 자바 ORM 표준 JPA 프로그래밍 제 9장 - 김영한 지음
'SpringFramework > JPA' 카테고리의 다른 글
JPA - Criteria (0) | 2021.11.19 |
---|---|
JPA - 객체지향 쿼리 언어, JPQL (0) | 2021.11.17 |
JPA - 연관관계 고급매핑 (0) | 2021.11.15 |
JPA - 연관관계 (0) | 2021.11.14 |
JPA - 엔티티 매핑 (0) | 2021.11.12 |