불변 클래스란?

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

불변 클래스는 가변 클래스보다 구현 및 사용하기가 쉬우며, 오류가 발생할 여지도 적고 안전하다.
가변 클래스의 경우에는 인스턴스를 생성하고 나서 중간에 값이 변경될 수 있고, 이를 알아채지 못하면 오류를 발생하는 코드를 작성하기 쉽다.

클래스를 불변으로 만드는 규칙 5가지

  • 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
    • 부주의하게 객체 상태를 변하게 만드는 사태를 막는다.
    • final, 정적 팩토리
  • 모든 필드를 final로 선언한다.
    • 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법
  • 모든 필드를 private으로 선언한다.
    • 필드가 참조하는 가변 객체를 직접 접근해 수정하는 일을 막아준다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근하지 못하도록 한다.
    • 가변 객체를 참조하는 필드가 하나라도 있다면 그 객체의 참조를 얻을 수 없게 해야 한다.
    • 방어적 복사를 수행할 것.

불변 복소수 클래스

public final class Complex { 
    private final double re; 
    private final double im; 
    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); 

    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.im + 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); 
    } 

    @Override 
    public boolean equals(Object o) { 
        if (o == this) return true; 
        if (!(o instanceof Complex)) return false; 
        Complex c = (Complex) o; 
        return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0; }  
    
    @Override 
    public int hashCode() { 
        return 31 * Double.hashCode(re) + Double.hashCode(im); 
    } 

    @Override 
    public String toString() { return "(" + re + " + " + im + "i)"; } 


    // 생성자 대신 정적 팩터리를 사용한 불변 클래스
    // private 생성자 + 정적 팩토리 메서드  
    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); 
    } 
}

위 코드는 불변 복소수 클래스이다.
클래스는 각 속성을 반환하는 접근자만 제공할 뿐 수정할 수 있는 메서드는 제공하지 않는다.

함수형 프로그래밍

사칙연산 메서드를 살펴보면 특이점이 있다. 인스턴스 자체를 수정하지 않고 새로운 인스턴스(new Complex)를 생성해서 반환한다.
이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.

장점

코드에서 불변이 되는 영역의 비율이 높아진다.

  • 불변 객체는 단순하다.
    • 생성시점의 상태를 파괴될때까지 그대로 간직한다.
  • 근본적으로 thread-safe하여 따로 동기화할 필요 없다.
    • 여러 스레드가 동시에 접근해도 절대 훼손되지 않는다.
    • 불변 객체는 안심하고 공유할 수 있다.
      • 따라서 최대한 재활용할 것 (메모리문제도 있음)

불변 클래스 단점

값을 나타내는 클래스를 주로 불변 클래스로 설계한다. 이 때, 각 값이 다르면 독립된 객체를 만드는 것이 단점이다.
값이 가짓수가 많으면 이들을 모두 생성하는데 큰 비용이 들기 때문이다.

  • BigInteger 클래스의 flipBit 메서드
    • 매번 새로운 BigInteger 인스턴스를 생성
    • 이러한 연산은 BigInteger의 크기에 비례해 시간과 공간을 모두 잡아 먹음

반면에 BitSet은 BigInteger와 달리 가변 클래스이다. 따라서 원하는 비트를 빠르게 바꿔주는 메서드를 제공한다.
이러한 불변 클래스의 단점을 대처하기 위해서 가변 동반 클래스를 두곤 한다.

정리

1) 클래스는 특정 예외 상황을 제외하고 되도록이면 불변이어야 한다.
2) 불변으로 만들 수 없는 클래스라도 변경 가능한 범위는 최소한으로 줄이자.
3) 다른 이유가 없다면 모든 필드는 private final로 선언하자.
4) 생성자는 불변식 설정이 완료되고, 초기화가 완벽히 끝난 상태에서 호출(객체 생성)을 해야 한다.

댓글남기기