이펙티브 자바, 쉽게 정리하기 - item 17. 변경 가능성을 최소화하라
불변 클래스
불변클래스란, 인스턴스 내부 값을 수정할 수 없는 클래스이다. 불변 클래스 내부 정보는 객체가 파괴되는 순간까지 절대 달라지지 않는다. 자바에는
String
,기본 타입 박싱 클래스
,BigInteger
,BigDecimal
이 있다.
- 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉽다.
- 오류가 생길 여지도 적고 훨씬 안전하다.
클래스를 불변으로 만드는 방법
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다. (상속이 불가능하게 만든다.)
- 모든 생성자를
private
혹은package-private
으로 만들고 정적 팩터리를 제공한다.
- 모든 생성자를
- 모든 필드를
final
로 선언한다. - 모든 필드를
private
으로 선언한다.public final
은 추후 API 리팩토링의 유연성을 저해할 수 있기 때문에 주의해야 한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
static 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 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 (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);
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
- 위 예는 복소수를 클래스로 구현한 것이다.
- 접근자 메서드와 사칙연산 메서드가 있다.
- 사칙연산 메서드는 피연산자인 클래스의 멤버를 건들지 않고, 매번 새로운 메서드를 반환한다.
- 그래서 메서드 이름으로
add
같은 동사 메서드 대신plus
와 같은 전치사를 사용하였다. - 이를 함수형 프로그래밍이라 한다.
- 이러한 방식을 이용하면, 코드에서 불변이 되는 영역이 늘어나는 장점을 누릴 수 있다.
- 그래서 메서드 이름으로
불변이 되면 얻어지는 장점
- 불변객체는 기본적으로 스레드 안전하여 따로 동기화가 필요 없다.
- 불변객체는 안심하고 공유할 수 있다.
- 불변의 장점을 활용하려면 클래스 내부에 많은 상수들을 제공하여 사용하는 것도 좋다.
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);
불변 클래스 및 불변 객체의 특징
- 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.
- 정적 팩터리를 통해 캐싱을 추후에 쉽게 덧붙일 수 있다.
- 불변 클래스는
clone()
메서드나 복사 생성자를 제공하지 않는 것이 좋다.String
의 경우 이전의 잘못된 설계로 복사 생성자를 제공하고 있지만 사용하지 않는 것이 좋다.
- 불변 객체는 자유롭게 공유도 가능할 뿐더러 내부 데이터 공유도 가능하다.
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
BigInteger
내부negate()
의 경우this.mag
필드를 재활용한다.- 이 경우 배열 필드라서 사실 내부 값 수정이 가능하지만,
private final
로 된 불변 배열이고, 어디서도 접근 권한을 주지 않아 안전하다.
- 이 경우 배열 필드라서 사실 내부 값 수정이 가능하지만,
- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
Map
이나Set
내부에value
로서 들어간 객체들에 변경이 생기면 불변식이 허물어지는데, 불변 객체를 사용하면 그런 걱정은 없다.
- 불변 객체는 그 자체로 실패 원자성을 제공한다.
- 메서드에서 예외가 발생한 뒤에도 그 객체는 여전히 메서드 호출 전과 같은 유효한 상태여야 한다는 것을 실패 원자성이라고 한다. 내부 상태를 바꾸지 않으니 메서드 호출 전과 정확히 동일할 것이다.
불변 클래스의 단점
- 값이 다르면 반드시 독립된 객체로 만들어야 한다.
- 보통 약간의 메모리 낭비가 있다.
- 값이 다를 때마다 새로운 객체를 만들어야 한다.
- 값의 가짓수가 많다면, 이들을 모두 만드는데 큰 비용을 치러야 한다.
- ex) 100만 자리수를 가지고 있는 비트에서 앞의 1자리만 다른 경우, 낭비가 심하다.
- 보통 약간의 메모리 낭비가 있다.
불변 클래스의 단점 해소
- 다단계 연산(multistep operation)들을 예측하여 기본 기능으로 제공할 수 있다.
- 클라이언트가 원하는 복잡한 연산을 미리 예측하여 제공하는 것이다.
- 불변인
String
클래스의 경우 가변 동반 클래스인StringBuilder
클래스가 있다.- 구닥다리 전임자
StringBuffer
도 있다.
- 구닥다리 전임자
불변 클래스의 설계 방법
- 자신을 상속하지 못하게 해야 한다.
- 클래스를
final
로 선언하면 된다. - 모든 생성자를
private
혹은package-private
으로 만들고public
정적 팩터리를 제공하면 된다.
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);
}
}
- 위의 코드 예에서 생성자는
private
으로 제공하고,valueOf
정적 팩터리를 통해서 객체를 제공한다. - 이 방식이 최선인 경우가 많다.
- 정적 팩터리 방식은 다수의 구현 클래스를 활용한 유연성을 제공한다.
- 다음 릴리즈에서 객체 캐싱 기능을 추가해 성능을 끌어올릴 수도 있다.
BigInteger
클래스의 경우, 불변 객체가 사실상final
이어야 한다는 아이디어가 널리 퍼지기 전에 만들어진 것이라public
생성자를 제공하는데, 이 때문에 하위 호환성에 의한 제약이 걸리고 지금까지 이 문제를 고치지 못했다.
public static BigInteger safeInstance(BigInteger val) {
return val.getClass() == BigInteger.class ?
val : new BigInteger(val.toByteArray());
}
- 위는
BigInteger
클래스를 상속받은 하위 클래스가BigInteger
타입의 자리에 들어오는 것을 방지하는 메서드이다.
불변 클래스를 설계할 때, '모든 필드가
final
이어야 한다'는 규칙은 사실 좀 과해서 성능을 위해 '어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다.' 정도로 완화할 수 있다.
어떤 불변 클래스는 계산비용이 큰 값을 계산하여final
이 아닌 필드에 넣어놓고 재활용하기도 한다. 불변 객체는 몇번을 계산해도 같은 값임이 보장되기 때문에 가능하다.
핵심 정리
- 클래스는 꼭 필요한 경우가 아니면 불변인 것이 좋다.
- 단점이라곤 잠재적 성능저하 뿐이다.
String
혹은BigInteger
와 같은 무거운 값 객체도 불변으로 만들 수 있는지 고심해야 한다.- 불변으로 만들 수 없는 클래스는 변경할 수 있는 부분을 최대한 줄이는 것이 좋다.
- 객체가 가질 수 있는 상태의 수를 줄이는 것이 객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다.
- 꼭 변경할 필드 외에는
final
로 선언하자.
- 다른 합당한 이유가 없다면 모든 필드는
private final
접근 제어자를 가지는 것이 좋다. - 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
불변은 아니지만, 상태가 적어야 한다는 원칙을 잘 지킨 클래스의 예로
CountDownLatch
가 존재한다. 이 클래스는 한번 카운트를 정하면 수정도 불가능하고 단, 1회만 이용 가능하다는 제약을 걸어 단순명료함을 극대화 시켰다.
CountDownLatch 관련 설명
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) | 2022.01.04 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 18. 상속보다는 컴포지션을 사용하라 (2) | 2022.01.04 |
이펙티브 자바, 쉽게 정리하기 - item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.01.01 |
이펙티브 자바, 쉽게 정리하기 - item 15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.01.01 |
이펙티브 자바, 쉽게 정리하기 - item 14. Comparable을 구현할지 고려하라 (0) | 2022.01.01 |