DEV ℧ Developer Diary

[EffectiveJava] item17 - 변경 가능성을 최소화하라

변경 가능성을 최소화하라

불변 클래스

불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다.

자바 플랫폼 라이브러리에도 다양한 불변 클래스가 있다.

  1. String
  2. 기본 타입의 박싱된 클래스들
  3. BigInteger, BigDecimal

등등이 있으며, 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

불변을 만들기 위한 규칙

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다.
  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private으로 선언한다.
  5. 자신 외에는 내부 가변 컴포넌트에 접근할 수 없도록 한다.
static 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 c) {
        return new Complex(re + c.re, im + c.im);
    }

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

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

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

    @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 "(" + re + " + " + im + "i)";
    }
}

이 클래스는 복소수(실수부와 허수부로 구성된 수)를 표현한다.

해당 클래스의 특징은 함수형 프로그래밍으로 작성되었으며, 가진 특징들은 이러하다.

  1. Object의 메서드 몇개를 재정의 했다.
  2. 실수부와 허수부의 값을 반환하는 접근자 메서드(realPart와 imaginaryPart)와 사칙연산 메서드(plus, minus, times, dividedBy)를 정의했다.
  3. 사칙연산 메서드들이 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다.

불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다. 여러 스레드가 동시에 사용해도 절대 훼손 되지 않으므로, 불변 객체는 안심하고 공유할 수 있다.

해당 클래스는 불변을 설명하기 위한 예시 일뿐, 실무에서 쓸만한 수준은 못되니 참고만 하도록 하자.

예를 들어 Complex 클래스는 다음 상수들을 제공할 수 있다.

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복생성하지 않게 해주는 정적 팩터리 item01 를 제공할 수 있다. 이런 정적 팩터리를 사용하면, 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.

불변 객체를 자유롭게 공유하 수 있다는 점은 방어적 복사도 필요 없다는 결론으로 자연스럽게 이어진다. 어자피 원본과 같으니 복사해도 의미가 없다. 그러므로 불변 클래스는 clone 메서드나 복사 생성자를 제공하지 않는게 좋다.

불변 객체의 내부 데이터

불변 객체는 자유룝게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다. BigInteger 클래스는 내부에서 값의 부호(sign)와 크기(magnitude)를 따로 표현한다. 해당 내부 메서드인 negate 메서드는 크기가 같고 부호만 반대인 새로운 BigInteger를 생성하는데, 새로 만든 BigInteger 인스턴스도 원본 인스턴스가 가리키는 내부 배열을 그대로 가리킨다.

public class BigInteger extends Number implements Comparable<BigInteger> {

    /* 부호 */
    final int signum;

    /* 크기 */
    final int[] mag;

    public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);
    }
}

불변 객체의 이점

객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.

불변 객체는 그자체로 실패 원자성을 제공한다. 실패 원자성이란 메서드에서 예외가 발생 한 후에도 그객체는 메서드의 호출전과 똑같은 상태를 유지하는것을 말한다.

불변 객체의 단점

불변 객체의 단점은 값이 다르면 반드시 독립된 객체로 만들어야 한다는것이다.

BigInteger moby = ...;
moby = moby.flipBit(0);
/* flipBit 메서드 valueOf를 통해 새로운 BigInteger을 반환한다. */
public BigInteger flipBit(int n) {
    if (n < 0)
        throw new ArithmeticException("Negative bit address");

    int intNum = n >>> 5;
    int[] result = new int[Math.max(intLength(), intNum+2)];

    for (int i=0; i < result.length; i++)
    result[result.length-i-1] = getInt(i);

    result[result.length-intNum-1] ^= (1 << (n & 31));

    return valueOf(result);
}

private static BigInteger valueOf(int val[]) {
    return (val[0] > 0 ? new BigInteger(val, 1) : new BigInteger(val));
}

다만 BitSet은 BigInteger처럼 임의 길이의 비트순열을 표현하지만 BigInteger과 다르게 ‘가변’이다. BigInteger와 다르게 BitSet클래스는 원하는 비트 하나만 상수시간 안에 바꿔주는 메서드를 제공한다.

BitSet moby = ...;
moby = moby.flip(0);
public void flip(int bitIndex) {
    if (bitIndex < 0)
        throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

    int wordIndex = wordIndex(bitIndex);
    expandTo(wordIndex);

    words[wordIndex] ^= (1L << bitIndex);

    recalculateWordsInUse();
    checkInvariants();
}

불변의 가변 동반 클래스

클라이언트들이 원하는 복잡한 연산들을 정확히 예측할 수 있다면 package-private의 가변 동반 클래스만으로 충분하다.

이에 대표적인 예시가 String클래스이다. String의 가변 동반 클래스는 StringBuilder(와 구닥다리 전임자 StringBuffer)이다.

String은 불변이지만, StringBuilder와 StringBuffer는 가변으로 new 를 통해 인스턴스를 생성한다.

불변클래스를 만드는 설계 방법

  • 모든 생성자를 private, package-private으로 만들고 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);
    }
}

정리

게터(getter)가 있다고 해서 무조건 세타(setter)를 만들지는 말자. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다. 주로 entity나 클래스의 불변을 지키기 위해 lombok에서 @Getter만 선언하는 것과 같다.

한편, 모든 클래스르 불변으로 만들 수는 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 객체가 가질 수 있는 상태의 수를 줄이면 그객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다.

다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.

생성자는 불변식 설정이 모두 완료된 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.