티스토리 뷰

불변클래스

  • 간단히 말해 그 인스턴스 내부 값을 수정할 수 없는 클래스이다.
  • 불변 인스턴스의 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
  • 대표적인 불변 클래스 ex. String, Wrapper class, BigInteger, BigDecimal

불변클래스의 장점

  • 가변 클래스보다 설계하고 구현하고 사용하기 쉽다.
  • 오류가 생길 여지가 적다.
  • 안전하다.
  • 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
  • 근본적으로 스레드 안전하여 따로 동기화 할 필요가 없다.
  • 안심하고 공유할 수 있다.

불변클래스를 만드는 법

  • 객체의 상태를 변경하는 메소드(변경자)를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
    • 하위클래스에서 객체의 상태를 변경하는 사태를 막아준다.
    • 대표적인 방법은 클래스를 final 로 선언하는 것이다.
  • 모든 필드를 final로 선언한다.
    • 생성된 인스턴스를 다른 스레드에서 문제없이 동작하게끔 보장하는데 필요하다.
  • 모든 필드를 private로 선언한다.
    • 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
    • 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안되며, 접근자 메소드가 그 필드를 그대로 반환해서도 안된다.
    • 생성자, 접근자, readObject 메소드 모두에서 방어적 복사를 수행하라.
// 예시. 불변 복소수 클래스
public final class Complex {
    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 plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.re + im * c.re);
    }

    public Complex dividedBy(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));
    }

    // equals, hashCode, toString ..
}
  • 사칙연산 메소드들이 인스턴스 자신을 수정하는게 아니라 새로운 Complex 인스턴스를 만들어 반환한다.
  • 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.
  • 메소드 이름으로 동사형 add 대신 전치사 plus 를 사용했다. 이는 해당 메소드가 객체의 값을 변경한게 아니라는 사실을 강조하려는 의도이다.

불변클래스 재활용

  • 불변 클래스는 한 번 만든 인스턴스를 최대한 재활용하기를 권한다.
  • 가장 쉬운 재활용 방법은 자주 쓰이는 값들을 상수(public static final)로 제공하는것이다.
  • ex. 박싱된 기본 타입 클래스, BigInteger
  • 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩토리를 제공할 수 있다.
  • 이런 정적 팩토리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
  • 새로운 클래스를 설계할 때 public 생성자 대신 정적 팩토리를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙일 수 있다.
  • 불변 객체를 자유롭게 공유할 수 있다는 점은 방어적 복사도 필요 없다는 결론으로 이어진다.
  • 그러니 불변 클래스는 clone 메소드나 복사 생성자를 제공하지 않는게 좋다.
// Complex.class 
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

성질

객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.

  • 값이 바뀌지 않는 구성요소들로 이루어진 객체라면 불변식을 유지하기 훨씬 수월하기 때문이다.
  • ex. 불변 객체는 Map의 key와 Set의 원소로 쓰기에 안성맞춤이다. Map이나 Set은 안에 담긴 값이 바뀌면 불변식도 허물어지는데, 불변 객체 사용시 그런 걱정이 필요가 없다.

불변 객체는 그 자체로 실패 원자성을 제공한다.

  • 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.
  • 원자성 (atomicity): 메소드에서 예외가 발생한 후에도 그 객체는 여전히 호출전과 유효한 상태 상태여야 한다.

"모든 필드가 final 이고 어떤 메서드도 그 객체를 수정할 수 없어야 한다"

"어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다"

불변 클래스의 단점

  • 값이 다르면 반드시 독립된 객체로 만들어야 한다.
    • 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 한다.
  • 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어서 버려진다면 성능 문제가 더 불거진다.

불변클래스의 성능 대처 방법

  • 다단계 연산 (multistep operation)
    • 다단계 연산을 기본으로 제공한다면 각 단계마다 객체를 생성하지 않아도 된다.
    • ex. String → StringBuilder 의 append()

불변클래스를 만드는 법

  1. final class 만들기
  2. 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩토리메소드 제공
// 예시 2
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);
    }

    // ...
}

BigInteger, BigDecimal

  • 이전 설계당시의 문제점으로 인해 불변이라는 보장이 없다.
  • 만약 신뢰할 수 없는 클라이언트로부터 BigInteger나 BigDecimal의 인스턴스를 받는다면 주의해야 한다.
  • 신뢰할 수 없는 하위 클래스의 인스턴스라고 확인되면, 가변이라 가정하고 방어적으로 복사해 사용해야 한다.
// 방어적 복사의 예시
public static BigInteger safeInstance(BigInteger val) {
    return val.getClass() == BigInteger.class ? 
        val : new BigInteger(val.toByteArray());
}

결론

  • getter가 있다고 무조건 setter를 만들지는 말자.
  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 불변으로 만들 수 없더라도 변경할 수 있는 부분을 최소한으로 줄이자.
  • 꼭 변경해야 할 필드를 제외한 나머지 모두를 final로 선언하자.
  • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
  • 확실한 이유가 없다면 생성자와 정적팩토리 외의 초기화 메서드는 public으로 제공해서는 안된다.