본문 바로가기

JPA/값 타입

임베디드 타입과 불변 객체

임베디드 타입 공유 참조

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용이 발생할 수 있다.
어떤 부작용이 생기는지 코드를 통해 알아보겠다!
github 전체코드 주소(branch: immutableObject)

 

Student.java

package hellojpa;

import lombok.Data;

import javax.persistence.*;

@Entity
@Data
public class Student {

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int age;

    @Embedded
    private Address address;

}

 

Address.java

package hellojpa;

import lombok.Data;

import javax.persistence.Embeddable;

@Data
@Embeddable
public class Address {

    private String city;
    
    private String street;
    
    private String zipcode;

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

    public Address() {
    }
}

 

JpaMain.java - student 저장로직

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

Student codeMania = new Student();
codeMania.setName("code-mania");
codeMania.setAddress(address);
em.persist(codeMania);

Student codeLover = new Student();
codeLover.setName("code-lover");
codeLover.setAddress(address);
em.persist(codeLover);

codeMania.getAddress().setCity("newCity");

tx.commit();

 

address 객체를 만들어서 codeMania와 codeLover의 address로 설정해주고, persist()했다.
그리고 codeMania의 city를 newCity로 수정했다.
나는 codeMania의 city가 바뀌는 것을 원한다. 잘 적용됐는지 DB를 확인해보자!

 

DB 결과

DB를 확인해보면 codeMania와 codeLover의 CITY가 모두 newCity로 되어있다.
codeLover의 CITY가 newCity로 변하는 것은 원하던 결과가 아니다!
이렇게 임베디드 타입을 여러 엔티티에서 공유하는 것으로 인해 예상치 못한 부작용이 발생할 수 있다.
(address가 임베디드 타입이 아닌 기본값타입(int, String, double 등)이었다면 이런 현상은 발생하지 않았을 것이다.
임베디드 타입과 같은 객체 변수들은 객체참조값을 가지고있기때문에 이런 현상이 발생한 것이다.
현재 codeMania와 codeLover의 address는 동일한 객체참조값을 가지고 있다.
더 쉽게 말하자면 두 address가 같은 객체를 가리킨다는 뜻으로, 두 address는 완전히 같은 객체이다.
그래서 codeMania의 city를 변경했는데, codeLover의 city도 변경된 것이다.)

 

임베디드 타입 복사

위에서 살펴봤듯이 임베디드 타입 객체를 공유하는 것은 매우 위험하다.(임베디드 타입뿐만 아니라 객체 공유는 위험!)
따라서 값을 복사해서 사용해야 한다.

JpaMain.java - Student 저장 로직

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

Student codeMania = new Student();
codeMania.setName("code-mania");
codeMania.setAddress(address);
em.persist(codeMania);

Address address2 = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Student codeLover = new Student();
codeLover.setName("code-lover");
codeLover.setAddress(address2);
em.persist(codeLover);

codeMania.getAddress().setCity("newCity");

tx.commit();

 

DB 결과

이번에는 address2를 만들어서, codeLover의 address로 설정해주었다.
(adrees2는 위에 선언한 address의 값을 복사해서 만들었다.)
결과적으로 codeMania의 city만 수정된 것을 알 수 있다.
이렇게 임베디드 타입을 사용할 때는 값을 복사해서 사용해야 한다.

객체 타입의 한계

(참고: 이번 글에서 다룬 문제점들은 임베디드 타입에만 한정되지 않는다.

오늘 다뤄본 내용들은 객체 타입(클래스)이라면 모두 해당된다.)

 

항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
하지만 실수로 값을 복사해서 사용하지 않고, 공유참조를 사용할 수도 있다.
그러면 아까 봤던 부작용이 일어날 수도 있는 것이다.
그렇다고 해서 공유참조 자체를 막는 방법은 존재하지 않는다. 이는 불가능하다.
(맨 처음에 살펴봤던 공유참조 코드와 함께 생각해보면,
`codeLover.setAddress(adress);` 이 코드 자체를 막을 방법은 없다.)

 

그런데 불변 객체를 도입하면, 부작용을 걱정하지 않고 코딩할 수 있다!!

불변 객체(immutable object)

불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체를 뜻한다.
즉, 값을 수정할 수 없도록 클래스를 설계해서 부작용을 원천 차단하는 것이다.
이런 임베디드 타입 클래스는 불변 객체 클래스로 설계하는 것이 좋다.
불변 객체 클래스는 어떻게 만들까? 너무너무 쉽다!
생성자로만 값을 설정할 수 있게 하고, Setter는 제거하면 된다.
이렇게 하면 처음에 객체를 생성할 때만 값을 초기화할 수 있고, 수정은 불가능해진다.
(당연히 멤버변수는 private으로 지정해야 한다)
(Integer, String은 자바가 제공하는 대표적인 불변객체이다.)

 

Address.java

package hellojpa;

import lombok.Getter;

import javax.persistence.Embeddable;

@Getter
@Embeddable
public class Address {

    private String city;

    private String street;

    private String zipcode;

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

    public Address() {
    }
}

 

Address class를 불변 객체 class로 설계하기 위해
상단의 @Data 어노테이션을 @Getter로 바꿔줌으로써 Setter를 제거했다.

Setter에서 오류 발생

위 사진은 맨 처음에 사용했던 공유참조와 동일한 코드이다.
하지만 setter가 없어졌기때문에 setCity 메서드에서 컴파일 에러가 발생한다.
따라서 임베디드 타입 객체의 값 수정으로 인해 생기는 부작용을 걱정하지 않아도 된다.
막상 이렇게 만들고 나니 "그러면 수정은 어떻게 해야 되지?"라는 궁금증이 생길 수 있다.

//            codeMania.getAddress().setCity("newCity");
codeMania.setAddress(new Address("newCity", address.getStreet(), address.getZipcode()));

사진에서의 `codeMani.getAddress().setCity("newCity");` 코드는 위 코드처럼 바뀌어야 한다.
불변객체이기때문에 값 수정이 불가능하기에
값을 수정하려면 기존 객체에서 필요한 값만 가져와서 새로운 객체를 만들어야 한다.

 

비록 수정은 조금 불편할지 몰라도 코드의 구조를 안정적으로 가져갈 수 있기때문에
임베디드 타입에서는 불변객체를 사용하는 것이 좋다고 생각한다!!!

'JPA > 값 타입' 카테고리의 다른 글

값 타입 컬렉션을 엔티티로 승격시키기  (0) 2021.09.06
값 타입 컬렉션  (0) 2021.09.04
값 타입의 비교  (0) 2021.08.31
임베디드 타입  (0) 2021.08.28
값 타입  (0) 2021.08.28