값 타입 - 불변 객체와 값 타입 비교(1)
"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."
www.inflearn.com/course/ORM-JPA-Basic#
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그
www.inflearn.com
* 값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다, 따라서 단순하고 안전하게 다룰 수 있어야 한다.(값 타입 이라는 것 자체가 단순하고 안전하게 만들어져 있기 때문에 값을 복사하는데 있어 신경을 안 쓰게 된다.)
값 타입 공유 참조
임베디드 타입 같은 값 타입을 여러 Entity 에서 공유하면 위험해진다.
예시 : 회원1, 회원2 가 같은 값 타입인 주소(Address)를 보는 경우, 주소 city 의 값을 NewCity 로 변경해버리면 회원1, 회원2 테이블의 주소 값이 NewCity 로 바뀌어 버린다.(side - effect 발생)
아래와 같이 코드를 작성하고 애플리케이션을 실행해보자.
- JpaMain.java
Address address = new Address("city", "street", "10000");
// member1 과 member2 가 같은 address 를 사용한다.
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
member.setWorkPeriod(new Period());
em.persist(member);
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
member2.setWorkPeriod(new Period());
em.persist(member2);
member.getHomeAddress().setCity("newCity"); // member1 의 주소만 newCity 로 바꿀 수 있을까?
위의 코드와 같이 member1, member2 의 데이터를 생성한 후 member1 의 데이터만 바꾸기 위한 코드를 작성한 다음 애플리케이션을 실행하면 아래와 같은 hibernate SQL 을 출력하는 것을 볼 수 있다.
- hibernate SQL
Hibernate: // update query 가 두 번 나왔다.
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
ZIPCODE=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
ZIPCODE=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
분명히 member1 의 주소 값만 바꾸려고 했음에도 불구하고 member2 의 주소 값 까지 바꾸는 update query 가 DB 에 전달됨 으로서 member2 의 주소 값 까지 바뀌어 버렸다.(side - effect 발생)
위와 같이 임베디드 타입을 서로 다른 Entity 끼리 공유하게끔 하는 것은 굉장히 위험할 수 있다.
만약 그럼에도 불구하고 같은 값을 공유하게끔 하고 싶다면 값 타입이 아니라 Entity 를 사용해야 한다.
(임베디드 타입인 Address 를 Entity 로 만들어서 사용해야 한다는 뜻이다.)
- 값 타입의 실제 인스턴스 값을 공유하는 것은 위험하다.
- 대신 값(인스턴스) 를 복사해서 사용할 수 있다.
-> address 라는 임베디드 타입 객체가 있다면, 해당 값을 newAddress 라는 이름으로 복사해서 사용해야 한다.
- JpaMain.java
Address address = new Address("city", "street", "10000");
// member1 과 member2 가 같은 address 를 사용하고 있다.
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
member.setWorkPeriod(new Period());
em.persist(member);
// 임베디드 타입 객체 address 값 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress); // 복사한 객체를 사용한다.
member2.setWorkPeriod(new Period());
em.persist(member2);
/*
member2 에 복사한 객체를 사용하면
member1 의 임베디드 타입 값을 변경해도
member2 의 값은 변경되지 않는다.(공유되지 않기 때문)
*/
member.getHomeAddress().setCity("newCity"); // member1 의 주소만 newCity 로 바꾸고 싶은 경우
위와 같이 코드를 작성한 후 애플리케이션을 실행해보면 아래와 같은 hibernate SQL 을 출력하는 것을 볼 수 있다.
- hibernate SQL
Hibernate: // update query 가 한번만 나오는 것을 확인할 수 있다.
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
ZIPCODE=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
그런데 값을 복사하는 방식 또한 문제가 있다.
복사를 해서 사용해야 하는데, 협업을 하는 개발자 중에서 실수로 복사를 사용하지 않고 원래 임베디드 타입 객체를 사용할 경우 값 타입의 공유 문제가 발생하게 된다.(사람 개인의 실수이기 때문에 막을 방법이 없다.)
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용(side - effect 효과) 를 피할 수 있다.
- 문제는 임베디드 타입 처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
-> 기본 타입(primitive type) 은 = 으로 값을 할당하면 복사가 되어서 넘어가기 때문에 값을 공유하는 것 자체가 불가능하다.(임베디드 타입은 기본 타입이 아닌 객체 타입이다.)
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 객체 참조 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 즉, 객체의 공유 참조를 피할 수 없다.
-> 임베디드 타입 같이 직접 정의한 타입은 모두 객체 타입이다, 그런데 객체 타입은 공유 참조 문제를 피할 수가 없다.
(= 대입을 사용하면 그냥 다 공유로 들어가버림)
* 객체 타입의 한계
아래의 두 코드를 비교해보자.
int a = 10;
int b = a;
b = 4;
Address a = new Address("Old");
Address b = a;
b.setCity("New");
첫번째 코드는 기본 타입을 다른 변수에 대입하고 있고, 두번째 코드는 객체 타입을 다른 객체 변수에 대입하고 있다.
기본 타입의 경우 다른 변수에 값을 대입하면 값을 복사하기 때문에 공유 참조 문제가 발생할 일이 없다.
그러므로 b 의 값을 바꿔도 a의 값은 10 으로 계속 유지된다.
그러나 객체 타입의 경우 다른 객체 변수에 대입을 하게 되면 값 자체가 아닌 참조 값을 전달하기 때문에 setCity 와 같은 Setter 메소드를 통해 b 의 값을 바꾸게 되면 a 의 값도 함께 바뀌게 된다.
객체 참조로 인해 a 와 b 둘 다 같은 인스턴스를 가리키고 있기 때문이다.
그렇다면 이와 같은 문제를 어떻게 해결하면 좋을까?
다음 글에서 불변 객체에 대해 알아보자.