불변으로 만들자
불변 클래스 한 줄 요약
인스턴스의 내부 값을 수정할 수 없는 클래스
불변 클래스는 생성 될 때 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다. 객체는 상태가 변경됨에 따라 같은 메서드에 다른 방식으로 동작하는 자율적인 행동 주체라고 하지 않았나? 왜 상태를 고정시키려는 걸까? 여러 장점이 있기 때문이다.
불변 클래스의 장점
가변 클래스보다 설계하고 구현하고 사용하기 쉽다. 오류가 생길 여지도 적다. 단순하기 때문이다. 상태가 가변한 인스턴스는, 동일한 인스턴스를 활용하는 곳이 많아질수록 오류가능성이 늘어난다. 내가 사용하려던 상태와 동일한지 확인을 해주지 않으면 원하는 동작을 보장받을 수 없다.
불변 객체는 근본적으로 스레드 안전하기 때문에 따로 동기화할 필요가 없다. 여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.
이 점 덕분에 불변 클래스는 재활용할 수 있고, 캐시를 활용해 성능을 개선할 수 있다.
또한 방어적 복사(item 50)도 필요 없어진다. 아무리 복사해도 원본과 차이가 없기 때문에, 복사 자체가 의미가 없다. 그리하여 불변 클래스는 clone이나 복사 생성자를 제공하지 않는 것이 옳다.
객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면, 불변식을 유지하기 쉽다는 장점이 있다. 좋은 예로 불변 객체는 맵Map의 키와 집합Set의 원소로 쓰기에 안성 맞춤이다. 키가 바뀔 일이 없으니.
불변 클래스는 그 자체로 실패원자성을 제공한다. (itme 76) 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.
불변 클래스의 단점
값이 다르면 반드시 독립된 객체로 만들어야 한다는 점이 단점으로 작용할 수 있다. 값이 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치뤄야 한다.
예컨데 백만 비트 짜리 BigInteger에서 비트 하나를 바꾸려면,
BigInteger moby = ...;
moby = moby.flipBit(0);
Java
복사
flipBit 메서드는 새로운 BigInteger 인스턴스를 생성하기 때문에 원본과 단지 한 비트만 다른 백만 비트짜리 인스턴스를 생성한다.
객체를 완성하기 까지의 단계가 많고, 그 중간 단계에서 만든 객체들이 모두 버려진다면 성능 문제가 더 커진다.
단점을 극복하기 위해
1.
다단계 연산(multistep operation)을 예측하여 제공한다. 더 이상 각 단계마다 객체를 생성하지 않아도 된다. 예컨데 BigInteger는 모듈러 지수 같은 다단계 연산 속도를 높여주는 가변 동반 클래스(companion class)를 package-private로 두고 있다고 한다. 가변 동반 클래스를 구현하는 건 매우 어렵지만, BigInteger의 경우는 다행히 기구현되어 있다.
2.
클라이언트가 원하는 복잡한 연산을 정확히 예측할 수 없다면 public으로 가변 동반 클래스를 제공하는 것이 최선이다. String의 String Builder가 그 예시이다.
불변 클래스를 만드는 방법
다음 다섯가지 규칙을 따르자.
1.
객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
2.
클래스를 확장할 수 없도록 한다.
3.
모든 필드를 final로 선언한다.
4.
모든 필드를 private로 선언한다.
5.
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
이를 위해 단순하게 따를 수 있는 원칙은 모든 필드를 private final로 선언하는 것이다.
확장(상속)하지 못 하도록 하자
가장 쉬운 방법은 final class로 만드는 방법이지만, 더 유연한 방법으로는 모든 생성자를 private 혹은 package-private로 만들고 public 정적 팩터리를 제공하는 방법이다. 바깥에서 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 유연하고, 패키지 바깥에서 클라이언트가 바라본 이 불변 객체는 사실상 final이므로 안전하다.
불변과 관련된 주의사항
1.
클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다. 게터getter가 있다고 무조건 세터setter를 만들지 말자.
2.
모든 클래스를 불변으로 만들 수 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
3.
생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다. 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안 된다. 객체를 재활용할 목적으로 상태를 다시 초기화하는 메서드도 안 된다.
용어정리
한글명 | 영어명 |
절차적 | procedural |
명령적인 | imperative |
전치사 | prepositions |