[Java] 불변 클래스로 변경 가능성을 최소화하자

Effective Java의 Item 17을 읽고 정리한 내용입니다.

 

변경 가능성을 최소화하라


불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스를 의미한다. (객체가 파괴되는 순간까지 절대 달라지지 않는다.)

 

클래스를 불변으로 만들기 위한 규칙

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다. (final 키워드 사용)
  3. 클래스의 모든 필드를 final로 선언한다.
  4. 클래스의 모든 필드를 private로 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

 

5번 규칙이 잘 이해가 되지 않아 좀 더 자세히 정리해 봤다.

 

5번 규칙이 의미하는 바는 캡슐화라는 불변성의 측면을 나타내는데 더 자세히 말하면 객체의 내부 상태가 자체 메서드를 통해서만 변경되어야 한다는 의미이다.

 

불변 객체의 관점에서, 클래스에 가변 객체(생성 후 상태를 변경할 수 있는 객체)를 참조하는 필드가 있는 경우 이 가변 객체가 클래스 외부에서 접근하거나 변경할 수 없도록 해야 한다.

 

이를 위해 복사 또는 생성자를 통해 방어적인 복사를 해야 한다.

  1. 복사: 클래스에 변경 가능한 필드가 있을 때 해당 필드에 대한 getter 메서드가 있는 경우 복사본을 반환해야 한다. 원래 객체를 반환하면 클래스 외부에서 변경할 수 있으므로 불변성이 깨지게 된다.
  2. 생성자에서 방어적인 복사: 생성자에서 직접 mutable 한 객체를 허용해서는 안 된다. 대신, 직접 사용할 수 있도록 이러한 객체의 복사본을 만들어야 한다. 이렇게 하면 원래 객체가 나중에 변경되더라도 객체의 상태가 영향을 받지 않는다.

 

예를 들어 아래와 같은 코드가 있다고 가정하자.

 

여기서 Date는 변경 가능한 객체(java.util 패키지의 Date객체)이다.

 

복사하는 상황을 보면 생성자에서 파라미터로 제공되는 Date객체를 직접 사용해서 새로운 Date객체를 생성한다. 마찬가지로 getter메서드에서 ImmutableObject객체의 내부 Date 객체를 직접 반환하지 않고 새로운 Date객체를 반환하도록 되어있다.

 

이렇게 하면 ImmutableObject클래스가 필드로 변경 가능한 객체를 갖고 있어도 항상 Immutable 한 상태로 유지된다.

 

 

이번엔 불변 클래스에 대한 에시를 살펴보자.

 

import java.util.Objects;

public final class Complex {
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    }

    public Complex plus(Complex complex) {
        return new Complex(re + complex.re, im  + complex.im);
    }

    public Complex minus(Complex complex) {
        return new Complex(re - complex.re, im - complex.im);
    }

    public Complex times(Complex complex) {
        return new Complex(
            re * complex.re - im * complex.im,
            re * complex.re + im * complex.im);
    }

    public Complex dividedBy(Complex complex) {
        double temp = complex.re * complex.re + complex.im * complex.im;
        return new Complex(
            (re * complex.re + im * complex.im) / temp,
            (im * complex.re - re * complex.im) / temp);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Complex complex = (Complex) o;
        return Double.compare(complex.re, re) == 0 && Double.compare(complex.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(re, im);
    }

    @Override
    public String toString() {
        return "Complex{" +
            "re=" + re +
            ", im=" + im +
            '}';
    }
}

위 Complex라는 클래스에서 사칙연산을 하는 메서드(plus(), minus(), times(), dividedBy())들은 인스턴스 자신은 수정하지 않고 새로운 인스턴스(new Complex)를 만들어서 사칙연산 후 반환한다.

즉, 사칙연산 메서드를 실행해도 객체의 값을 변경하지 않고 새로운 객체를 만들어서 반환한다.

 

위와 같이 불변한 객체는 생성된 시점(new)의 상태를 파괴될 때까지 그대로 간직한다는 특징이 있다.

그리고 불변 객체는 여러 스레드가 동시에 사용해도 절대 변경되지 않아 따로 동기화를 해줄 필요가 없다. (스레드 안전)

 

 

정적 팩터리 메서드를 이용한 불변 클래스


위에서 불변 클래스를 final 키워드를 이용해 불변 클래스를 만들었지만 더 유연한 방법이 있다.

바로 모든 생성자를 private 또는 package-private로 만들고 public 정적 팩터리를 제공하는 방법이다.

 

위 Complex 클래스를 public 정적 팩터리를 이용해 다시 만들어 보자.

 

public class Complex {
    private final double re;
    private final double im;

    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

		...
}

위 클래스는 public 또는 protected 한 생성자가 없기 때문에 사실상 final 한 불변 객체이다. (다른 패키지에서 이 클래스를 확장하는 게 불가능)

 

 

정리


모든 클래스를 불변으로 만들 수는 없지만 최대한 변경할 수 있는 부분을 최소한으로 줄이는 게 좋다.

그래야 객체를 예측하기 쉽고 오류 가능성을 줄일 수 있기 때문이다.

 

변경해야 하는 필드를 제외한 나머지 모든 필드를 private final으로 선언하자.