본문 바로가기
Back-end/JPA 개념

19. 값 타입(2) - 문제점과 불변 객체

by devraphy 2022. 4. 15.

0. 개요

* 본 포스팅은 이전 포스팅과 이어지는 내용임을 알립니다.

 

- 이전 포스팅에서 값 타입에 대해서 알아보았다.

- 이번 포스팅에서는 값 타입을 사용할 때 발생하는 문제점과 이를 해결하기 위해 사용하는 불변 객체에 대해서 알아보자.

 

1. 값 타입을 사용하는 이유

- 이전 포스팅에서 값 타입에 대해서 배웠다.

- 굳이 값 타입을 사용하는 이유가 무엇일까 생각해볼 필요가 있다.

- 왜냐면 JPA를 사용하므로, 이미 객체지향적 프로그래밍을 달성했다고 볼 수 있기 때문이다. 

 

a) 굳이 값 타입을 사용하는 이유

- 값 타입을 사용하는 이유는 객체의 내용을 조금이라도 더욱 간소화하기 위함이다.

- 그러므로 값 타입 또한 단순하고 간단한 구조를 이뤄야 한다.

- 더불어, 값 타입을 사용함에 있어서 그 안정성이 보장되어야 한다. 

 

- 그러나 값 타입을 사용하는 과정에서 안정성의 문제가 있다.

- 이에 대해서 알아보도록 하자. 

 

2. 값 타입 사용의 문제점 - 공유 참조

a) 공유 참조의 개념

- 값 타입 중 임베디드 타입을 사용하는 경우, 다수의 Entity에서 이를 공유(= 사용)할 수 있다.

- 여기서 공유의 의미는 동일한 임베디드 객체를 사용하여 값을 설정한다는 것이다.

- 이렇게 되면 어떤 문제가 발생할까? 

 

b) 예시 코드

- 이전 포스팅에서 다음과 같은 임베디드 타입을 생성 및 적용하였다.

@Embeddable // 임베디드 값 타입을 정의한다는 어노테이션
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;
    }
    
    // Getter & Setter 생략
}
@Entity
public class Member {

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

    @Embedded
    private Address homeAddress;
    
    // Getter & Setter 생략
}

 

- 그렇다면 이 임베디드 타입을 이용해서 다음의 코드를 작성하였다고 해보자.

 

- 위의 코드를 분석해보면, 동일한 Address 객체를 사용하여 집 주소를 설정하였다.

- 그리고 member2만 도시의 주소를 수정하였다.

- 이 코드의 결과를 생각해보면 member2 객체만 도시가 Busan으로 값이 변경되어야 한다.

- 그러나 실제 실행 결과는 이와는 조금 다르다. 

 

c) 실행 결과

- 위의 코드를 실행한 후, DB를 확인해보면 결과는 다음과 같다.

 

- 분명 위에서 member2 객체에 대해서만 도시의 주소를 Busan으로 변경했다.

- 그러나 member1의 도시 주소도 Busan으로 변경된 것을 확인할 수 있다.

 

- 이러한 버그는 따로 에러가 발생하는 것이 아니므로 발견하기 매우 힘들다.

- 특히, 실무에서 이와 같은 일이 발생했다면 간담이 서늘해진다. 

 

- 왜 이와 같은 결과가 발생하는 것일까? 

 

d) 공유 참조의 원인

- 위에서 사용한 값 타입(= Address) 객체는 하나의 메모리 주소를 가리킨다.

- 즉, member2.getHomeAddress().setCity()는 member1에서 사용한 것과 동일한 Address 객체의 값을 수정한다.

- 즉, member1과 member2는 동일한 Address 객체를 사용하므로 member2에서 변경한 값을 그대로 적용하는 것이다.

 

3. 다양한 해결 방법

a) 해결 방법(1) - 값 타입 대신 Entity로 선언

- 기본적으로 값 타입 객체는 다수의 Entity와 공유하여 사용하지 않는다.

- 위의 코드처럼 값 타입 객체를 공유하여 사용하고 싶다면 값 타입이 아니라 Entity로 정의하여 사용해야 한다.

- 즉, 값 타입이 아니라 Entity로 선언하여 사용한다면 이와 같은 문제는 발생하지 않는다.

 

b) 해결 방법(2) - 인스턴스 복사 

- 값 타입을 반드시 사용해야 한다면 인스턴스 복사를 이용해 새로운 값 타입 객체를 생성하는 것을 권장한다.

- 이는 다음과 같이 작성할 수 있다.

 

- 이처럼 아예 새로운 인스턴스(= address2)를 생성하여 address1의 값을 복사해오는 방법을 사용할 수 있다.

- 그러나 이 방식은 그다지 효율적이지 않은 방식이다. 매번 다른 객체의 값을 가져와야 하기 때문이다. 

- 더불어, 이 방식은 개발자의 실수를 예방할 수 있는 필터가 존재하지 않는다.

 

 

- 자바의 primitive 타입을 사용하는 경우, 위와 같은 방식으로 새로운 객체를 생성하면 다른 메모리 주소 값을 가진다. (이전 포스팅 참고)

- 그러나 address1과 address2는 객체이므로 동일한 메모리 주소 값을 가진다.

 

- 결국 여전히 객체의 공유 참조가 발생할 수 있는 방법이다. 

- 그렇다면 이를 완벽하게 차단 및 예방할 수 있는 방법이 무엇일까?

 

4. 불변 객체(immutable Object)

a) 개념

- 위에서 언급한 값 타입 객체 사용의 문제점과 해결책을 종합해보면 다음과 같은 결론을 얻을 수 있다.

  → 애초에 값 타입 객체의 값을 변경할 수 없게 만들어야 한다. 

 

- 이처럼 값을 수정할 수 없도록 설계된 값 타입을 불변 객체라고 한다.

- 불변 객체의 경우 Setter가 존재하지 않거나 private 접근 지정자를 가진 Setter만을 가진다.

- 즉, 생성 이후에는 절대 객체의 값을 변경할 수 없다.

 

- 다음 예시 코드를 통해 어떤 구조를 가지는지 알아보자. 

- 먼저, Setter가 없는 구조다.

@Embeddable
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;
    }

    public String getCity() {return city;}
    public String getStreet() {return street;}
    public String getZipcode() {return zipcode;}
}

 

- 다음 코드는 private 접근 지정자를 사용한 Setter를 보유한 구조다.

@Embeddable
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;
    }

    public String getCity() {return city;}
    public String getStreet() {return street;}
    public String getZipcode() {return zipcode;}
    
    private void setCity(String city) {this.city = city;}
    private void setStreet(String street) {this.street = street;}
    private void setZipcode(String zipcode) {this.zipcode = zipcode;}
}

 

b) 만약 값을 바꾸고 싶다면

- 불변 객체를 사용하는 경우, 값을 바꾸고 싶다면 새로운 객체를 생성하여 통째로 값을 바꾼다.

 

c) 결론

- 이처럼 불변 객체란 Setter를 사용하지 못도록 제약을 걸어서 문제를 해결하는 방식이다.

댓글