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

2022. 10. 5. 00:24JAVA/Effective Java

1. 불변 클래스(Immutable Class)란 무엇인가?

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스입니다. 대표적인 불변 클래스로 String, Integer와 같은 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal과 같은 클래스가 불변 클래스에 해당됩니다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 발생할 여지도 적고 훨씬 안전한 클래스입니다.

 

2. 클래스를 불변으로 만들기 위한 5가지 규칙

클래스를 불변 클래스로 만들려면 다음 다섯 가지 규칙을 따르면 됩니다.

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않습니다
    • 대표적인 예가 Setter 메서드가 존재합니다
  2. 클래스를 확장할 수 없도록 합니다
    • 하위 클래스에서 객체의 상태를 변하게 만들 수 있기 때문에 확장하지 못하도록 합니다
    • 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것입니다.
  3. 모든 필드를 final로 선언합니다
  4. 모든 필드를 private로 선언합니다.
    • 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아줍니다.
    • 예를 들어 필드가 Student 클래스의 가변 객체를 참조한다고 가정하면 private로 선언하지 않는다면 Student 인스턴스의 내부 값을 변경할 가능성이 높습니다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 합니다
    • 예를 들어 Person이라는 클래스가 필드로 PhoneNumber라는 가변 객체를 참조한다고 가정하면 Person 클래스외에는 PhoneNumber 가변 객체에 접근하면 안됩니다
    • PhoneNumber 가변 객체는 Getter와 같은 메서드로 그대로 반환해서도 안됩니다. 클라이언트에서 메서드로 접근하여 PhoneNumber의 내부 값을 변경할 가능성이 높습니다
    • 생성자, 접근자(Getter), readObject 메서드 모두에서 방어적 복사(원본은 훼손하지 않고 값 자체를 복사하여 새로운 인스턴스로 반환하는 방법)를 수행해야 합니다

 

3. 불변 클래스 예제

다음 클래스는 복소수(실수부와 허수부로 구성된 수)를 표현한 Complex 클래스입니다. Object 클래스의 메서드 몇개를 재정의했고, 실수부와 허수부 값을 반환하는 접근자 메서드(realPart, imaginaryPart)와  사칙연산 메서드를 정의했습니다. 이 사칙 연산 메서드들은 인스턴스 자신이 가지고 있는 필드 멤버 값을 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환합니다.

 

public final class Complex {
    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);

    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 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 (this == o) return true;
        if (!(o instanceof Complex)) return false;
        Complex complex = (Complex) o;
        return Double.compare(complex.re, re) == 0 && Double.compare(complex.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(re, im);
    }

    @Override
    public String toString() {
        return String.format("(%d + %di)", re, im);
    }
}

 

4. 불변 클래스의 장단점

불변 클래스의 장점

  1. 불변 객체는 스레드 안전하여 따로 동기화할 필요가 없습니다
  2. 불변 객체는 안심하고 공유할 수 있습니다
  3. 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩토리를 제공할 수 있습니다.
    • Integer와 같은 박싱된 기본 타입 클래스 전부와 BigInteger와 같은 클래스가 대표적인 사례입니다
    • 정적 팩토리를 사용하여 여러 클라이언트가 인스턴스를 공유하기 때문에 메모리 사용량과 가비지 컬렉션 비용을 줄여줍니다.
  4. 불변 클래스를 사용하면 값이 변하지 않기 때문에 방어적 복사가 필요하지 않습니다
  5. 불변 객체는 자유롭게 공유할 수 있고, 불변 객체 끼리는 내부 데이터를 공유할 수 있습니다
    • BigInteger 클래스는 내부에서 값이 부호(sign)과 크기(magnitude)를 따로 표현합니다. 부호에는 int 변수, 크기(절대값)에는 int 배열을 사용합니다. 한편 negate 메서드는 크기가 같고 부호만 반대인 새로운 BigInteger를 생성하는데, 이때 배열은 비록 가변이지만 복사하지 않고 원본 인스턴스와 공유합니다.
  6. 불변 객체는 그 자체로 실패 원자성(failure atomicity)을 제공합니다
    • 실패 원장성이란 메서드에서 예외가 발생한 후에도 그 객체는 여전히 메서드 호출 이전과 똑같은 상태여야 한다는 성질입니다.

 

불변 클래스의 단점

  1. 값이 다르다면 반드시 독립된 객체로 만들어야 합니다
    • 예를 들어 BigInteger 인스턴스의 flipBit 메서드는 해당 인스턴스의 비트 하나를 바꾸는 메서드입니다. 이 메서드는 원본과 한비트만 다른 새로운 인스턴스를 생성합니다

 

5. 불변 클래스를 만드는 또 다른 설계 방법

클래스가 불변임을 보장하기 위해서 상속을 하지 못하도록 하는 방법 외에 불변 클래스를 만드는 방법은 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩토리를 제공하는 방법이 존재합니다.

 

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

위와 같이 정의하면 클라이언트는 Complex 인스턴스를 생성하기 위해서 public 정적 팩토리(valueOf)를 이용할 수밖에 없습니다. public이나 protected 생성자가 없기 때문에 다른 패키지에서는 이 클래스를 확장하는게 불가능합니다.

 

6. 정리

  1. Getter가 있다고 해서 무조건 Setter를 만들지 않습니다
  2. PhoneNumber, Complex와 같은 단순한 값을 가지는 객체는 항상 불변 클래스로 만들어야 합니다
  3. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 합니다
    • 꼭 변경해야 할 필드를 뺀 나머지 모두를 final로 선언합니다
    • 다른 이유가 없다면 모든 필드는 private final이어야 합니다
  4. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다
    • 다른 이유가 없다면 생성자와 정적 팩토리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안됩니다
    • 객체를 초기화할 목적으로 상태를 다시 초기화하는 메서드도 안됩니다

 

References

source code : https://github.com/yonghwankim-dev/effective_java/blob/master/src/role17/Complex.java
effective java 3/E