본문 바로가기
Effective Java

[Effective Java] 아이템17 변경 가능성을 최소화하라

by byeongoo 2021. 2. 17.

■ 불변 클래스란?

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 클래스는 가변 클래스보다 설계하고 구현하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다. 불변 객체는 단순하며 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다. 

 

■ 불변 클래스 생성 규칙

  • 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  • 클래스를 확장할 수 없도록한다.
    • 클래스 final 선언
    • private 생성자
  • 모든 필드를 final로 선언한다.
  • 모든 필드를 private로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행하라.

다음은 불변 복소수 클래스이다. 이 클래스는 복소수(실수부, 허수부로 구성된 수)를 표현한다. 사칙 연산 메서드(plus, minus, times, divideBy)를 정의하고 있다. 이 사칙연산 메서드들이 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환하는 모습에 주목하자. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다. 

 

이와달리 절차적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다. 또한 메서드 이름으로 add와 같은 동사대신 plus같은 전치사를 사용한 점에도 주목하자.

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

    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 divdedBy(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)";
    }
}

위의 Complex 클래스 코드에서 생성자를 private로 하고 정적 팩토리 메소드를 이용하여 불변 클래스를 생성한다면 훨씬 유연하게 사용할 수 있다. 이 방식이 최선인 경우가 많다. 패키지 바깥의 클라이언트에서 바라본 이 불변 객체는 사실상 final이다. public이나 protected 생성자가 없으니 다른 패키지에서는 이 클래스를 확장하는게 불가능하기 때문이다. 정적 팩터리 방식은 다수의 구현 클래스를 활용한 유연성을 제공하고, 이에 더해 다음 릴리스에서 객체 캐싱 기능을 추가해 성능을 끌어올릴 수도 있다.

 

 

■ 불변 클래스 장점

  • 스레드에서 안전하여 따로 동기화할 필요가 없다. 따라서 안심하고 사용할 수 있다.
  • 불변 클래스의 경우 한번 만든 인스턴스를 최대한 재활용하면 메모리 사용량과 가비지 컬렉션 비용을 줄일 수 있다.

 

■ 불변 클래스 단점

값이 다르면 반드시 독립된 객체로 만들어야 한다. 또한 불변 객체의 경우 복사를 해도 결국 원본과 똑같으므로 복사 자체가 의미가 없다. 따라서 clone 메서드나 복사 생성자를 제공하지 않는다.

 

■ 정리

  • 무작정 Setter 메서드를 만들지 말자
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자
  • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다.