아이템 17. 변경 가능성을 최소화하라

콜트·2021년 7월 25일
0
post-thumbnail

아이템 17. 변경 가능성을 최소화하라

불변 클래스

  • 불변 클래스란 그 인스턴스 내부 값을 수정할 수 없는 클래스다.
  • 이는 객체가 파괴되는 순간까지 절대 달라지지 않는다.
    • ex. String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal ...
  • 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉽고 오류가 생길 여지가 적으며 훨씬 안전하다.

불변 클래스로 만들기 위한 5가지 규칙

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다. 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아준다.
  • 모든 필드를 final로 선언한다. 시스템이 강제하는 수단을 이용해 설계자의 의도를 드러내는 방법이다.
  • 모든 필드를 private로 선언한다. 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다. 그리고 접근자 메서드가 해당 필드를 그대로 반환해서도 안 되며, 방어적 복사를 수행해야 한다.

불변 복소수 클래스

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

    // ...

    @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);
    }
}
  • plus 메서드는 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어서 반환한다.
  • 이처럼 피연산자에 함수를 적용해 그 겨과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.
  • 절차적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.

불변 객체

  • 불변 객체는 단순하다. 생성 시점의 상태를 파괴될 때까지 간직한다.
  • 모든 생성자가 클래스 불변식(class invariant)을 보장한다면 별다른 노력을 기울이지 않아도 영원히 불변으로 남는다. 반면 가변 객체는 복잡한 상태에 놓일 수도 있다.
  • 불변 객체는 근복적으로 스레드 안전하여 따로 동기화할 필요 없다.
  • 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변 객체는 안심하고 공유할 수 있다.
  • 가장 쉬운 불변 클래스의 인스턴스 재활용 방법은 자주 쓰이는 값들을 상수(public static final)로 제공하는 것이다.
  • 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.
  • 이런 정적 팩터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
  • 새로운 클래스를 설계할 때 public 생성자 대신 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙일 수 있다.
  • 불변 객체는 방어적 복사가 필요 없다. 복사해봐야 원본과 똑같으니 복사 자체가 의미가 없다.
  • 불변 객체는 그 자체로 실패 원자성을 제공한다.
    • 실패 원자성(failure atomicity)이란 '메서드에서 예외가 발생한 후에도 그 객체는 여전히 (메서드 호출 전과 똑같은) 유효한 상태여야 한다'는 성질이다.

불변 객체의 단점

  • 불변 객체는 값이 다르다면 반드시 독립된 객체로 만들어야 한다. 따라서 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 할 수 있다.
  • 이에 대처하는 방안은 두 가지이다. 첫 번째는 흔히 쓰일 다단계 연산(multistep operation)들을 예측하여 기본 기능으로 제공하는 방법이다.
  • 클라이언트가 원하는 복잡한 연산들을 정확히 예측할 수 있다면 이러한 다단계 연산 속도를 높여주는 package-private인 가변 동반 클래스(companion class)만으로도 충분하다.
  • 예측이 불가능하다면 가변 동반 클래스를 public으로 제공하는 게 최선이다. 대표적인 예로 StringBuilder, StringBuffer가 있다.

생성자 대신 정적 팩터리를 사용한 불변 클래스

  • 클래스가 불변임을 보장하려면 자신을 상속하지 못하게 해야 한다.
  • 이에 대한 가장 쉬운 방법은 final 클래스로 선언하는 것이다.
  • 또는 생성자를 private로 만들고 정적 팩터리를 제공할 수도 있다(이 편이 더 유연하다).
public final 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);
    }

    // ...
}
  • 이 방식은 바깥에서 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 훨씬 유연하다.
  • 패키지 바깥의 클라이언트에서 바라본 이 불변 객체는 사실상 final이다. public이나 protected 생성자가 없으니 다른 패키지에서 이 클래스를 확장하는 게 불가능하기 때문이다.
  • 정적 팩터리 방식은 다수의 구현 클래스를 활용한 유연성을 제공하고, 이후에 캐싱 기능을 추가해 성능을 끌어올릴 수도 있다.
  • 계산 비용이 큰 값의 경우, 나중에 (처음 쓰일 때) 계산하여 final이 아닌 필드에 캐시하여 사용함으로써 계산 비용을 절감할 수 있다. 불변 객체는 변하지 않기 때문에 가능하다.

정리

  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 단순한 값 객체는 불변으로 만들도록 하자.
  • 성능 때문에 어쩔 수 없다면 불변 클래스와 함께 가변 동반 클래스를 public 클래스로 제공하도록 하자.
  • 모든 클래스를 불변으로 만들 수는 없다. 하지만 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이도록 하자.
    • 꼭 변경해야 하는 필드를 제외한 나머지 모두를 final로 선언하도록 하자. 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
    • 확실한 이유가 없다면 생성자나 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공하면 안 된다.
    • 객체를 재활용할 목적으로 상태를 다시 초기화하는 메서드도 마찬가지다. 복잡성만 커지고 성능 이점은 거의 없다.
profile
개발 블로그이지만 꼭 개발 이야기만 쓰라는 법은 없으니, 그냥 쓰고 싶은 내용이면 뭐든 쓰려고 합니다. 코드는 깃허브에다 작성할 수도 있으니까요.

0개의 댓글