[Effective Java] 15. 변경 가능성을 최소화하라

3 분 소요

immutable class란 변경 불가능한 클래스로 객체가 생성되고 난 이후 그 객체 값의 변경이 불가능한 class를 말한다.

자바에서는 String, 기본자료형 wrapper, BigInteger, BigDeciaml 등이 이러한 class 이다.

변경 불가능한 클래스는 변경 가능 클래스보다 설계하기 쉽고 구현이 쉬우며 사용도 쉽다. 또한 오류 가능성이 적어 더 안전하다.

1. Immutable Class의 규칙

  1. 객체 상태를 변경하는 메서드를 제공하지 않는다.

  2. 상속이 불가능하게 한다.(final class로 만들면 된다.)
    상속을 할 경우 하위 클래스가 부모 클래스의 immutable 속성을 깨트릴수 있다.

  3. 모든 맴버 필드를 final로 선언한다.
    final로 선언시 자바 메모리 모델에 명시된바와 같이 객체가 synchronization 없이도 다른 스레드에 전달될때 안전하다.

  4. 모든 멤버 필드를 private로 선언한다.
    굳이 private으로 선언하지 않아도 immutable class를 만들 수 있지만 굳이 추천하고 싶은 방법은 아니란다.

  5. 내부 멤버 중 변경 가능한 class의 instance가 있을때는 독점적인 접근권을 보장해야 한다.
    해당 멤버는 외부에서 생성된 객체로 초기화 하는 행위를 해서는 안되고, getter 또한 instance를 그대로 리턴해서는 안된다.

외부에서 생성된 객체로 초기화를 할 경우 외부에서 해당 멤버 instance를 가지고 있기 때문에 수정이 가능해버린다. 따라서 반드시 객체 생성은 immutable class 내부에서 이뤄져야 한다.

또한, getter와 같은 접근자가 해당 instance를 그대로 리턴하면 역시 외부에서 값을 수정할 수 있게 되므로 지양해야 한다.

참고로 이는 다소 과한 규칙이다.

2. Immutable class 예

public final class Complex {
    // 모든 멤버 필드는 private final로 만든다.
    private final double re;
    private final double im;
	
    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
	
    // getter와 대응되는 setter는 만들지 않는다.
    public double realPart() { return re; }
    public double imaginaryPart() { return im; }
	
    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
	
    public Complex subtract(Complex c) {
        return enw Complex(re - c.re, im - c.im);
    }
}

대부분의 immutable class의 형태이다.

3. Immutable class의 장점

3.1. 단순하고, 안정적이다.

객체는 생성될때 부여한 한가지 속성만을 가지게 되므로 안정적이고 형태가 단순하다.

3.2. Thread safe 하다.

변경이 불가능하기 때문에 thread safe 할 수 밖에 없어 동기화가 필요 없다. 그래서 Thread간에 자유롭게 공유할 수 있다.

3.3. 자주 사용하는 객체를 캐시하는 형태의 구현이 가능하다.

변경 불가능한 객체는 자주 사용하는 객체를 캐시하여 이미 있는 객체가 중복으로 생성되지 않도록 하는 static factory method를 제공할 수 있다.

즉, static factory method를 통해 외부에서 객체 생성을 요청할때 객체 생성 후 이를 메모리에 가지고 있고, 이후 다시 같은 값을 요구할때 해당 객체를 리턴해주는 것이다.

Immutable class의 특징 덕분에 어차피 같은 상태를 같은 객체가 생성될 것이라 그대로 재활용이 가능하다.

static factory method는 규칙1 참고

3.4. 방어적 복사본을 만들 필요가 없다.

애초에 객체의 복사본을 만들 필요 자체가 없다. 따라서 immutable class에는 clone 메서드나 복사 생성자를 만들 필요가 없고 만들어서도 안된다.

방어적 복사본은 규칙 39참고. 복사 생성자는 규칙 11 참고.

3.5. 다른 객체의 구성요소로 좋다.

예를 들어 Map, Set을 들 수 있다. 변경 불가능한 객체의 경우 Map의 Key나 Set의 구성요소로 활용할 수 있다.

4. Immutable class의 단점과 개선

값마다 별도의 객체를 가지고 있어야 한다. 만약 객체의 생성 비용이 높다면 성능 문제가 있을 수 있다.

이러한 성능 문제를 해결하기 위해 자주 요구되는 연산은 기본연산으로 따로 제공하는 방법이 있다.

4.1. companion class의 사용

예를 들어 BigInteger class는 package-private으로 선언된 변경 가능한 동료 클래스(companion class)가 있다. 이러한 클래스를 이용해 모듈라 거듭제곱 같은 복잡한 연산의 계산 속도를 높인다.

참고로 package-private으로 선언된 companion class를 사용하는 방법은 Immutable class의 연산 과정에서 이를 언제 사용해야 하는지 Client가 명확히 알고 있을때 쓸 수 있다.

그렇지 않다면 public으로 선언된 companion class를 사용하는 방법이 최선이다. 자바에서 String 생성을 위해 StringBuilder를 제공하는 것이 그러한 예이다.

5. Immutable Class를 만드는 다른 방법

보통 class를 final로 만들지만 이보다 더 유연한 방법도 있다.

모든 생성자를 private 또는 package-private으로 선언하고 public 생성자를 대신하여 public static method를 제공하는 방법이다.

서두에서 본 Immutable class의 규칙은 다소 과하며 성능 향상을 위해서는 다소 완화할 수도 있다. 중요한것은 어떤 메서드도 외부에서 관측 가능한 상태를 변화시키지 않는다 라는 것이다.

6. Serialize 관련하여 주의할 점

Immutable class가 serializable 인터페이스를 구현하고 있는데 해당 클래스 내부에 mutable 객체를 참조하는 부분이 있다면 반드시 readObject 메서드나 readResolve 메서드를 제공해야 한다.
그렇지 않다면 ObjectOutputStream.writeUnshareObjectInputStream.readUnshare 메서드를 반드시 사용해야 한다.

그렇지 않으면 시스템을 공격하고자 하는 사람이 Immutable class로 부터 mutable 객체를 만들 수 있게 된다.

규칙 76 참고

7. 정리

  1. 모든 get 메서드에 대응하는 set 메서드를 두는 것을 피하라.

  2. mutable class로 만들 타당한 이유가 없다면 immutable class로 만들어라.

  3. immutable class로 만들기 어렵다면 가능한 변경 가능성을 최소화 하라. (특별한 이유가 없다면 필드는 final로 선언하라.)

  4. 생성자는 초기화가 끝나서 불변식이 만족되는 객체를 만들어야 한다. 이것 외에 따로 public 초기화 메서드를 만들지 마라. 코드 복잡성만 높아진다.

댓글남기기