# ITEM17 변경 가능성을 최소화하라
# 불변클래스
인스턴스 내부의 값을 수정할 수 없는 클래스
예시
- String
- 기본타입의 박싱된 클래스
- BigInteger
- BigDecimal ...
# 불변 클래스로 설계하는 이유
가변클래스보다
- 설계/구현/사용이 쉬움
- 오류가 더 적고 훨씬 안전함
# 불변 클래스로 만드는 방법
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않기
- 클래스를 확장할 수 없도록 한다.
- 하위클래스에서 부주의하게/나쁜의도로 객체의 상태를 변하게 하는 사태를 방지
- final 클래스로 선언하는 방법
- 모든 필드를 final 로 선언
- 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제가 없다
- 모든 필드를 private 으로 선언
- 클라이언트에서 가변객체를 참조하는 필드를 수정하는 일을 방지
- public final 로 선언한다면 다음 릴리스에서 내부 표현을 수정할 수 없다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 필드는 클라이언트의 객체 참조를 가리켜서는 안된다.
- 접근자 메서드로 가변 객체 필드를 반환하면 안된다.
- 생성자, 접근자, readObject 메서드 [88] 에서 방어적 복사를 수행해야 한다.
# 함수형 프로그래밍과 절차형 프로그래밍
# 함수형 프로그래밍
사칙연산 메서드들이 인스턴스 자신을 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double 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);
}
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
- 동사(add) 대신 전치사(plus) 사용
- 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조함
# 절차적(명령형) 프로그래밍
메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.
# 장점
# 1. 스레드 안전 → 동기화 불필요
- 안심하고 공유 가능
# 2. 불변 인스턴스의 캐싱
- public static final 상수로 불변 인스턴스 제공
public static final Complex ZERO = mew Complex(0, 0); public static final Complex ONE = new Complex(1, 0);
1
2 - 불변 클래스는 자주 사용되는 인스턴스를 캐싱하는 정적 팩터리를 제공할 수 있다.
- 박싱된 기본 타입, BigInteger 의 특징
- 클라이언트가 인스턴스를 공유하여, 메모리 사용량과 가비지 컬렉션의 비용이 감소
# 3. 방어적 복사 불필요
- 아무리 복사해도 원본과 같음
- clone 메서드, 복사 생성자[13] 불필요
# 4. 불변 객체끼리는 내부 데이터를 공유할 수 있다.
BigInteger 클래스는 내부에서
- 값의 부호(sign)와 크기(magnitude) 를 따로 표현함
- negate 메서드는 크기가 같고, 부호만 반대인 새로운 BigInteger 생성.
- 배열은 가변이지만, 원본 인스턴스와 공유하여 사용함.
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum;
final int[] mag;
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 5. 객체를 만들 때 불변객체들을 구성요소로 사용하면 이점이 많다.
- 구조가 아무리 복잡하더라도 불변식을 유지하기 훨씬 수월하다
- 불변 객체는 Map 의 키와 Set 의 원소 에 적합하다
- 맵이나 집합은 담긴값이 바뀌면 불변식이 무너진다.
# 6. 불변객체는 그 자체로 실패 원자성을 제공한다[76]
실패원자성 failure atomicity
- 메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태여아 한다.
상태가 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.
# 단점
# 1. 값이 다르면 반드시 독립된 객체로 만들어야 한다.
- 값의 가짓수가 많다면 모두 만드는데 큰 비용이 든다.
filpBit 메서드는 새로운 BigInteger 인스턴스를 생성한다.
- 원본과 단지 한비트만 다른 백만비트짜리 인스턴스 이다.
- BigInteger의 크기에 비례해 시간과 공간을 소비한다.
BigInteger moby = ...;
mody = moby.filpBit(0);
1
2
2
BitSet
- BigInteger 처럼 임의 길이의 비트 순열도 표현
- BigInteger 와 달리 가변이다.
- 비트 하나만 상수 시간안에 바꾸는 메서드를 제공한다.
BitSet moby = ...;
moby.flip(0);
1
2
2
# 2. 성능 문제
- 원하는 객체를 완성하기까지의 단계가 많다
- 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 불거진다.
대처방법
- 다단계 연산들을 예측하여 기본 기능으로 제공한다.
- 각 단계마다 객체를 생성하지 않아도 된다
- BigInteger 는 모듈러 지수 같은 다단계 연산 속도를 높여주는 가변 동반 클래스 companion class 를 package-private 으로 두고 있다.
- String 클래스의 가변 동반 클래스는 StringBuilder 와 StringBuffer
# 불변 클래스를 만드는 방법
# 생성자 대신 정적 팩터리를 사용한 불변 클래스
자신을 상속하지 못하게 할 수 있다. (하위 클래스에서 상위 클래스의 생성자를 호출하지 못하므로)
public class Complex {
// ... 불변 필드
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 이 방법이 최선일 때가 많다
BigInteger 와 BigDecimal
- 설계할 당시, 불변 객체가 사실상 final 이어야 한 다는 생각이 널리 퍼지지 않았음
- 두 클래스의 메서드 들은 모두 재정의할 수 있게 설계되었으며, 하위 호환성이 지금까지 발목을 잡고 있다
- 이 인수들은 가변이라 가정하고 방어적으로 복사해 사용해야 한다
# 유연성
"모든 필드는 final 이고 어떤 메서드도 그 객체를 수정할 수 없어야 한다"
성능을 위해 다음처럼 살짝 완화해야 한다.
지연 초기화
- 어떤 불변 클래스는 계산 비용이 큰 값을 나중에 계산하여 final 이 아닌 필드에 캐시 할 수 있다.
- PhoneNumber 의 hashCode 메서드[11]는 처음 불렸을 때 해시 값을 계산해 캐시함
# 직렬화의 주의점
Serializable 을 구현하는 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면,
- readObject, readResolve 메서드를 반드시 제공하거나
- ObjectOutputStream.writeUnshared 와 ObjectInputStream.readUnshared 메서드를 사용해야 한다.
그렇지 않으면, 공격자가 이 클래스로부터 가변 인스턴스를 만들어 낼 수 있다.
# 타협점
클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
- 불변 클래스는 장점이 많으며, 단점이라곤 특정 상황에서의 잠재적 성능 저하 뿐이다.
- 단순한 값 객체는 항상 불변으로 만들자
- 성능 때문에 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공하자 ??
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화
- 객체가 가질 수 있는 상태의 수를 줄이면, 그 객체를 예측하기 쉬워지고 오류가 생길 가능성 감소
- 꼭 변경해야 할 필드를 뺀 나머지 모두를 final 로 선언
- 다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
- 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public 으로 제공해서는 안된다.
- 객체를 재활용할 목적으로 상태를 다시 초기화 하는 메서드도 안된다.