7 분 소요

📌 Topic

  • 💡 값 타입과 불변 객체
  • 💡 값 타입의 비교

⚡ 01. 값 타입이란?

값 타입은 복잡한 객체 세상을 조금이라도 단순화하기 위해 나온 개념. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

우리는 개발을 할 때 엔티티에 대해서는 신경을 많이 쓰지만, 값을 변경하는 것에 있어서는 크게 고민을 하지 않는다. 그 이유 자체가 값 타입 자체는 자바 내에서 안전하게 설계가 되어 있기 때문이다.

✅ 01-1. 값 타입 공유 참조

2022_03_06_값타입_공유_참조

혹시 어떤 데이터를 공유하여 사용 하려고 한다면 값 타입이 아닌 엔티티로 만들어 사용해야한다. 즉, 절대 값 타입(임베디드 타입)은 공유하여 사용하면 안된다.

  • 임베디드 타입(값 타입)은 여러 엔티티에서 공유가 가능하다.
  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 상당히 위험하다.
  • 즉, 부작용(side effect)이 발생 할 수 있다.

✅ 01-2. 임베디드 타입 공유 예제

// Member.class
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Period period;

    @Embedded
    private Address homeAddress;
}

현재 위와 같은 Member 엔티티가 있다 가정한다.

public class JpaMainTest {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Address homeAddress = new Address("city", "streert", "zipcode");

            MemberTest member = new MemberTest();
            member.setUsername("member1");
            member.setHomeAddress(homeAddress);
            em.persist(member);

            MemberTest member2 = new MemberTest();
            member2.setUsername("member2");
            member2.setHomeAddress(homeAddress);
            em.persist(member2);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

위 코드를 실행하면 member, member2 객체를 통해 INSERT 쿼리가 2번 발생된다.
또한 Address 객체를 인수값으로 넣어줬기 때문에 결과값은 다음과 같다.

2022_03_06_임베디드타입_공유_안함

그렇다면 아래와 같은 코드가 추가되면 어떻게 될지 한번 살펴보자.

public class JpaMainTest {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Address homeAddress = new Address("city", "streert", "zipcode");

            MemberTest member = new MemberTest();
            member.setUsername("member1");
            member.setHomeAddress(homeAddress);
            em.persist(member);

            MemberTest member2 = new MemberTest();
            member2.setUsername("member2");
            member2.setHomeAddress(homeAddress);
            em.persist(member2);

            // 의도 : 첫번째 member의 city만 newCity로 변경 한다 생각하고 추가
            // ⚡ 추가된 코드
            member.getHomeAddress().setCity("newCity");

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

위와 같이 member, member2의 값은 영속화 시킨 후 member 참조변수를 통해 City의 값을 변경하였다. 다음 사진을 살펴보자.

2022_03_06_임베디드타입_공유함

member 참조변수를 통해 city 변수의 값을 변경 했을 뿐인데, DB에는 두개의 값이 모두 newCity로 들어간 것을 확인 할 수 있다.

✅ 01-3. 값 타입의 복사

2022_03_06_값타입의복사

  • 값 타입의 실제 인스턴스 값을 공유하는 것은 상당히 위험하다.
  • 대신 값(인스턴스)를 복사해서 사용해야 한다.
  • 다음 예제를 한번 살펴보자.
Address address = new Address("city", "streert", "zipcode");

MemberTest member = new MemberTest();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);

// ⚡ 추가된 부분
Address copyAddress
    = new Address(address.getCity(), address.getStreet(), address.getZipcode());

MemberTest member2 = new MemberTest();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);

// 첫번째 member의 city만 newCity로 변경해야겠구나..
member.getHomeAddress().setCity("newCity");

우선 아까와는 다르게 copyAddress라는 Address 타입의 객체를 새로 생성한다. 후에 member2의 데이터를 초기화 할 때 해당 참조변수를 인수값으로 넣어준다.

2022_03_06_값타입복사_개별객체생성

CITY의 필드의 값이 각각 newCity, city로 초기화 된 것을 확인 할 수 있다.

⚡ 02. 객체 타입의 한계

🔥 02-1 객체 타입의 한계

Address address = new Address("city", "street", "10000");

Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(address);
em.persist(member);

// 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
Address copyAddress = address; // ⚡ 대입

Member member2 = new Member();
member2.setUserName("member2");
member2.setHomeAddress(member.getHomeAddress()); // ⚡ 대입
...
  1. 항상 값을 복사해서 사용하면 공유 참조로 인한 부작용을 피할 수 있다.
  2. 문제는 임베디드 타입은 자바에서 기본 타입이 아니라 객체 타입.
  3. 예를들어 자바에서 기본 타입에 값을 대입 하는 경우에는 해당 값을 복사한다.
  4. 하지만 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  5. 즉, 객체의 공유 참조는 피할 수 없다.

🔥 02-2 객체 타입의 한계

기본 타입(primitive type)

int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4;

// a = 10
// b = 4

객체 타입

Address a = new Address("Old");
Address b = a; // 객체 타입은 참조를 전달
b.setCity("New")

// a = New
// b = New
  • 임베디드 타입은 Java에서 기본 값타입이 아닌, 객체 타입에 속한다.
  • 메모리 번지수를 전달하는 것이기 때문에 값이 변한다.

💡 03. 불변 객체

