해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.



※ 변경 불가능 클래스

: 변경 불가능 클래스는 객체를 수정할 수 없는 클래스이다. 객체를 생성할 때 내부 정보가 다 채워지며 객체가 살아있는 동안 그 값이 보존된다. 변경 불가능 클래스를 사용하는 이유는 설계,구현,사용면에서 쉽다. 안전하고 오류 가능성도 적다. 


- 변경 불가능 클래스 작성 규칙

1. 객체 필드를 변경하는 수정자 메서드를 제공하지 않는다.

2. 상속이 불가능하게 final로 선언한다. 상속을 하게 되면 악의적으로 필드 값을 변경하는 것처럼 보이게 할 수 있다.

3. 모든 필드는 final로 선언해 변경을 막는다. 그러면 동기화 없이 스레드들의 접근에 안전한다

4. 모든 필드는 private로 선언한다. 클라이언트가 필드에 참조에 접근하는 일을 막는다. 

5. 변경 불가능 클래스에 포함된 변경 가능한 객체에 대한 참조를 외부(클라이언트)에서 불가능하게 해야 한다. 그런 필드는 클라이언트가 제공하는 객체로 초기화해서는 안 되고, 접근자가 그런 필드를 반환해서는 안 된다. 따라서 생성자나 접근자, readObject 메서드 안에서는 방어적 복사본을 만들어야 한다.


아래 코드는 위 사항에 맞추어 변경 불가능 클래스를 작성한 예이다.

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;
    }
    // 수정자가가 없는 접근자들이다.
    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 new Complex(re - c.re, im - c.im);
    }
 
    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
    }
 
    public Complex divide(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re
                * c.im)
                / tmp);
    }
 
    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Complex))
            return false;
        Complex c = (Complex) o;
 
        return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
    }
 
    @Override
    public int hashCode() {
        int result = 17 + hashDouble(re);
        result = 31 * result + hashDouble(im);
        return result;
    }
 
    private int hashDouble(double val) {
        long longBits = Double.doubleToLongBits(re);
        return (int) (longBits ^ (longBits >>> 32));
    }
 
    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}
위에서 사칙연산을 하는 접근자 메서드를 보면 모두 this를 반환하는 것이 아닌 생성자로 새로운 객체를 반환하는 것을 알 수 있다. 대부분의 변경 불가능 클래스가 따르는 패턴이다. 

변경 불가능한 클래스를 사용함으로써 얻을 수 있는 장점은 여러 개 있다. 일단, 변경 불가능 객체는 스레드에 안전에서 동기화할 필요가 없다. 따라서 자유롭게 공유가 가능하다. 그런 예 중 하나가 public static final 상수로 만들어 제공하는 것이다. 더해서 정적 팩터리 메서드를 사용해 캐시 기능을 구현해 추가적인 메모리 소요를 안 할 수 있다. 또한 변경 불가능한 객체는 다른 객체의 구성요소로도 훌륭하다. 예로 맵(map)과 집합(set)을 들 수 있다. 변경 불가능 객체는 맵의 키나 집합의 원소로 활용하기 좋다. 한 번 집어넣으면 그 값이 변경되지 않아 맵과 집합의 불변식이 깨질 걱정은 하지 않아도 된다.

하지만 변경 불가능 객체에는 단점이 있다. 값마다 새로운 객체를 만들어야 된다. 만약 객체 생성 비용이 높다면 이는 문제가 될 수 있다. 또한 새로운 객체들을 만들고 마지막 객체를 제외한 모든 객체를 버리는 연산을 수행할 때도 문제가 된다. 이에 대한 해결책으로는 동료 클래스를 사용하는 것이다. 예로 String 객체에 대한 변경 가능 클래스 StringBuffer가 있다. 


변경 불가능 클래스 작성 규칙에서 클래스에 final을 선언하라고 했는 데 더 유연하게 작성할 수 있는 방법이 있다. 모든 생성자를 private나 package-private로 선언하고 public 생성자 대신 public 정적 팩터리를 제공하는 것이다. 해당 방법으로 위 예제를 바꿔보겠다.

     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);
    }

위와 같이 사용하면 정적 팩터리의 장점들을 가질 수 있다. 또한 패키지 외부 입장에서는 public이나 protected로 선언된 생성자가 없기 때문에 final로 선언한 거나 마찬가지가 된다. 


변경 불가능 클래스에서 모든 필드를 final로 선언할 필요는 없다. 성능 향상을 위해서 규칙을 완화할 수도 있다. 예를 들어 특정 변경 불가능 클래스는 시간이 많이 걸리는 계산에 대해서 비final 필드에 캐시해 둔다. 하지만 주의할 게 만약 변경 불가능 클래스에서 변경 가능 객체를 참조하는 필드가 있다면 직렬화를 구현할 때 readObject 나 readResolve 메서드를 반드시 제공해야 한다. 아니면 ObjectOutputSteam, writeUnshared나 ObjectInputStream.readUnshared 메서드를 반드시 사용해야 한다.



모든 get 메서드마다 set 메서드를 만들 필요는 없다. 변경 가능 클래스로 만들어야할 이유가 없다면 변경 불가능 클래스로 만들어라. 만약 변경 가능 클래스로 만들어야 한다면 변경 가능성을 최대한 줄이고 특별한 이유가 없다면 모든 필드에 대해서 final로 선언해라.


+ Recent posts