규칙 15.변경 가능성을 최소화하라

변경 불가능 클래스는 그 객체를 수정할 수 없는 클래스이다. 객체 내부의 정보는 객체가 생성돌 때 주어진 것이며, 객체가 살아있는 동안 그대로 보존된다.
변경불가능 클래스를 만드는 이유는 변경 가능 클래스보다 설계하기 쉽고 구현하기 쉬우며, 사용하기도 쉽다. 오류 가능성도 적고, 더 안전하다.

변경 불가능 클래스가 따라야 할 규칙

1.객체 상태를 변경하는 메서드를 제공하지 않는다.
2.계승(상속)할 수 없도록 한다. 보통 final을 붙인다.
3.모든 필드를 final로 선언한다. - 객체에 대한 참조가 동기화 없이 다른 스레드로 전달되어도 안전.
4.모든 필드를 private로 선언한다.
5.변경 가능 컴포넌트에 대한 독점적 접근권을 보장한다. - 클래스에 포함된 변경가능 객체에 대한 참조를 클라이언트는 획득할 수 없어야한다. 그런 필드는 클라이언트가 제공하는 객체로 초기화해서는 안되고 접근자 또한 그런 필드를 반환해서는 안된다. 따라서 생성자나 접근자, readObject 메서드 안에서는 방어적 복사본을 만들어야 한다.

대부분의 변경 불가능 클래스는 함수형 접근법을 사용한다.

특정 연산 후 this객체를 변경하는 대신 새로운 객체를 만들어 반환하는것을 함수형 접근법이라 한다. 이르 사용하는 이유는 피연산자를 변경하는 대신 연산을 적용한 결과를 새롭게 만들어 반환하기 때문이다.

함수형 접근법의 장점

함수형 접근법은 불가능성을 보장하므로 장점이 많다.
1.변경불가능 객체는 단순하다. - 생성될 때 부여된 한 가지 상태만 갖는다. 따라서 생성자가 불변식을 확실히 따른다면 해당 객체는 불변식을 절대로 어기지 않게된다.

변경 불가능 객체의 장단점

1.변경 불가능 객체는 스레드에 안전할수밖에 없다. 어떤 동기화도 필요없으며 여러 스레드가 동시에 사용해도 상태가 훼손될 일이 없다. 그러므로 변경불가능한 객체는 자유롭게 공유할 수 있다.

변경 불가능 클래스는 클라이언트가 기존 객체를 재사용하도록 적극 장려해서 이런 장점을 충분히 살릴 필요가 있다. 그렇게 하는 가장 쉬운 한가지 방법은 자주 사용되는 값을 public static final 상수로 만들어 제공하는 것이다.
가령 Complex클래스는(변경 불가능클래스, 필드가 private final이며 생성자를 통해 한번만 초기화되고 setter또한 없다) 아래와 같이 제공될 수 있다.
public static final Complex ZERO = new Complex(0,0);

2.자주 사용하는 객체를 캐시하여 이미 있는 객체가 거듭 생성되지 않도록 하는 정적 팩터리를 제공할 수있다. - 기존 객체를 공유하므로 메모리 요구량,GC비용이 줄어든다.
3.변경 불가능 객체를 자유롭게 공유할 수 있다는점은 방어적 복사본을 만들 필요가 없다는 뜻이기도 하다. 사실 복사본을 만드는 메소드가 불필요하다.아니 만들면 안된다.(어차피 자기자신과 동일할테니)
4.변경 불가능한 객체는 그 내부도 공유할 수 있다. 다른 클래스가 변경불가능 객체내에 있는 배열필드를 사용해도 된다.
5.변경 불가능 객체는 다른 객체의 구성요소로도 훌륭하다. - 변경 불가능 객체는 맵의 키나 집합의 원소로 활용하기 좋다. (변하지 않으므로)

단점1.변경 불가능 객체의 유일한 단점은 값마다 별도의 객체를 만들어야 한다는 것이다.
이는 단계별로 새로운 객체를 만들고 결국에는 마지막 객체를 제외한 모든 객체를 버리는 연산을 수행해야 하는 경우 성능 문제는 커진다.
이를 해결하기 위한 방법은 두가지가 있다.
첫 번째 방법은 다단계 연산 가운데 자주 요구되는것을 기본연산으로 제공하는 것이다. (BigInteger클래스는 package-private로 선언된 변경 가능 동료클래스를 사용해 모듈라 멱승같은 연산의 속도를 높인다. 이는 다단계 연산이 어떻게 적용될지 확실하게 예측할 수 있을때 쓴다.)
두 번째 방법은 변경 가능한 public 동료 클래스를 제공하는것이다.

변경 불가능 클래스의 두번째 구현방법

보통은 하위 클래스 정의가 불가능하도록 하기 위해 final로 선언하지만 다른방법도 있다.
이는 모든 생성자를 private나 package-private로 선언하고 public 생성자 대신 public 정적 팩터리를 제공하는 것이다.

방어적 복사?

BigInteger, BigDecimal은 클래스가 만들어질 당시 변경 불가능 클래스를 final로 선언해야 한다는 사실을 이해하지 못했기 때문에 final로 되어있지 않다. 즉 상속을 할 경우 해당 클래스는 변경이 가능할 수도 있다.
만일 신뢰할 수 없는 클라이언트가 전달한 BigInteger나 BigDecimal 인자의 변경 불가능성에 보안이 좌우되는 클래스를 작성할 때는 전달된 인자가 BigInteger나 BigDecimal의 하위 클래스가 아니라 진짜 BigInteger나 BigDecimal 클래스 인지 확인해야 한다. 만일 하위 클래스 객체라면 해당 객체가 변경 가능한 객체일지도 모른다는 가정하에 방어적복사를 시행해야 한다.

default
1
2
3
4
5
public static BigInteger safeInstace(BigInteger val) {
if(val.getClass() !=BigInteger.class)
return new BigInteger(val.toByteArray());
return val;
}

요약

변경 가능한 클래스로 만들 타당한 이유가 없다면 변경 불가능 클래스로 만들어라.
변경 불가능한 클래스로 만들 수 없다면, 변경 가능성을 최대한 제한하라.

특별한 이유가 없다면 생성자 이외의 public 초기화 메서드나 정적 팩터리 메서드를 제공하지 마라.

Share