지금까지 값 타입(임베디드 타입)

불변 객체란 생성 시점 이후에 절대 값을 변경할 수 없는 객체를 의미한다.

  1. 객체 타입을 수정할 수 없게 만들면 위와 같은 부작용을 원천 차단.
  2. 값 타입은 불변 객체로 설계(immutable object)로 설계해야 한다.
  3. 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다.
  4. 대표적으로 Integer, String은 Java가 제공하는 대표적인 불변 객체다.

✅ 03-1. Address 객체를 불변 객체로 만들기

수정 전

@Embeddable
public class Address {

    private String city;
    private String street;

    @Column(name = "ZIPCODE")
    private String zipcode;

    public Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}

수정 후

@Embeddable
public class Address {

    private String city;
    private String street;

    @Column(name = "ZIPCODE")
    private String zipcode;

    public Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    public String getCity() {
        return city;
    }

    private void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    private void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    private void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}
  1. 기존에 존재하였던 Setter 메소드를 전부 제거한다.
  2. Setter 메소드를 private로 선언한다.

✅ 03-2. Address의 값을 변경하고 싶은 경우는?

// 기존 값 타입(임베디드 타입)
Address address = new Address("city", "streert", "zipcode");

MemberTest member = new MemberTest();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);

// 실제 Address의 값을 변경하고 싶은경우는?
// 값을 통으로 갈아 끼워 넣어야 한다
Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);

✅ 03-3. 결론

불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
또한 기본 값 타입을 사용하는 경우에는 불변 객체로 설계를 하자! 🔥

💡 복기

값 타입을 설명하다가 갑자기 객체 타입의 한계가 나오기도 해서 정리가 안되는 부분이 존재 하였다. 다시 정리를 하면 JPA의 최상위 타입은 두 개로 구분이 되며 엔티티 타입값 타입이 존재한다.

여기서 중요한 부분은 값 타입은 단순히 값으로 사용하는 자바 기본 타입이나 자바의 객체를 의미한다. (02-1 객체 타입의 한계의 2번)

⚡ 04. 값 타입의 비교

이번 시간에는 값 타입을 어떻게 비교하는지 알아보자.

✅ 04-1. 값 타입의 비교 에시

  • 값 타입 : 인스턴스가 달라도 그 안에 값이 있으면 같은 것으로 봐야한다.
// a == b => true
int a = 10;
int b = 10;
// a == b => false
Address a = new Address("서울시");
Address b = new Address("서울시");

✅ 04-2. 값 타입의 비교

public class ValueMain {

    public static void main(String[] args) {
        int a = 10;
        int b = 10;

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

        Address address1 = new Address("city", "street", "10000");
        Address address2 = new Address("city", "street", "10000");

        System.out.println("address1 === address2 : " + (address1 == address2)); // false
    }
}

동일성, 동등성 내용은 해당 링크를 참고 하였습니다.

💡 04-2-1. 동일성

두 개의 변수가 같은 메모리 번지수를 바라보고 있을 경우, 동일성을 갖는다.

동일성은 동일하다는 뜻으로 두 개의 객체가 완전히 동일한 경우를 의미한다.
여기서 완전히 같다는 말은 두 객체가 사실상 하나의 객체로 봐도 무방하며, 주소 값이 같기 때문에 두 변수가 같은 객체를 가르키게 된다.

💡 04-2-2. 동등성

두 개의 변수가 같은 메모리를 번지수를 바라보고 있지 않아도, 내용이 같으면 동등성을 갖는다.

동등성은 동등하다는 뜻으로 두 개의 객체가 같은 정보를 가지고 있는 경우를 의미한다.
동등성은 변수가 참조하고 있는 메모리 번지가 다르더라도 내용만 같으면 동등하다고 이야기할 수 있다.

💡 04-2-3. 정리

두 객체가 할당된 메모리 주소가 같으면 동일하고, 두 객체의 내용이 같으면 동등하다고 말한다. 또한 동일성은 == 연산자를 통해 판별할 수 있고, 동등성은 equals() 연산자를 통해 판별할 수 있다.

✅ 04-2-3. 값 타입의 비교(동일성, 동등성)

primitive을 제외한 나머지는 ‘==’ 비교가 아닌 ‘equals’ 비교를 수행해야 한다.

  • 동일성(Identity) 비교
    • 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교
    • 인스턴스의 을 비교, equals() 사용
  • 값 타입(임베디드 타입)은 a.equals(b)를 사용하여 동등성 비교를 해야 한다.
  • 값 타입의 equals() 메서드를 적절하게 재정의(주로 모든 필드 사용)

✅ 04-2-4. Override equals, hashCode

@Embeddable
public class Address {

    private String city;
    private String street;

    @Column(name = "ZIPCODE")
    private String zipcode;

    public Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    //.. 중략

    // 객체간 논리적 동치성 보장, 값이 같으면 true를 반환하도록 재정의
    @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);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }

기존 Object 클래스의 equals 메소드는 자신과 인자로 들어오는(Object obj) 값을 비교하여 동일 여부를 반환한다. 즉, 참조값이 같아야 동일하다는 결과가 나온다는 말이다. 하지만 값 자체가 같은 것을 확인하기 위해서는 위와 같이 Object 클래스의 equals를 재정의하여 사용 해야한다.

참고 자료

댓글남기기