본문 바로가기
Java

[Effective Java3/E] Item17 - 변경 가능성을 최소화 하라

by 김노카 2024. 8. 6.

Itme 17은 불변클래스에 대한 이야기 이다.

불변 클래스란?

불변 클래스란 간단히 말해 그 인스턴스 내부 값을 수정 할 수 없는 클래스이다. 불변 인스턴스에 간직된 정보들은 객체가 파괴되는 순간까지 달라리지 않는다. String, BigInteger, BigDeciamal
클래스가 불변 클래스에 속한다. 불변 클래스는 가변 클래스 보다 설계하고 구현하고 사용하기 쉽다. 또한 오류가 생길 여지도 적고 훨씬 안전하다.

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

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
    • Setter와 같은 메소드를 만들지 않는다.
  • 클래스를 확장할 수 없도록 한다.
    • 클래스를 상속하면 메서드를 오버라이딩 하거나 추가하면 변경이 가능하도록 만들 수 있다. final 클래스로 선언하여 클래스를 상속하지 못하게 해야한다.
  • 모든 필드를 final로 선언한다.
    • Java 언어 차원에서 지원하는 final 키워드를 사용하면 변경을 제한 할 수 있다. -> 필드에 대한 수정을 막겠다는 설계자의 의도를 보여준다.
    • Java 에서는 final로 선언된 필드는 멀티스레드에도 안전하다.
  • 모든 필드를 private으로 선언한다.
    • 필드가 참조하는 가변 객체를 클라이언트에서 수정하는 일을 막아준다.
    • 참조형이 아닌 기본 타입 필드 는 public fianl로 만 선언해도 불변 객체가 되지만 아이템 15, 16를 참고하면 public으로 필드를 선언하는것은 권하지 않는다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    • 클라이언트에서 인스턴스 내에 가변 객체의 참조를 없게 해야한다. Collection 같은 가변 객체 필드에 대한 Getter가 없어야한다.
    • 생성자, 접근자, readObject 메소드 모두에게 방어적 복사를 수행해야한다.(아이템 88)

불변 클래스를 만들기 위한 규칙을 지켜 만든 Complex 클래스이다.

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 c) {
        return new Complex(re + c.re, im + c.im);
    }

    // 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
    public static Complex valueOf(double re, double im) {
        return new Complex(re, 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 (o == this)
            return true;
        if (!(o instanceof Complex))
            return false;
        Complex c = (Complex) o;

        // == 대신 compare를 사용하는 이유는 63쪽을 확인하라.
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }
    @Override public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

위 클래스의 메소드들은 객체의 값을 변경하지 않고 새로운 Complex 인스턴스를 반환한다.

이와 같이 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍 이라고한다.

메서드의 이름으로 add 와 같은 동사 대신 plus 와 같은 전치사를 사용하는 명명규칙이 존재한다

이러한 명명규칙은 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하는 의도이다.

(참고 : BigInteger 클래스는 해당 명명 규칙을 지키지 않은 불변 클래스 이다.)

불변 클래스의 장점

  • 불변 객체는 생성 시점 부터 소멸될 때 까지 변하지 않는다.
  • 불변 객체는 근본적으로 스레드에 안전하여 따로 동기화할 필요 없다.
  • 불변 객체는 안심하고 공유할 수 있다.
public static final Complex ONE = new Complex(1, 0); 
public static final Complex I = new Complex(0, 1);
  • 위와 같이 상수로 사용해 최대한 한번 만든 인스턴스를 재활용하기를 권한다.
  • 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수잇다.
  • 불변 객체는 자유롭게 공유할 수 있으며, 불변 객체끼리는 내부데이터를 공유 할 수 있다.BigInteger 클래스는 내부에서 부호와 크기를 따로 표현한다. negate()로 부호가 반대인 BigInteger 생성지 복사 과정 없이 공유한다
public class BigInteger extends Number implemtns Comparable {
    final int signum; // 부호 
    final int[] mag; // 크기 

    public BigInteger negate() { 
        return new BigInteger(this.mag, -this.signum); 
    }
}
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다. (불변 객체를 key로 하면 이점이 많다.)
    • Map이나 Set은 안에 담긴 값이 바뀌면 불변식이 허물어 지는데, 불변 객체를 사용하면 그런 걱정은 하지 않아도 된다.
  • 불변 객체는 그자치로 실패 원자성을 제공한다. (아이템 76)
    • 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.

불변 클래스의 단점

  • 값이 다르다면 반드시 독립된 새 인스턴스를 만들어 주어야 한다.

불변 클래스 단점 해결

  • 가변 동반 클래스
    • String 클래스의 StringBuilder 클래스
    • BigInteger 클래스의 MutableBigInteger 클래스
      • MutableBigInteger클래스는 BigInteger 클래스의 메서드들이 내부적으로 사용한다.

정리

  • 클래스는 꼭 필요한 경우가 아니면 불변이어야 한다.
  • 불변으로 만들 수 없는 클래스라도 가변부분을 최대한 줄이자.
  • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.