회원 1과 회원 2가 같은 city를 갖고 있을 때 회원 1의 city를 바꾸면 회원 2의 city도 바뀐다.
public class JpaMain {
public static void main(String[] args) {
// 같은 Address 값을 세팅한다.
Address address = new Address("city", "street", "10012");
Member member = new Member();
member.setUsername("member1");
member.setAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("member2");
member2.setAddress(address);
em.persist(member2);
tx.commit();
}
}
일단 같은 주소를 회원 1, 2에게 넣어준다.
public class JpaMain {
public static void main(String[] args) {
Address address = new Address("city", "street", "10012");
Member member = new Member();
member.setUsername("member1");
member.setAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("member2");
member2.setAddress(address);
em.persist(member2);
// 첫번째 멤버의 주소만 변경하려고 한다.
member.getAddress().setCity("new City");
tx.commit();
}
}
그리고 나서 회원 1의 주소를 바꾼다면?
업데이트 쿼리가 두 번 나간 것을 볼 수 있다.
실제 데이터도 회원 1, 2 모두가 바뀌었다. 이런 버그는 찾기가 굉장히 어렵다.
값 타입의 실제 인스턴스 값을 공유하는 것은 매우 위험하다.
일부러 데이터를 공유해서 사용하고 싶다면 임베디드 타입이 아니라 Entity로 만들어서 공유해야 한다.
대신, 값(인스턴스)를 복사해서 사용해야 한다.
public class JpaMain {
public static void main(String[] args) {
Address address = new Address("city", "street", "10012");
Member member = new Member();
member.setUsername("member1");
member.setAddress(address);
em.persist(member);
// 값을 복사해서 사용한다.
Address address2 = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("member2");
member2.setAddress(address2);
em.persist(member2);
// 이제 회원 1만 update 쿼리가 나간다.
member.getAddress().setCity("new City");
tx.commit();
}
}
값을 복사해서 사용해야 의도대로 데이터가 돌아간다.
객체 타입의 한계
항상 값을 복사해서 사용하면 공유 참조 때문에 발생하는 부작용을 피할 수 있다.
그런데 임베디드 타입 등 직접 정의한 타입은 자바 기본 타입이 아니라 객체 타입이다.
primitive 같은 값 타입은 =으로 할당해도 복사해서 들어가서 두 변수를 한 번에 수정하는 사이드 이펙트가 발생하지 않는다.
객체 타입은 참조 값을 직접 가지므로 둘 다 값이 바뀌는 사이드 이펙트를 막을 방법이 없다.
즉, 객체의 공유 참조는 피할 수 없다.
public class JpaMain {
public static void main(String[] args) {
Address address = new Address("city", "street", "10012");
Member member = new Member();
member.setUsername("member1");
member.setAddress(address);
em.persist(member);
Address address2 = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("member2");
// 실수로 기존의 address를 삽입한다면?
member2.setAddress(address);
em.persist(member2);
member.getAddress().setCity("new City");
tx.commit();
}
}
만약 실수로 복사한 값 대신 이전 값을 집어넣는다면, 컴파일 단계에서 이 문제를 짚어낼 수 없다.
기본 타입은 값을 복사하기 때문에 문제가 없지만, 객체 타입은 참조를 전달하기 때문에 변경하면 둘 다 반영되는 부작용이 발생한다.
불변 객체
생성 시점 이후 절대 값을 변경할 수 없는 객체
Integer, String은 자바가 제공하는 대표적인 불변 객체다.
생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다.
객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있다.
값 타입은 불변 객체로 설계해야 한다.
public class JpaMain {
public static void main(String[] args) {
Address address = new Address("city", "street", "10012");
Member member = new Member();
member.setUsername("member1");
member.setAddress(address);
em.persist(member);
// city를 바꾸고 싶다면 setCity() 대신
// Address 객체를 새로 만들어서 통으로 갈아 끼운다.
Address newAddress = new Address("new city", address.getStreet(), address.getZipcode());
member.setAddress(newAddress);
tx.commit();
}
}