아이템 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으로 제공하면 안 된다.
- 객체를 재활용할 목적으로 상태를 다시 초기화하는 메서드도 마찬가지다. 복잡성만 커지고 성능 이점은 거의 없다.