지나공 : 지식을 나누는 공간

JPA 7-1 : 값 타입과 불변 객체, 값 타입 비교 본문

Tech/JPA

JPA 7-1 : 값 타입과 불변 객체, 값 타입 비교

해리리_ 2020. 11. 10. 21:31

(JPA7에 이어지는 내용)

이번에 알아볼 내용은,

  • 값 타입의 공유 참조와 그 부작용
  • 공유 참조 시 부작용 피하기
  • 객체 타입의 한계
  • 불변 객체
  • 값 타입의 비교

 

 

값 타입 공유 참조

  • 임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. (부작용 발생)

임베디드 값을 공유했을 때 문제 발생 가능성

부작용 주의!

Address address = new Address("city","street","10000");
//member1 생성
Member member1 = new Member();
member1.setAddress(address);
em.persist(member1);
//member2 생성
Member member2 = new Member();
member2.setAddress(address);
em.persist(member2);

tx.commit();

 

위처럼 member1, member2를 생성했다고 하자. 그러면 DB는 아래와 같을 것이다.

 

필드1)member 필드2) city 필드3) street 필드4) number
member1 city street 10000
member2 city street 10000

 

그런데 여기서 member2의 city를 변경하고자 아래와 같은 코드를 추가했다고 하자.

 

member1.getAddress().setCity("New City");

 

우리 예상은 member1의 city만 New City로 변경되고 member2는 그대로여야 할 것 같지만, 실상은 아래 모습처럼 두 레코드가 모두 변경된다.

필드1)member 필드2) city 필드3) street 필드4) number
member1 New City street 10000
member2 New City street 10000

 이런 걸 side-effect, 부작용이라고 하고, 이러한 버그는 잡기도 매우 어렵다.

 

반면에, 이게 실수가 아니라 진짜로 뭔갈 공유해서 값을 한번에 바꾸고 싶을 때도 있다.

그럴 땐 이 임베디드를 사용하기보다, Address 자체를 클래스로 만들어서 사용하는 것이 좋다.

 

어쨌든 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.

그러므로 값(인스턴스)를 복사해서 사용하는 것이 좋다. 어떻게?

 

공유 참조의 부작용 피하기

 

인스턴스(값)은 복사해서 공유하자

 

위의 그림처럼, new Address를 만들어서 이전에 만든 회원1의 address를 복사한다. 아래의 코드처럼.

 

Address address = new Address("city","street","10000");
//member1 생성
Member member1 = new Member();
member1.setAddress(address);
em.persist(member1);
//member2 생성
Member member2 = new Member();
//값 복사
Address cpAddress = new Address(address.getCity(), address.getStreet(), address.getZipCode());
member2.setAddress(cpAddress);
em.persist(member2);

tx.commit();

 

이렇게 하면 나중에 member1의 City를 변경해도 둘 다 변하지 않고 member1만 변경이 된다.

이런 방식으로 항상 값을 복사해서 사용한다면 공유참조로 인해 발생하는 부작용을 피할 수 있다.

 

하지만 한계도 존재한다.

 

객체 타입의 한계

  • 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.

자바 기본 타입(premitive)이라면 본래 대입할 때 값을 복사하지만, 임베디드 타입 같은 건 객체 타입이므로 복사가 아닌 참조값을 직접 대입할 위험이 있다. 이걸 막을 방법이 없다. 그러므로 객체 타입을 수정할 수 없도록 만들어서 부작용을 원천 차단하는 방식을 사용한다.

 

 

불변 객체

  • 객체 타입을 수정할 수 없게 만든 것이다. immutable object
  • 생성 시점 이후 절대 값을 변경할 수 없는 객체이다.
  • 생성자로만 값을 설정하고 수정자인 setter를 만들지 않으면 된다.
  • 예) Integer, String 등은 자바가 제공하는 대표적인 불변 객체임

예를 들어 위의 예시대로 Address에 대해 생각해보자면,

setter를 없애서 불변객체로 만들었으므로 member1.setCity("New City");를 한 부분에서 에러가 난다.

수정이 불가능해지는 것이다.

 

그럼 setter도 없는데 어떻게 수정하는가? 아래처럼 하면 된다.

 

//이미 member1이 있는 상태다.
Address address = new Address("Newcity",member1.getStreet(),member1.getNumber);
member1.setAddress(address);//address를 통으로 바꾸기, Address안의 한 필드는 변경할 수 없으니까.
tx.commit();

 

실무 팁 : 정말 추적하기 어려운 버그가 발생할 수 있으므로 값 타입은 다 불변으로 만들자.

 

값 타입의 비교

 

int a = 10;
int b = 10;
System.out.println("a==b: " + (a==b)); // a == b: true

Address address1 = new Address("city","street","1000");
Address address2 = new Address("city","street","1000");
System.out.println("address1 == address2 : " + (address1 == address2)); // false

 

동일성(identity) 비교와 동등성(equivalance) 비교를 구분하자!

 

  • 동일성 비교 : 인스턴스의 참조값을 비교, == 사용
  • 동등성 비교 : 인스턴스의 값을 비교, equals() 사용
  • 값 타입은 a.equals(b)를 사용해서 동등성비교를 해야 한다.

그런데,

  • 값 타입의 equals() 메소드를 적절하게 재정의해야 한다.

왜 그런지 코드로 살펴보자.

Address address1 = new Address("city","street","1000");
Address address2 = new Address("city","street","1000");
System.out.println("address1 == address2 : " + (address1 == address2)); // false
System.out.println("address1.equals(address2) : " + (address1.equals(address2))); // false

==로 비교한 것은 false가 나오는 게 당연하지만, equals()의 결과 또한 false이다.

 

왜냐하면, java.lang.Object에 있는 equals 메소드의 기본이 ==로 되어 있기 때문이다.

java.lang.Object의 equals 메소드

 

따라서 이 equals()를 Address 클래스에서 오버라이드해야 한다.

(이걸 오버라이드 하면 hashCode()도 오버라이드 해야 함)

Address 클래스에서 오버라이드한 equals 메소드

 

그러면 이젠 정말 address1.equals(address2)가 true로 반환된다.

 

 

김영한 님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리한 내용입니다.

728x90
Comments