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

8882 단어 JavaJava

아이템 17

변경 가능성을 최소화하라

*불변 클래스: 인스턴스의 내부 값을 수정할 수 없는 클래스
자바 플랫폼에는 예를 들어 String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal가 있다.

불변 클래스로 설계하는 이유는 (1) 가변보다 사용하기 쉬우며, (2) 오류가 생길 여지가 적고 안전하다.

불변 클래스의 규칙

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다. (Setter)
  2. 클래스를 확장할 수 없도록 한다.
    상속을 막는 대표적인 방식은 final로 선언하는 것이다.
  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private으로 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다. 참조를 얻을 수 없게 하는 대신, 방어적 복사를 수행해야 한다.

불변 클래스

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 Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im); //새로운 객체 반환
    }
}

위의 코드를 보면, 인스턴스 자신은 수정하지 않고 새로운 인스턴스를 만들어 반환한다. (함수형 프로그래밍 패턴이라 한다.) 즉, 생성된 시점의 상태를 파괴될 때까지 그대로 간직하고 있다.

불변 클래스의 특징

  1. 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.
    여러 스레드가 동시에 사용해도 훼손되지 않는다. 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변 객체는 안심하고 공유할 수 있다.
    가장 쉬운 재활용 방법으로는 자주 쓰이는 값을 상수 public static final 로 제공하는 것이다.

    //상수 예시
    pulbic static final complex ZERO = new complex(0, 0);
  2. 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
    예를 들어, BigInteger 클래스는 부호를 int변수, 크기를 ìnt배열을 사용한다. 여기서 negate 메서드는 크기는 같고 부호만 반대인 새로운 BigInteger를 생성하는데, 크기는 복사하지 않고 원본 인스턴스와 공유해도 된다.

  3. 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
    값이 바뀌지 않는 구성요소들로 이뤄진 객체라면 불변식을 유지하기에 수월하다. 예를 들어 불변 객체는 맵의 키와 Set의 원소로 쓰기에 좋다.

  4. 불변 객체는 그 자체로 실패 원자성을 제공한다.
    failure atomicity: 메서드에서 예외가 발생한 후에도 객체 상태는 메서드 호출 전과 같아야 한다.

  5. 불변 클래스는 값이 다르면 반드시 독립된 객체로 만들어야 한다.
    비트 하나라도 다르다면 비트 하나만 다른 새로운 인스턴스를 생성하도록 한다.

    대처하는 방법 중 하나는 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법이다. 다단계 연산을 기본으로 제공한다면 더 이상 각 단계마다 객체를 생성하지 않아도 된다. (BigInteger는 모듈러 지수 같은 다단계 연산 속도를 높여주는 가변 동반 클래스를 Package-private으로 두고 있다.) 복잡한 연산을 예측하기 어렵다면 public 으로 제공한다.

불변 클래스 설계 방법

자신을 상속하지 못하게 해야 한다.
즉, 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법이다.

public final class Complex {
    private final double re;
    private final double im;
    
    private Complex(double re, double im) { //생성자는 private
        this.re = re;
        this.im = im;
    }
    
    public static Complex valueOf(double re, double im){ //정적 팩터리 
    	return new Complex(re, im);
    }
}

BigInteger과 BigDecimal는 재정의할 수 있도록 설계가 되어 있다. 이 경우, 신뢰할 수 없는 하위 클래스의 인스턴스라고 확인된다면 방어적으로 복사해 사용해야 한다.

public static BigInteger safeInstane(BigInteger val) { //클래스 확인
        return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
    }

정리

  1. 게터가 있다고 해서 세터를 만들지는 말자. (불변을 위해)
  2. 단순한 값 객체 (예를 들어 phoneNumber, Complex)는 항상 불변으로 만들자
  3. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
    변경해야 할 필드를 뺀 나머지 모두는 합당한 이유가 없다면 private final로 선언하자
  4. 생성자는 불변식 설정이 모두 완료된 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
    초기화 메서드 또한 public으로 제공해서는 안된다. (객체 재활용 목적의 초기화 메서드 또한 마찬가지)

좋은 웹페이지 즐겨찾기