item 17) 변경 가능성을 최소화하라
불변 클래스란?
인스턴스 내부 값을 수정할 수 없는 클래스
불변 클래스 인스턴스의 정보는 객체가 파괴되는 순간까지 절대 변하지 않는다
ex) String, 기본 타입의 박싱된 클래스(Integer, ...), BigInteger, BigDecimal
가변 클래스보다 설계, 구현, 사용이 쉽다.
오류 생길 여지가 적고 안전하다.
꼭 필요한 경우가 아니라면 불변 클래스로 만들고, 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화할 것.
생성자는 불변식 설정이 모두 완료되어 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
불변 클래스의 5가지 규칙
객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다
클래스를 확장할 수 없도록 한다
하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만들지 못하도록 함
상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것
모든 필드를 final로 선언
시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법
새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데 필요
하지만 final로 선언된 변수에 변경 가능한 객체가 지정되어 있다면, 해당 변수에 들어있는 객체의 값을 사용하는 부분을 모두 동기화 시켜야 한다.
성능을 위해 "객체의 상태 중 외부에 보이는 값을 변경하는 메서드는 있으면 안된다" 고 완화할 수 있다.
모든 필드를 private으로 선언한다
필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.(item15,16)
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다
클래스에 가변 객체를 참조하는 필드가 하나라도 있다면, 클라이언트에서 해당 가변 객체의 참조를 얻을 수 없도록 해야 한다.
생성자, 접근자, readObject 메서드(item88)에서 필드를 그대로 반환하면 안되며 방어적 복사를 수행해야 한다.
함수형 프로그래밍
피연산자에 함수를 적용해 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴. (절차적/명령형 프로그래밍에서는 메서드에서 피연산자인 자기 자신을 수정해 상태가 변화된다.)
불변이 되는 영역이 넓어진다.
아래 예시는 Complex 객체끼리 더할 때 기존 피연산자를 수정하는 것이 아닌 새로운 인스턴스를 반환하는 메서드이다.
불변 객체의 장점
불변 객체는 단순하다
가변 객체는 임의의 복잡한 상태에 놓일 수 있기 때문에 가변 객체는 믿고 사용하기 어렵다.
불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 가지므로 믿고 사용할 수 있다.
근본적으로 스레드 안전하여 동기화 할 필요가 없다
여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.
클래스를 thread safe하게 만드는 가장 쉬운 방법이기도 하다.
불변객체는 안심하고 공유 가능
스레드 간 영향을 주고받을 수 없기 때문이다.
아무리 복사해봐야 원본과 똑같아 방어적 복사가 필요 없으므로, clone 메서드나 복사 생성자를 제공하지 않는 게 좋다.
같은 맥락에서 String 클래스는 불변 객체이므로 복사 생성자는 되도록 사용하지 말 것
한번 만든 인스턴스 최대한 재활용
자주 사용되는 인스턴스를 캐싱해 메모리 사용량과 가비지 컬렉션 비용을 줄인다.
자주 쓰이는 값들을 상수로 제공하여 캐싱할 수 있다. (ex. BigInteger, Wrapper class 같이 박싱된 기본 타입 클래스 전부)
public 생성자 대신 정적 팩토리를 제공하여 캐싱하면 클라이언트 수정이 없이 필요에 따라 캐시 기능을 덧붙일 수 있다.
불변 객체끼리 내부 데이터 공유 가능
BigInteger 클래스는 부호(sign, int변수)와 크기(magnitude, int배열)를 각각의 필드로 표현한다.
크기는 같고 부호만 반대로 표현하는 negate 메서드는 새로운 BigInteger를 생성하는데, 가변인 배열을 복사하는 대신 원본 인스턴스와 공유한다. 따라서 새로운 인스턴스는 원본 인스턴스가 가리키는 내부 배열을 그대로 가리킨다.
Map과 Set은 값이 바뀌지 않는 구성요소들(keySet, elements)로 이루어져 있다. 이러한 상황에서 불변 객체를 사용하면 안에 담긴 값이 바뀔 일이 없어 적합하다.
불변 객체의 단점
값이 다르면 독립된 객체로 만들어야 하므로 비용이 클 수 있다.
특정 상황에서 잠재적 성능 저하가 발생할 수 있다. 예를 들면 백만 비트짜리 인스턴스에서 단 하나의 비트만 변경할 때 새로운 인스턴스를 생성하면 시간과 공간을 잡아먹는다.
원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려져야 한다면 성능 문제가 극대화된다.
다단계 연산을 제공하거나, 다단계 연산 속도를 높여주는 package-private 가변 동반 클래스를 두어 해결 가능하다. ex) String 클래스의 가변 동반 클래스인 StringBuilder(+StringBuffer)
불변 클래스 만드는 방법
final 클래스로 선언해 상속하지 못하도록 불변 클래스를 만들 수 있다.
모든 생성자를 private 혹은 package-private로 만들고 public 정적 팩토리를 제공하는 방식을 사용하면 더욱 유연하다.
패키지 바깥의 클라이언트에서 볼 때 확장이 불가능해 사실상 final 클래스와 같다.
다수의 구현 클래스를 활용하여 유연성을 제공하고, 객체 캐싱 기능을 추가해 성능을 향상시킬 수도 있다.
생성자와 정적 팩토리 외에 public으로 초기화 메서드를 제공하면 복잡성만 커지고 성능 이점이 없으므로 사용하지 말 것.
그 외
신뢰할 수 없는 클라이언트로부터 final이 아닌 불변 객체가 있다면, 인수로 받은 객체가 '진짜' 해당 객체인지 확인해야 한다. 신뢰할 수 없는 하위 클래스의 인스턴스는 가변 객체로 구현되었을 수 있기 때문에, 일단 가변으로 가정하고 방어적으로 복사해서 사용해야한다. (item50)
직렬화할 때 Serializable을 구현하는 불변 클래스 내부에 가변 객체를 참조하는 필드가 있다면, 공격자가 이 클래스로부터 가변 인스턴스를 만들어낼 수 있다. 따라서 readObject/readResolve 메서드를 반드시 제공하거나, writeUnshared, readUnshared 메서드를 사용해야 한다.(item 88)
Last updated