Effective Java를 읽고 정리한 정리본입니다.

📌 Item 17 : 변경 가능성을 최소화하라

🫧 불변 클래스

불변 클래스는 그 인스턴스의 내부 값을 수정할 수 없는 클래스이다. 쉽게 말해 한 번 만들어진 이상 값이 바뀌지 않는 클래스를 말한다.

String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal이 이러한 불변 클래스에 속한다.

🫧 클래스를 불변으로 만드는 방법

  1. 객체의 상태를 변경하는 메서드 (변경자)를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다.
    -> 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이다.

  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private으로 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    -> 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다. 또한, 접근자 메서드가 그 필드를 그대로 반환하게 해도 안 된다.

🫧 함수형 프로그래밍

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

반면, 절처적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.

✨ 함수형 프로그래밍 예시 코드

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

    public Conplex(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 Comliex 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;

        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)";
    }
}
  • 피연산자 자체는 그대로이다.
  • 메서드 이름으로 add 같은 동사 대신 plus 같은 전치사를 사용한다.
    -> 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조함.

코드에서도 확인할 수 있듯, 함수형 프로그래밍은 함수의 순수성 (함수의 참조 불변성)과 변수의 불변성이라는 두 가지 특징을 갖고 있다.

따라, 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요도 없으며, 안심하고 공유할 수 있게 된다.

🫧 불변 객체(클래스) 장점

  1. 스레드 안전하여 따로 동기화할 필요가 없다.
  2. 안심하고 공유할 수 있다.
  3. 정적 팩터리 제공이 가능하다
    -> item 1, 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해준다. 이런 정적 팩터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.

  4. 그 자체로 실패 원자성을 제공한다
    -> 메서드에서 예외가 발생한 후에도 그 객체는 여전히 메서드 호출 전과 똑같은 유효한 상태여야 한다는 성질. 불변 객체는 내부 상태를 바꾸지 않으므로 이 성질은 만족한다.

🫧 불변 클래스 단점

  1. 값이 다르면 반드시 독립된 객체로 만들어야 한다.

이러한 문제는 성능 저하를 야기한다. 1번 문제를 해결하는 방법은 크게 두 가지가 있다.

  1. 흔히 쓰일 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법.
    -> 이를 통해 각 단계마다 객체를 생성하지 않아도 된다.

추가로, 불변 클래스는 아무리 복사해 봐야 원본과 똑같으므로 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋다.

🫧 가변 동반 클래스

본래 불변인 객체의 관련값을 수정해 줄 수 있는 클래스를 동반해 제공할 수 있다.

예를 들어 불변 객체인 String의 단점을 보완해 주는 가변 동반 클래스인 StringBuilder, StringBuffer가 있다.

이러한 가변 동반 클래스는 불변 객체의 값이 다르다면 반드시 독립된 객체로 만들어야 한다. 의 문제를 해결하고자 생겨났다.

클라이언트들의 원하는 복잡한 연산을 정확히 예측할 수 있다면 package-private의 가변 동반 클래스로, 그렇지 않다면 public으로 클래스를 제공해야 한다.

🫧 불변 클래스 만들기 - 상속 금지

  1. final 클래스로 선언
  2. 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공

✨ 2번 코드 예시

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);
    }
    // ... 생략
}

🫧 참고 자료