본문 바로가기

Spring/JPA

JPA- 값 타입과 불변 객체

값 타입 공유참조의 위험성

member1, member2가 모두 Address라는 단일 값 타입 객체(임베디드 타입)를 공유함

 서로 다른 엔티티에서 동일한 값 타입을 공유하는 경우, 하나의 엔티티에서 값 타입을 변경하는 경우 이것이 다른 엔티티에서도 영향을 미치게 되는 부작용이 발생할 수 있다. 

  문제 발생 예시

Member.class

@Entity
@Getter @Setter
public class Member extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME", nullable = false)
    private String userName;

    @Embedded // @Embedded: 임베디드 타입 사용하는 곳에 사용
    private Address homeAddress;
}

JpaMain.class

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Address address = new Address("city", "street", "zipcode");
            Member member1 = new Member();
            Member member2 = new Member();

            member1.setUserName("member1");
            member2.setUserName("member2");

            // 임베디드 타입을 공유하는 경우
            member1.setHomeAddress(address);
            member2.setHomeAddress(address);

            em.persist(member1);
            em.persist(member2);

            // side effect 발생. update 쿼리가 member1, member2 모두 나감.
            member1.getHomeAddress().setCity("newCity");

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); // 오류 발생 시 롤백
            e.printStackTrace(); // 에러 내용 출력
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}

 main을 보면 member1과 member2 모두 동일한 Address 객체를 공유하는 것을 알 수 있다.

(참고로 Address는 임베디드 값타입이다.) 그리고 member1에서 Address를 변경하니 member2도 값이 변경됨을 알 수 있다. 이는 두 엔티티가 서로 같은 값타입을 공유하기 때문이다. 따라서 위와 같은 문제를 예방하기 위해선 서로 다른 값 타입을 사용해야 한다.

 

문제 해결

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Address address = new Address("city", "street", "zipcode");
            Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipCode());

            Member member1 = new Member();
            Member member2 = new Member();

            member1.setUserName("member1");
            member2.setUserName("member2");
            
            // 서로 다른 임베디드 타입 사용
            member1.setHomeAddress(address);
            member2.setHomeAddress(copyAddress);

            em.persist(member1);
            em.persist(member2);

            // 서로에게 영향을 끼치지 않음
            member1.getHomeAddress().setCity("newCity");

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); // 오류 발생 시 롤백
            e.printStackTrace(); // 에러 내용 출력
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}

 위 처럼 서로 다른 값 타입(address, copyAddress)를 사용함으로써 공유 참조 문제를 해결 할 수 있다.

 

객체 타입의 한계

 물론 위의 해결 방안에서 항상 값을 서로 다른 값 타입을 생성하면 문제를 해결할 수 있다고 했다. 하지만 이는 항상 지키기는 좀 힘들다. 잘못해서 까먹을 수 도 있고, 실수로 공유 참조를 할 수 도 있다. 근본적인 문제점을 좀 더 알아보자.

알다시피 기본타입은 CALL BY VALUE이다. 하지만 객체는 CALL BY REFERENCE이다. 객체는 call by reference라는 특성을 가지므로 위처럼 공유참조의 부작용이 발생할 수 있는 것이다. 따라서 공유참조 부작용을 해결하기 위한 근본적인 방책은 아예 객체의 값을 변경불가하게 하는 것이다. 생성시점 이후에 값을 변경이 불가한 객체를 우리는 불변 객체라고 한다.

불변 객체

  • 불변객체: 생성 시점 이후에 값이 변경 불가능한 객체
  • 생성자로만 값을 설정하고 Setter를 만들지 않는다.

 불변 객체를 만들기 위해서는 Setter를 만들지 않고 생성자로만 값을 설정하면 된다.

 

불변 객체로 설정된 Address

@Embeddable
@Getter  // Setter가 존재하지 않는 불변객체
public class Address {

    private String city;
    private String street;
    private String zipCode;

    // 임베디드 타입은 기본생성자가 필수!!
    public Address(){}

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

 일단 불변 객체로 만들어지면 나중에 값을 수정할 수 없으므로, 수정이 필요한 경우 귀찮긴 하지만 아예 새로운 객체를 만들어야 한다. 

 

결론

 값 타입은 무조건 불변 객체로 만든다.

'Spring > JPA' 카테고리의 다른 글

JPA- 값 타입 컬렉션  (0) 2023.04.30
JPA- 값 타입의 비교  (0) 2023.04.30
JPA- 임베디드 타입  (0) 2023.03.22
JPA- Cascade와 고아 객체  (0) 2023.03.21
JPA- 즉시 로딩, 지연 로딩  (0) 2023.03.16