// 매핑 정보 재정의 예제 - @AttributeOverride 사용
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column
(name = "COMPANY_CITY")),
@AttributeOverride(name = "street", column = @Column
(name = "COMPANY_STREEET")),
@AttributeOverride(name = "zipcode", column = @Column
(name = "COMPANY_ZIPCODE"))
})
Address companyAddress;
}
💡 @AttributeOverrides는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 꼭 설정해야 한다.
member.setAddress(null); // null 입력
em.persist(member);
임베디드 타입인 Address의 값을 null로 셋팅하면 회원 테이블의 주소와 관련된 모든 컬럼 값이 null이 된다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
위의 코드를 작성하면서 당연히 회원2의 도시만 NewCity로 변경될거라 생각하지만, 이 경우 회원1의 주소도 같이 NewCity로 변경되게 된다.
회원1과 회원2가 같은 address 인스턴스를 참조하기 때문에 영속성 컨텍스트가 회원1, 회원2 둘 다 city 속성이 변경 된 것으로 판단해서 UPDATE SQL을 실행하기 때문이다.
💡 뭔가를 수정했는데 전혀 예상치 못한 곳에서 문제가 발생하는 것을 부작용(Side-Effect)라고 한다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
위 코드를 실행하면 회원2의 주소만 NewCity로 변경된다.
int a = 10;
int b = a; // 기본 타입은 항상 값을 복사한다.
b = 4;
// result
a = 10, b = 4
a의 값을 b에 넣고, b의 값을 4로 변경해도 a는 그대로 10이라는 원래 값을 유지한다.
자바는 객체에 값을 대입하면 항상 참조 값을 전달한다.
Address a = new Address("Old");
Address b = a; // 객체 타입은 항상 참조 값을 전달한다.
b.setCity("New");
Address b = a에서 a가 참조하는 인스턴스의 참조 값을 b에 넘겨주게 된다.
그렇게 되면 a와 b는 같은 인스턴스를 공유 참조하게 되는 것이다. b.setCity("New")의 의도는 b.city 값만
변경하는 것이겠지만 의도와 다르게 a.city의 값도 변경되게 된다.
물론 객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있지만,
복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 것이다.
💡 객체의 공유 참조는 피할 수 없으므로, 근본적인 해결책은 setter, 즉 수정자 메서드를 모두 제거하면 된다.
이렇게 하면 공유 참조를 해도 값을 변경하지 못하므로 부작용을 막을 수 있다.
// 불변 객체 구현 예제
@Embaddable
public class Address {
private String city;
protected Address() { } // JPA에서는 기본 생성자가 반드시 필요하다.
// 생성자로 초기 값 설정
public Address(String city) { this.city = city }
// 접근자(Getter)는 노출한다.
public String getCity() {
return city;
}
// 수정자(Setter)는 만들지 않는다.
}
// 불변 객체 사용 예제
Address address = member1.getHomeAddress();
// 회원1의 주소값을 조회해서 새로운 주소값을 생성
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);
자바가 제공하는 객체 비교는 2가지가 있다.
int a = 10;
int b = 10;
Address a = new Address("서울시", "종로구", "1번지");
Address b = new Address("서울시", "종로구", "1번지");
💡 equals( )를 재정의할때 hashCode( )도 같이 재정의하는 것이 안전하다.
그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)이 정상 동작하지 않기 때문이다.
// 값 타입 컬렉션 예제
@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<String>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns
= @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Address>();
// ...
}
@Embeddable
public class Address {
@Column
private String city;
private String street;
private String zipcode;
}
💡 @CollectionTable를 생략하면 기본값을 사용해서 매핑한다.
기본값 : {엔티티이름}_{컬렉션 속성이름}
// 값 타입 컬렉션 사용예제
Member member = new Member();
// 임베디드 값 타입
member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123"));
// 기본값 타입 컬렉션
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");
// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울", "강남", "123-123"));
member.getAddressHistory().add(new Address("서울", "강북", "000-000"));
em.persist(member);
💡 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
// 값 타입 컬렉션의 수정 예제
Member member = em.find(Member.class, 1L);
// 1. 임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시", "신도시", "123456"));
// 2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");
// 3. 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "기존주소", "123-123"));
addressHistory.add(new Address("새로운도시, "새로운주소", "123-456"));
위의 코드처럼 값타입 컬렉션을 수정한다고 했을때 과정은 아래와 같다.
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory =
new ArrayList<AddressEntity>();
위와 같이 코드를 작성하면 값 타입 컬렉션처럼 일대다 관계의 엔티티를 사용할 수 있다.
💡 값 타입 컬렉션을 사용할 때는 모두 삭제하고 다시 저장하는 최악의 시나리오를 고려하면서 사용해야 한다.