item 17. 변경 가능성을 최소화하라
불변 클래스?
- 객체가 파괴되는 순간까지 인스턴스의 내부 값을 절대 수정할 수 없는 클래스
- ex) String, 기본 boxing type class, BigInteger, BigDecimal
[ 불변 클래스를 만드는 이유 ]
- 가변 클래스보다 설계, 구현, 사용이 쉽다.
- 오류가 적고 안전하다.
[ 불변 클래스를 만드는 다섯 가지 규칙]
1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.
2. 클래스를 확장할 수 없도록 한다.
- 하위 클래스에서 객체의 상태를 변하게 하는 불상사를 막아준다.
- 상속을 막는 대표적인 방법은 클래스를 final로 설정하는 것이다. (다른 방법도 있음)
3. 모든 필드를 final로 선언한다.
- 시스템이 강제하는 수단을 이용하여 설계자의 의도를 명확히 드러내는 방법이다.
- 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제 없이 동작하게 보장할 때 필요하다.
4. 모든 필드를 private로 선언한다.
- 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막는다.
- (기본 타입 필드나 불변 객체를 참조하는 필드를 public final로만 선언해도 불변 객체가 되지만, 릴리스에서 내부 표현을 바꾸지 못하게 되므로 권장하지 않는다.)
5. 자신 외에는 내부 가변 컴포넌트에 접근할 수 없게 한다.
- 가변 객체를 참조하는 필드가 클래스 내에 단 한개라도 있다면, 클라이언트에서 객체의 참조를 얻을 수 없어야 한다.
- 이러한 필드는 클라이언트가 제공한 객체 참조를 가리켜서도, 접근자 메서드가 그 필드를 그대로 반환해서도 안된다.
* 함수형 프로그래밍
- 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
- 코드에서 불변이 되는 영역의 비율이 높아진다.
* 절차적(명령형) 프로그래밍
- 메서드에서 피연산자(자신)를 수정해 자신의 상태가 변하게 하는 프로그래밍 패턴.
- 메서드 이름으로 동사보단 전치사를 사용함. (메서드가 값을 변경하지 않는다는 것을 강조함)
[ 불변 객체 ]
- 생성된 시점의 상태를 파괴될 때 까지 그대로 간직한다.
- 모든 생성자가 클래스 불변식을 보장한다면 프로그래머는 불변을 위해 따로 조치를 취할 필요가 없다.
- 가변 객체를 사용할 경우 변경자 메서드가 일으키는 상태 전이를 정밀하게 문서로 남겨놓지 않기 때문에, 믿고 사용하기에 어려움이 있다.
[ 불변 객체의 장점 ]
1. 불변 객체는 스레드 안전하여 동기화 할 필요가 없다.
- 여러 스레드가 동시에 사용해도 훼손되지 않는다.
- 클래스를 스레드 안전하게 만드는 가장 쉬운 방법
2. 불변 객체는 안심하고 공유할 수 있다.
- 불변 객체에 대해서는 어떤 스레드라도 다른 스레드에 영향을 줄 수 없기 때문이다.
- 따라서 한 번 만든 인스턴스를 최대한 재활용하는것을 권장한다.
- 가장 쉬운 재활용 방법 : 사수 쓰이는 값들을 상수(public static final)로 제공하는 것.
- 불변 클래스는 자주 사용되는 인스턴스를 캐싱해 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.
- ex) 박싱된 기본 타입 클래스 전부와 BigInteger
- -> 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량, 가비지 컬렉션 비용이 줄어든다.
- 따라서, 새로운 클래스 설계 시 public 생성자 대신 정적 팩터리를 만들어보자. (클라이언트를 수정하지 않고도 캐시 기능을 덧붙일 수 있다.)
3. 불변 객체는 자유롭게 공유할 수 있다.
- 방어적 복사를 할 필요가 없어진다.
- 그러므로 clone이나 복사생성자를 제공하지 말자. (String class의 복사 생성자 되도록 사용 금지!)
4. 불변 객체끼리는 내부 데이터를 공유할 수 있다.
- ex) BigInteger Class
- 부호에는 int를 , 절댓값에는 int배열을 사용한다.
- negate 메서드는 크기가 같고 부호는 반대인 새로운 BigInteger을 생성하는데, 이 때 배열은 가변이지만 복사하지 않고 원본 인스턴스와 공유할 수 있다. (원본 인스턴스가 가리키는 배열을 같이 가리킬 수 있다.)
5. 객체를 만들 때 다른 불변 객체들을 구성 요소로 사용할 때의 장점을 누릴 수 있다.
- 값이 바뀌지 않는 구성요소로 이뤄진 객체는 불변식을 유지하기가 훨씬 쉽다.
- Map의 key, Set의 원소로 쓰기 좋다.
6. 그 자체로도 실패 원자성을 제공한다.
- 실패 원자성이란? 메서드에서 예외가 발생한 후에도 그 객체는 여전히 메서드 호출 전과 같은 상태여야 한다는 의미.
- 상태가 절대 변하지 않으므로 당연하겠쥬
[ 불변 객체의 단점 ]
1. 값이 다르면 독립된 개체로 만들어야 한다.
- 값의 가짓수가 많다면, 값들을 모두 만드는데 비용이 많이 들겠죠???
- BigInteger(불변)을 새로 생성하는 flipBit method vs 원하는 비트 하나만 상수 시간에 바꿔주는 BitSet 클래스의 메소드 (가변)
2. 객체를 생성하는 시간이 길고 중간 단계 객체들이 버려질 경우 성능 문제로 이어질 수 있다.
- 대처 방법 1 : 다단계 연산들을 예측 해 기본 기능으로 제공
- 각 단계마다 객체를 생성하지 않아도 된다.
- ex) BigInteger은 moduler 지수와 같은 다단계 연산 속도를 높여주는 가변 동반 클래스를 package-private으로 둠.
- 대처 방법 2 : 해당 클래스를 그냥 public 으로 제공하기
- ex) String의 가변 동반 클래스는 StringBuilder, StringBuffer
- 클래스가 불변임을 보장하게 하려면 -> 자신을 상속받지 못하게 해야 함.
- 자신을 상속받지 못하게 하는 방법
- final class로 선언하기
- 모든 생성자를 private 또는 package-private으로 만들고, public 정적 팩터리를 제공하기
- 바깥에서 볼 수 없는 package-private 클래스를 여러개 만들어 활용 가능하므로 유연하다.
- package 바깥에서는 사실상 final로 취급됨. (다른 패키지에서 해당 클래스를 확장할 수 없기 때문)
- 유연성을 제공하고 객체 캐싱 기능을 추가해 성능을 올릴 수 있다.
[ 주의 사항 ]
신뢰할 수 없는 하위 클래스의 인스턴스라고 확인된다면, 이 인수들은 가변이라고 가정하고 방어적으로 복사해 사용해야 한다.
"모든 필드는 final이고 어떤 메서드도 그 객체를 수정할 수 없다 " -> 너무 과하다.
"어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다 " 로 하자...
[ 직렬화 시 주의 사항 ]
Serializable을 구현하는 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면?
- readObject, readResolve method를 반드시 제공하거나,
- ObjectOutputStream.writeUnshared, ObjectInputStream.readUnshared method를 제공해야 한다.
- 공격자가 클래스로부터 가변 인스턴스를 만들어 내는 것을 막기 위함이다.
[ 정리 ]
1. getter가 있다고 무조건 setter를 만들지 말자.
2. class는 꼭 필요한 경우가 아니면 불변이여야 한다.
3. 단순한 값 객체는 불변으로 만들자. ex) PhoneNumber, Complex class.
4. String, BigInteger처럼 무거운 값 객체도 불변으로 만들지 고려는 해야 한다. 성능 때문에 그럴 수 없다면 가변 동반 클래스를 public으로 제공하자.
5. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 한다. 오류를 줄이기 위함이다.
-> 합당한 이유가 없다면, 모든 필드는 private final이여야 한다.
6. 생성자는 불변식 설정이 모두 완료된 초기화된 객체를 생성해야 한다.
7. 생성자, 정적 팩터리 외에는 웬만하면 public으로 제공하지 말자.