- 불변 클래스란 그 인스턴스 내부 값을 수정할 수 없는 클래스
- 불변 인스턴스에 간직된 정보는 객체가 파괴되는 순간까지 절대 달라지지 않는다.
- 예 : String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal
- 불변 클래스는 가변 클래스보다 설계,구현 하기가 쉽다. 또한 오류가 생길 여지도 적고 훨씬 안전하다.
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- ex: setter method
- 클래스를 확장할 수 없도록 한다. (extend x)
- 모든 필드를 final로 선언한다. 시스템이 강제하는 수단을 이용해 설계자의 의도를 드러내는 방법이다.
- 모든 필드를 private로 선언한다. 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다. 그리고 접근자 메서드가 해당 필드를 그대로 반환해서도 안 되며, 방어적 복사를 수행해야 한다.
- 단순하다
- 불변 객체는 단순하다. 생성 시점의 상태를 파괴될 때까지 간직한다.
- 모든 생성자가 클래스 불변식(class invariant)을 보장한다면 별다른 노력을 기울이지 않아도 영원히 불변으로 남는다. 반면 가변 객체는 복잡한 상태에 놓일 수도 있다.
- 자유롭게 공유 가능
- 불변 객체는 스레드 안전 : 동기화할 필요 없다
- 변경되지 않으므로 여러 스레드에서 접근하더라도 훼손되지 않는다.
- 불변 객체는 안심하고 공유 가능 : 어떤 스레드도 다른 스레드에 영향을 줄 수 없다.
- 가장 쉬운 불변 클래스의 인스턴스 재활용 방법은 자주 쓰이는 값들을 상수(public static final)로 제공하는 것이다.
- 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.
- 이런 정적 팩터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
- 새로운 클래스를 설계할 때 public 생성자 대신 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙일 수 있다.
- 불변 객체는 방어적 복사가 필요 없다.
- 불변 객체를 복사해도 항상 원본이랑 같기 때문에 의미가 없다.
- 불변 객체는 그 자체로 실패 원자성을 제공한다.
- 실패 원자성(failure atomicity) : '메서드에서 예외가 발생한 후에도 그 객체는 여전히 (메서드 호출 전과 똑같은) 유효한 상태를 가진다’
- 불변 객체는 값이 다르다면 반드시 독립된 객체로 만들어야 한다.
- 특히 인스턴스 생성의 비용이 큰 경우(ex 100만 비트 짜리 BigInteger, 생명 주기가 긴 객체 등) 이 문제가 두드러질 수 있다.
대책
- 인스턴스 캐싱
- 인스턴스 캐싱을 통해 인스턴스를 새로 생성하지 않고 재사용하여 비용을 줄인다.
- 자바의 대부분의 기본값 wrapper클래스에서 사용되고 있는 방식이다. (Boolean, Integer, String...)
- 다단계 연산을 예측하여 기본 기능으로 제공
- 인스턴스를 생성하는데 여러 단계를 거치는데, 이 단계들을 예측하여 기능으로 제공
- ex) 불변 객체 String은 가변 동반 클래스인 StringBuilder를 제공
- StringBuilder에서는 String 인스턴스를 생성하는 여러 단계를 쪼개어 가변으로 객체를 생성할 수 있는 기능을 제공한다. (append메서드를 예로 들 수 있다.)
public static void main(String[] args) {
String count = "";
for (int i = 0; i <10; i++) {
count +=String.valueOf(i);
System.out.println(count);
}
}
다음과 같은 경우 String 의 인스턴스를 계속해서 만든다.
따라서 다음과 같은 경우 StringBuilder 가변 동반 클래스를 제공한다.
- final 클래스로 선언
- 모든 생성자를 private 혹은 package-private 으로 만들고 정적팩터리을 제공하는 방법이 더 유연한 방법이다.
- getter 메서드가 있다고 해서 무조건 setter 를 만들지는 말자.
- 클래스는 꼭필요한 경우가 아니라면 불변이어야 한다.
- 불변으로 만들 수없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
- 꼭 변경해야 할 필드를 뺀 나머지는 final로 선언
- 아이템 15의 조언과 종합하자면 다른 합당한 이유가 없다면 모든 필드는 private final 이어야한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다.
- 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public 으로 제공 하면 안된다.