값 타입 공유참조의 위험성
서로 다른 엔티티에서 동일한 값 타입을 공유하는 경우, 하나의 엔티티에서 값 타입을 변경하는 경우 이것이 다른 엔티티에서도 영향을 미치게 되는 부작용이 발생할 수 있다.
문제 발생 예시
Member.class
@Entity
@Getter @Setter
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME", nullable = false)
private String userName;
@Embedded // @Embedded: 임베디드 타입 사용하는 곳에 사용
private Address homeAddress;
}
JpaMain.class
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Address address = new Address("city", "street", "zipcode");
Member member1 = new Member();
Member member2 = new Member();
member1.setUserName("member1");
member2.setUserName("member2");
// 임베디드 타입을 공유하는 경우
member1.setHomeAddress(address);
member2.setHomeAddress(address);
em.persist(member1);
em.persist(member2);
// side effect 발생. update 쿼리가 member1, member2 모두 나감.
member1.getHomeAddress().setCity("newCity");
tx.commit();
} catch (Exception e) {
tx.rollback(); // 오류 발생 시 롤백
e.printStackTrace(); // 에러 내용 출력
} finally {
//종료
em.close();
}
emf.close();
}
}
main을 보면 member1과 member2 모두 동일한 Address 객체를 공유하는 것을 알 수 있다.
(참고로 Address는 임베디드 값타입이다.) 그리고 member1에서 Address를 변경하니 member2도 값이 변경됨을 알 수 있다. 이는 두 엔티티가 서로 같은 값타입을 공유하기 때문이다. 따라서 위와 같은 문제를 예방하기 위해선 서로 다른 값 타입을 사용해야 한다.
문제 해결
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Address address = new Address("city", "street", "zipcode");
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipCode());
Member member1 = new Member();
Member member2 = new Member();
member1.setUserName("member1");
member2.setUserName("member2");
// 서로 다른 임베디드 타입 사용
member1.setHomeAddress(address);
member2.setHomeAddress(copyAddress);
em.persist(member1);
em.persist(member2);
// 서로에게 영향을 끼치지 않음
member1.getHomeAddress().setCity("newCity");
tx.commit();
} catch (Exception e) {
tx.rollback(); // 오류 발생 시 롤백
e.printStackTrace(); // 에러 내용 출력
} finally {
//종료
em.close();
}
emf.close();
}
}
위 처럼 서로 다른 값 타입(address, copyAddress)를 사용함으로써 공유 참조 문제를 해결 할 수 있다.
객체 타입의 한계
물론 위의 해결 방안에서 항상 값을 서로 다른 값 타입을 생성하면 문제를 해결할 수 있다고 했다. 하지만 이는 항상 지키기는 좀 힘들다. 잘못해서 까먹을 수 도 있고, 실수로 공유 참조를 할 수 도 있다. 근본적인 문제점을 좀 더 알아보자.
알다시피 기본타입은 CALL BY VALUE이다. 하지만 객체는 CALL BY REFERENCE이다. 객체는 call by reference라는 특성을 가지므로 위처럼 공유참조의 부작용이 발생할 수 있는 것이다. 따라서 공유참조 부작용을 해결하기 위한 근본적인 방책은 아예 객체의 값을 변경불가하게 하는 것이다. 생성시점 이후에 값을 변경이 불가한 객체를 우리는 불변 객체라고 한다.
불변 객체
- 불변객체: 생성 시점 이후에 값이 변경 불가능한 객체
- 생성자로만 값을 설정하고 Setter를 만들지 않는다.
불변 객체를 만들기 위해서는 Setter를 만들지 않고 생성자로만 값을 설정하면 된다.
불변 객체로 설정된 Address
@Embeddable
@Getter // Setter가 존재하지 않는 불변객체
public class 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;
}
}
일단 불변 객체로 만들어지면 나중에 값을 수정할 수 없으므로, 수정이 필요한 경우 귀찮긴 하지만 아예 새로운 객체를 만들어야 한다.
결론
값 타입은 무조건 불변 객체로 만든다.
'Spring > JPA' 카테고리의 다른 글
JPA- 값 타입 컬렉션 (0) | 2023.04.30 |
---|---|
JPA- 값 타입의 비교 (0) | 2023.04.30 |
JPA- 임베디드 타입 (0) | 2023.03.22 |
JPA- Cascade와 고아 객체 (0) | 2023.03.21 |
JPA- 즉시 로딩, 지연 로딩 (0) | 2023.03.16 |