ex) 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가짐
=> 회원 엔티티는 이름, 근무 기간, 집 주소를 가짐
workPeriod => Period // startDate: Date, endDate: Date
homeAddress => Address // city: String, street: String, zipcode: String
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
private Period workPeriod; // 근무 기간
@Embedded
private Address homeAddress; // 집 주소
}
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
// getter, setter, 기본 생성자
public boolean isWork(Date date) {
// .. 값 타입을 위한 메소드를 정의할 수 있음
}
}
@Embeddable
public class Address {
@Column(name = "city") // 매핑할 컬럼 정의 가능
private String city;
private String street;
private String zipcode;
// getter, setter, 기본 생성자
}
@Embeddable: 값 타입을 정의하는 곳에 표시@Embedded: 값 타입을 사용하는 곳에 표시Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음@AttributeOverride: 속성 재정의@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
private Address homeAddress;
@Embedded
private Address companyAddress;
// 컬럼명 중복으로 인해 오류 발생
}
@AttributeOverrides, @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;
참고) 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념
=> 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 함
Address address = new Address("city", "street", "100000");
// member와 member2는 같은 address
Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(address);
em.persist(member2);
member.getHomeAddress().setCity("NewCity"); // member의 주소지를 변경하고 싶어 Address를 수정
tx.commit();
결과
member1의 주소만 "NewCity"로 변경되길 기대했지만, member2의 주소 역시 "NewCity"로 변경되어 버림
member1과 member2가 같은 address 인스턴스를 참조하기 때문
값 타입의 실제 인스턴스인 값을 공유하는 것은 위험 => 대신 값(인스턴스)를 복사해서 사용
Address address = new Address("city", "street", "100000");
Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);
// 값을 복사해서 사용
Address newAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(newAddress);
em.persist(member2);
member.getHomeAddress().setCity("newCity");
tx.commit();
// 기본 타입
int a = 10;
int b = a; // 기본 타입은 값을 복사함
b = 4; // a: 10, b: 4
// 객체 타입
Address a = new Address("Old");
Address b = a; // 객체 타입은 참조를 전달함
b.setCity("New"); // a: New, b: New (인스턴스가 하나이기에 같이 변경됨)
Integer, String은 자바가 제공하는 대표적인 불변 객체@Embeddable
public clas Address {
private String city;
private String street;
private String zipcode;
public Address() {}
// 생성자로 초기 값을 설정함
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// Getter는 만들고 Setter는 만들지 않거나, Setter의 접근 제어자를 private으로 지정해줌
}
값을 변경해야 하는 경우
Address address = member1.getHomeAddress();
Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
member2.setHomeAddress(newAddress);
a.equals(b)를 사용해서 동등성 비교를 해야 함== 사용equals() 사용equals() 메소드를 적절하게 재정의 해야 함(주로 모든 필드 사용)
@ElementCollection, @CollectionTable 사용참고) 값 타입 컬렉션은 영속성 전이(CASCADE) + 고아 객체 제거 기능을 필수로 가짐
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded
private Address homeAddress;
@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<>();
...
}
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100000"));
member.getAddressHistory().add(new Address("old2", "street", "100000"));
em.persist(member);
tx.commit();
참고) 값 타입 컬렉션도 지연 로딩 전략을 사용함 @ElementCollection(fetch = FetchType.LAZY)
em.persist(member);
em.flush();
em.clear();
System.out.println("============= START =============");
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistory = findMember.getAddressHistory(); // LAZY
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods(); // LAZY
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
tx.commit();
// 잘못된 수정 방식
// findMember.getHomeAddress().setCity("newCity");
// 임베디드 값 타입 수정
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
// 기본값 타입 컬렉션 수정
findMember.getFavoritFoods().remove("치킨");
findMember.getFavoritFoods().add("김밥");
// 임베디드 값 타입 컬렉션 수정
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("old1", "street", "10000"));
=> 이러한 이유로 실무에선 사용하지 않는 것을 추천함
실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
영속성 전이(CASCADE) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용
ex) AddressEntity
@Entity
public class AddressEntity {
@Id @GeneratedValue
private Long id;
@Embedded
private Address address;
...
}
public class Member {
...
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();
...
}
값 타입은 정말 값 타입이라 판단될 때만 사용해야 함
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티