JPA

값 타입 - 불변 객체와 값 타입 비교(2)

방구석 대학생 2020. 10. 22. 00:04

"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."

www.inflearn.com/course/ORM-JPA-Basic#

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그

www.inflearn.com

 

값 타입의 불변 객체

객체 타입은 수정할 수 없게 만들면 side - effect 효과를 원천 차단할 수 있다. 그렇기 때문에 값 타입은 불변 객체(immutable object) 로 설계해야 한다.

- 여기서 불변 객체란 생성 시점 이후 절대 값을 변경할 수 없는 객체를 말한다.

- 생성자로만 값을 설정하고 수정자(Setter) 를 만들지 않으면 된다.

- 참고로 Integer, String 은 자바가 제공하는 대표적인 불변 객체이다.

 

예를 들면 임베디드 타입으로 만들어진 Address 클래스 같은 곳에서 Setter 메소드를 모두 지워버리면 Address 타입의 객체는 불변 객체로 생성될 수 있다.

(아니면 Setter 메소드를 public 이 아닌 private 으로 만들어서 접근을 못하게 막아버리면 된다. - 클래스 내부적으로만 Setter 메소드 사용)

불변 이라는 작은 제약으로 side - effect 라는 큰 재앙을 막을 수 있는 것이다.

 

그렇다면 값을 바꾸고 싶다면 어떻게 해야 할까?

값을 공유하는 객체들이 존재하지 않아서 해당 임베디드 타입 객체를 사용하는 데이터 하나만 존재한다면 값을 변경해도 공유 참조 문제가 발생하지 않겠지만, Setter 메소드가 없는 경우 딱 하나만 존재하는 데이터 조차 값을 변경해 줄 수 없게 된다.

 

이럴 경우 Setter 메소드 없이 값을 변경하려면 그냥 객체 자체를 새로 만들어버리면 된다.

- 값 타입을 복사할 때와 같이 객체를 새로 만들면서, 생성자에서 부터 변경하고 싶은 값으로 바꿔서 생성해주면 된다.

- JpaMain.java

// Setter 메소드가 생성되지 않아 Address 클래스가 불변 객체로 만들어진 경우, 값을 변경하는 법
Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);
em.persist(member);

 

애플리케이션을 실행하면 아래와 같은 hibernate SQL 을 출력하는 것을 확인 할 수 있다.

Hibernate: // update query 가 한번만 나온다.
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            ZIPCODE=?,
            USERNAME=?,
            endDate=?,
            startDate=? 
        where
            MEMBER_ID=?

 

임베디드 타입의 객체를 공유하는 객체 없이 딱 하나의 Entity 만 해당 객체를 사용하고 있다면, 값 타입 복사를 활용하다가 실수로 복사한 객체가 아닌 원본 객체를 직접 대입한다고 해도 공유 참조 문제가 발생하지 않는다.

(실수로 원본 객체를 그대로 대입하였다고 하더라도 그냥 다시 복사본 객체를 만들어서 대입한 객체를 바꿔주면 된다. 이론적으로 값 자체를 통으로 갈아끼우게 되는 것이다.)

 

 

 

그렇다면 값 타입 간의 비교를 할 때는 어떻게 해야 할까?

값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 한다.

 

값 타입을 비교할 땐 동일성(identity) 비교와 동등성(equivalence) 비교를 구분해서 사용해야 한다.

- 동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 를 사용한다.

- 동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals() 를 사용한다.

- 값 타입은 a.equals(b) 를 사용해서 동등성 비교를 해야 한다.

- 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용) 해야 한다.

 

아래의 코드를 작성한 후 애플리케이션을 실행해보자.

- ValueMain.java

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

 

위와 같이 코드를 작성한 후 애플리케이션을 실행해보면 아래와 같은 결과를 출력하는 것을 알 수 있다.

address1 == address2 : false
address1 equals address2 : false

 

지금 당장은 둘 다 false 가 출력되는 것이 맞다.

왜? equals() 메소드의 기본 비교가 == 이기 때문이다.

그렇기 때문에 값 타입의 동등성 비교를 정확히 하기 위해서는 equals() 메소드를 오버라이드 해와야 한다.

- Address.java

@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Address address = (Address) o;
		return Objects.equals(city, address.city) &&
				Objects.equals(street, address.street) &&
				Objects.equals(zipcode, address.zipcode);
		// 각 임베디드 타입 클래스의 필드들을 Objects.equals() 를 통해 모두 비교한다. 
	}

	@Override
	public int hashCode() { // equals 를 구현하면 거기에 맞게 hashCode 또한 같이 구현해줘야 한다.
    				// 그래야 HashMap 과 같은 자바 컬렉션에서 equals 를 효율적으로 사용할 수 있기 때문이다.
		return Objects.hash(city, street, zipcode);
	}

 

위와 같이 equals() 메소드를 오버라이드 한 후 다시 애플리케이션을 실행해보면 아래와 같은 결과를 얻을 수 있다.

address1 == address2 : false
address1 equals address2 : true

 

 

 

다음 글 부터는 자바 컬렉션 에서의 값 타입 사용에 대해 알아보자.