개발 공부 기록하기/01. JAVA & Kotlin
짧은 코멘트와 함께하는 이펙티브 자바) #17 변경 가능성을 최소화하라 (불변객체)
lannstark
2020. 10. 15. 20:06
짧은코멘트
- 아래에 언급된 장점들로 인해 불변 객체는 정말 자주 사용되며 final 키워드에 대해서는 잘 알아두면 좋다. class나 method에서도 간혹 사용할 일이 있기 때문이다.
- 불변 객체의 연산을 예측하는 것 뿐만 아니라 객체 생성이 제한적이라면 내부에 캐싱해두는 것도 성능상 괜찮은 방법이다.
- 특히 Enum은 자연스럽게 불변 객체가 되는데 name이나 특정 key를 사용해 Enum 찾을 일이 많다면 캐싱이 권장된다.
변경 가능성을 최소화하라
불변 클래스란 그 인스턴스의 내부 값을 수정할 수 없는 클래스이다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다. 불변 클래스는 다섯 가지 규칙을 따라야 한다.
- 객체의 상태를 변경하는 메소드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 모든 필드를 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 Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
/* 아래 생략 */
}
사칙연산 메소드들은 자신의 인스턴스를 수정하는 것이 아니라 새로운 Complex 인스턴스를 만들어 반환한다. 또한 메소드 이름으로 add 같은 동사 대신에 plus 같은 전치사를 사용하였다. 메소드가 객체의 값을 변경하지 않는다는 것을 강조하려는 의도이다.
불변 객체의 장점은 이렇다
- 단순하다.
- 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.
- 안심하고 공유할 수 있다.
- 한 번 만든 인스턴스를 최대한 재활용할 수 있다.
- 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩토리를 제공할 수 있다.
- 방어적 복사가 필요하지 않다.
- clone 메소드나 복사 생성자를 제공하지 않는 것이 좋다.
- 불변 객체는 그 자체로 실패 원자성을 제공한다. * 실패 원자성 : 메소드에서 예외가 발생한 후에도 그 객체는 여전히 메소드 호출 전과 똑같은 유효한 상태인 특성
불변 객체에도 단점은 있다.
값이 다르면 반드시 독립된 객체로 만들어야 하기 때문에 객체 생성 비용이 발생한다는 점이다.
이때 흔히 쓰일 다단계 연산들을 예측하여 기본 기능으로 제공함으로써 비용을 낮출 수 있다. 이런 다단계 연산을 기본으로 제공한다면 더 이상 각 단계마다 객체를 생성하지 않아도 된다. 불변 객체의 다단계 연산 속도를 높여주는 가변 동반 클래스를 package-private으로 두면 되기 때문이다. 만약 다단계 연산들을 정확히 예측하기 어렵다면 가변 동반 클래스를 public으로 두어야 한다.
가장 손쉽게 와닿는 예가 바로 String의 가변 동반 클래스인 StringBuilder이다.
정리
- getter가 있다고 해서 setter를 만들어야 하는 것은 아니다.
- 클래스는 꼭 필요한 경우가 아니라면 불변이여야 한다. 불변은 장점이 많고 단점이라곤 특정 상황에서 잠재적 성능 저하 뿐이다. 이마저도 가변 동반 클래스와 캐싱을 적절히 사용하면 성능 저하를 막을 수 있다.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
- 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.