Java
[Effective Java] 아이템 17. 변경 가능성을 최소화하라.
jun9.com
2022. 4. 16. 00:27
1. 불변 클래스란?
- 인스턴스 내부 값을 수정할 수 없는 클래스
- 즉 인스턴스에 저장된 정보는 객체가 해제되기 전까지 절대 변하지 않는다.
- 장점
- 설계, 구현하기 쉽고, 사용하기도 쉽다.
- 값이 변하지 않기 때문에 오류가 생길 여지가 적어 안전하다.
- ex) String, Integer, Boolean, BigInteger, BigDecimal ...
2. 불변 클래스 만들 때 지켜야할 규칙
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- setter메서드 같이 외부에서 객체의 내부 상태를 바꿀 수 있는 메서드를 제공하면 불변이 깨지기 때문
- 클래스를 확장할 수 없도록 한다.
- 하위클래스에서 객체의 상태를 바꿀 수 있기 때문에.
- 클래스를 final로 선언하거나, 생성자를 private으로 바꾸고 public 정적 팩터리 메서드를 제공(아래 5번에서 다시 설명)하여 클래스를 확장할 수 없도록 한다.
- 모든 필드를 final로 선언한다.
- 변경할 수 없는 필드라는 설계자의 의도를 명확히 드러내는 방법이다.
- 새로 생성된 인스턴스를 동기화없이 다른 스레드로 건네도 문제없이 동작함이 보장된다.
- 모든 필드를 private로 선언한다.
- 필드가 참조하는 가변객체를 클라이언트에서 직접 수정할 수 없도록 한다.
- public static 만으로도 불변 객체가 되지만 -> 내부 표현을 바꾸려면 외부 API 변경이 동반되므로 권하지는 않는다. (아이템 15,16)
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 객체 내부에서 가변 컴포넌트를 참조하는 필드가 있다면, 클라이언트에서 그 객체를 참조할 수 없도록 해야 한다.
- 생성자, 접근자, readObject 메서드에서 방어적 복사를 수행해라!
3. 불변 객체의 특징
- 불변 객체는 단순하다.
- 앞서 말한대로 생성된 시점의 상태를 파괴될 때까지 간직한다.
- 예상치 못한 상황에 놓일 수 있는 가변 객체와 다르게, 프로그래머가 별다른 노력을 들이지 않아도 영원히 불변이다.
- thread-safe하여 따로 동기화할 필요 없다.
- 불변이기 때문에 여러 스레드가 동시에 사용해도 훼손되지 않는다. 때문에 문제없이 공유할 수 있다.
- 웬만하면 한 번 만든 인스턴스를 재활용하여 메모리 사용량과 가비지 컬렉션 비용을 줄이자.
- ex) 자주 쓰이는 값은 상수로 제공해라. 어차피 몇 번 계산해도 같은 값이기 때문에 상수로 넣어놓을 수 있다!
- 불변이기 때문에 여러 스레드가 동시에 사용해도 훼손되지 않는다. 때문에 문제없이 공유할 수 있다.
- 자유롭게 공유할 수 있고, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
- ex) BigInteger 클래스 (-> 불변 객체)
- int 부호
- int[] 크기
- negate()메서드는 크기는 같고 부호만 반대는 새로운 BigInteger 인스턴스를 생성하는데,
- 배열은 가변이지만 복사하지 않고 원본 인스턴스와 공유해도 된다.
- 실제로 새로 만든 인스턴스도 원본이 가리키는 내부 배열을 그대로 가리킨다.
- ex) BigInteger 클래스 (-> 불변 객체)
- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용할 수 있다.
- 내부가 아무리 복잡해도 많은 불변 객체로 이루어져 있다면 불변식을 유지하기 훨씬 쉬워진다.
- 맵이나 집합에서 불변객체를 사용하기 좋다. (값이 바뀌면 안되는데 불변 객체는 이를 충족하기 때문에)
- 예외가 발생해도 객체가 같은 상태이다. (= 실패 원자성을 제공한다.)
- 상태가 절대 변하지 않으니 불일치 상태에 빠질 가능성이 아예 없다.
4. 불변 객체의 단점은??
- 값이 다르다면 반드시 독립된 객체로 만들어야 한다.
- 값의 가짓수가 많다면 모두 새로 만드는데 많은 비용이 든다.
sol) 가변 동반 클래스 (ex: StringBuilder)
5. final 대신 자기자신을 상속하지 못하게 하는 방법 (불변임을 보장하기 위함)
public class Complex {
private final double re;
private final double im;
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);
}
...
- 모든 생성자를 private 또는 package-private으로 바꾸고 public 정적 팩터리 메서드를 제공한다.
- public, protected 생성자가 없기 때문에 다른 패키지에서는 이 클래스를 확장하는게 불가능
- 바깥에서는 볼 수 없는 구현 클래스를 원하는 만큼 만들어 활용할 수 있다.
- 유연하다. 다음 릴리스에서 객체 캐싱 기능을 추가해 성능을 끌어올릴수도 있다.
6. 기타 팁들
- BigInteger, BigDecimal은 하위 객체가 있을 수 있으므로 주의하자.
- 당시 final로 구현되어야 한다는 사실이 알려져 있지 않았기 때문에, 하위 클래스가 있을 수 있다.
- getter가 있다고 무조건 setter를 만들 필요는 없다.
- 꼭 필요한 경우가 아니라면 불변이어야 하는데 setter로 값을 바꿀 수 있기 때문이다.
- 불변 클래스가 아니더라도 변경할 수 있는 부분을 최소한으로 줄이자.
- 변경해야 할 필드를 뺀 나머지 모두를 final로 선언. -> 합당한 이유가 없다면 모든 필드는 private final
- 불변식 설정, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
- 명확한 이유가 없다면 생성자, 정적 팩터리 메서드 외에는 그 어떤 메서드도 public으로 제공해서는 안된다.