불변 객체 - 도입

지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제이다. 하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없다. 그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아니다. 객체를 공유한다고 바로 사이드 이펙트가 발생하지는 않는다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.

앞의예를떠올려보면 a , b 는처음시점에는둘다 "서울" 이라는주소를사용해야한다.그리고이후에 b 의주소를 "부산" 으로 변경해야 한다.

Address a = new Address("서울");
Address b = a;

따라서 처음에는 b=a 와같이 "서울" 이라는 Address 인스턴스를 a , b 가 함께 사용하는 것이, 다음 코드와 같이 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적이다. 인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적이다.

Address a = new Address("서울");
Address b = new Address("서울");

여기까지는 Address b = a 와 같이 공유 참조를 사용해도 아무런 문제가 없다. 오히려 더 효율적이다.

진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생한다.

b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); //사이드 이펙트 발생
System.out.println("b = " + b);

자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없다. 기본형과 다르게 참조형인 객체는 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었다. 따라서 이것은 문제가 아니다. 문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 어디선가 변경했기 때문이다.

만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것이다.

불변 객체 도입

객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다. 앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어보자.

  package lang.immutable.address;
  public class ImmutableAddress {
  
    private final String value;
      
    public ImmutableAddress(String value) {
          this.value = value;
		}
		
		public String getValue() {
          return value;
    }
    
    @Override
    public String toString() {
          return "Address{" +
                  "value='" + value + '\\'' +
                  '}';
		}
}

내부 값이 변경되면 안된다. 따라서 value 의 필드를 final 로 선언했다. 값을 변경할 수 있는 setValue() 를 제거했다. 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.

불변 클래스를 만드는 방법은 아주 단순하다. 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 된다.

  package lang.immutable.address;
  public class RefMain2 {
			  public static void main(String[] args) {
							ImmutableAddress a = new ImmutableAddress("서울");
							ImmutableAddress b = a; //참조값 대입을 막을 수 있는 방법이 없다.
							System.out.println("a = " + a);
							System.out.println("b = " + b);

							//b.setValue("부산"); //컴파일 오류 발생
							b = new ImmutableAddress("부산");
							System.out.println("부산 -> b");
							System.out.println("a = " + a);
							System.out.println("b = " + b);

				}
}

ImmutableAddress 의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었다. 이제 ImmutableAddress 인스턴스의 값을 변경할 수 있는 방법은 없다. ImmutableAddress 를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫게 된다.

예를 들어 b.setValue("부산") 을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지한다. 따라서 어쩔 수 없이 새로운 ImmutableAddress("부산") 인스턴스를 생성해서 b 에 대입한다. 결과적으로 a , b 는 서로 다른 인스턴스를 참조하고, a 가 참조하던 ImmutableAddress 는 그대로 유지된다.