Study/이펙티브 자바 / / 2023. 9. 14. 21:30

[Effective Java 3E] 변경 가능성을 최소화하라

💥 개요

불변클래스란 인스턴스 내부 값을 수정할 수 없는 클래스다. 불변 인스턴스에 간직된 정보는 생성된 순간부터 파괴되는 순간까지 절대 달라지지 않습니다. 자바에서 String, 기본 타입의 박싱된 클래스, BigInteger, BigDecimal이 여기에 속합니다.

 

📃 불변 클래스를 만드는 5가지 규칙

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다. (상속한 클래스에서 객체의 상태를 변하게 만드는 상태를 막아줌, 대표적인 방법으로 final Class)
  • 모든 필드를 final로 선언한다.
  • 모든 필드를 private으로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

 

👍대표적인 불변 클래스

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

    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);

    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 (o == this)
            return true;
        if (!(o instanceof Complex))
            return false;
        Complex c = (Complex) o;

        // See page 47 to find out why we use compare instead of ==
        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)";
    }
}

위와 같은 불변 객체는 단순합니다. 모든 메서드가 불변식을 보장하고, 스레드 안전하여 따로 동기화 할 필요도 없습니다. 안심하고 공유가 가능합니다. 이는 방어적 복사도 필요없게 됩니다.

불변 객체는 안전하게 공유가 가능하니 한번 만든 인스턴스를 재활용하는 방법이 좋습니다.(Point1)의 static 상수처럼요.

 

불변 클래스의 특징

  • 불변 객체는 자유롭게 공유할 수 있고, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
  • 불변 객체는 그 자체로 실패 원자성을 제공한다. (메서드에서 예외가 발생해도 그 객체는 유효해야함)
  • 불변 클래스의 단점으로는 값이 다르면 반드시 독립된 객체로 만들어야 해서 값의 가짓수가 많다면 큰 비용을 지불해야 한다.

 

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

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);
    }
}

자신을 상속하지 못하게 하는 방법 중 가장 쉬운 방법은 final을 클래스에 붙이는 것 이지만, 더 유연한 방법으로 모든 생성자를 private 혹은 package-private으로 선언하고 정적 팩토리 메서드를 제공하는 것 입니다. 

이 방법이 최선인 경우가 많습니다. 생성자가 private이니 이 클래스를 상속하는것은 불가능하고 정적 팩터리 방식으로 유연성을 지킬 수 있습니다.(다음 릴리스 때 객체 캐싱 기능을 추가할 수 있다.)

 

💡정리

1.getter가 있다고 꼭 setter를 만들면 안됩니다. 클래스는 꼭 필요한 경우가 아니면 불변이어야 합니다. Complex같은 단순한 값 객체는 불변이어야 하고 String과 BigInteger처럼 무거운 값 객체도 불변으로 만들 수 있는지 고려해야 합니다.

2.성능 때문에 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공해야 합니다.(String과 StringBuilder, StringBuffer)

3.불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분은 최소로 줄여야 합니다. 그래야 객체의 예측이 쉬워지고 오류가 생길 가능성이 줄어듭니다.(변경할 필드를 뺀 나머지는 final)

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

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

 

❤️ 스터디 질답 정리

Q. "객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다." 에서 이점이 많다고 했는데 어떤 이점이 있는지?
A. 함수형 프로그램에서도 이점이 있는데 쉽게 사용할 수 있다.

Q. new 생성자로 계속 불변성을 지키려면 성능이슈가 있을것같은데 ?
A. 예전에는 문제가 됐지만, 요즘에는 상관이 거의 없다. 있는 경우는 클래스가 큰 경우일 것 같고, 이런 경우도 어쩔 수 없는 경우는 제외하고 클래스를 나눠야하는게 맞다. 아니라면 크게 상관이 없고 오히려 불변성을 지키는게 객체지향적이고 가독성이 높아진다.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유