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

반응형
728x90
반응형

불변 클래스

불변 클래스란, 그 인스턴스의 내부 값을 수정할 수 없는 클래스이다. 불변 인스턴스에 간직된 정보는 고정되어 절대 달라지지 않는다. 자바 플랫폼 라이브러리에서 불변 클래스로 예를 든다면, String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal 등이 존재한다. 불변 클래스를 설계한 이유는 상태가 바뀌지 않는 특성 덕분에, 가변 클래스보다 설계 및 구현과 사용이 쉽고 오류가 생길 여지가 적고 훨씬 안전하기 때문이다. 

 

 

불변 클래스로 만들기

1) 객체의 상태를 변경하는 메서드 (setter) 를 제공하지 않는다.

2) 클래스를 확장할 수 없도록 한다. 예를 들면 클래스를 final 로 선언하여 상속을 막아서 하위 클래스에서 변경할 수 없도록 한다.

3) 모든 필드를 final 로 선언한다.

4) 모든 필드를 private 로 선언한다. 

5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야한다. 이런 필드는 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안되며, 접근자 메서드가 그 필드를 그대로 반환해서도 안된다. 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행해야한다.

 

 

함수형 프로그래밍

import lombok.EqualsAndHashCode;
import lombok.ToString;

@EqualsAndHashCode
@ToString
public 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 imagingPart() {
        return im;
    }

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
}

 

plus 메서드를 보면, 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어서 반환한다. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 '함수형 프로그래밍' 이라고 한다. 

 

이 방식으로 프로그래밍한다면, 코드에서 불변이 되는 영역의 비율이 높아진다. 불변 객체는 단순하다. 불변 객체는 생성된 시점의 상태를 파괴될때까지 수정없이 그대로 유지된다. 모든 생성자가 클래스 불변식을 보장한다면 그 클래스를 사용하는 프로그래머가 다른 노력을 들이지 않더라도 불변으로 남는다.

 

불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다. 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변 객체는 안심하고 공유할 수 있다.

 

 

불변 클래스의 인스턴스 캐싱

@EqualsAndHashCode
@ToString
public 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);
    
    ...
    
 }

 

불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다. 새로운 클래스를 설계할 때 public 생성자 대신 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙힐 수 있다. 

 

불변객체는 자유롭게 공유가 가능하기 때문에, 불변 클래스에서의 clone 메서드나 복사 생성자를 제공하지 않는게 좋다. 

 

 

불변클래스의 장점

1) 불변 객체는 자유롭게 공유할 수 있다.

2) 불변 객체끼리는 내부 데이터를 공유할 수 있다.

3) 객체를 만들때 다른 불변 객체들을 구성요소로 사용하면, 아무리 복잡하더라도 불변식을 유지하기 훨씬 수월하다.

4) 불변 객체는 그 자체로 실패 원자성을 제공한다. 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.

 

 

불변클래스의 단점

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

 

해결방안. 다단계 연산을 기본적으로 제공한다.
BigInteger 는 모듈러 지수 같은 다단계 연산 속도를 높여주는 가변 동반 클래스(companion class) 를 package-private 로 두고있다.이 가변 동반 클래스를 사용하기란 BigInteger 를 쓰는것보다 훨씬 어렵지만 이 어려운 부분들을 모두 BigInteger 가 대신 처리해준다. String 클래스의 가변 동반 클래스를 StringBuilder, String Buffer 로 제공받고있다.

 

 

상속을 막는 방법 

모든 생성자를 private 혹은 package-private 으로 만들고 public 정적 팩터리를 제공하는 예제로 상속을 막을 수 있다.

@EqualsAndHashCode
@ToString
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);
    }
}

 

public 이나 protected 생성자가 없으니 다른 패키지에서는 이 클래스를 확장하는게 불가능하다. 

 

 

불변 클래스의 완화된 규칙

"모든 필드가 final 이고 어떤 메서드도 그 객체를 수정할 수 없어야한다." -> "어떤 메서드도 객체의 상태 중에서 외부에 비치는 값을 변경할 수 없다." 라고 살짝 완화할 수 있다. 어떤 불변 클래스는 계산 비용이 큰 값을 나중에 계산하여 final 이 아닌 필드에 게시해놓기도 한다. 똑같은 값을 요청하면 캐시해둔 값을 반환하여 계산 비용을 절감한다. (객체가 불변이기 때문에 항상 같은 결과가 만들어짐이 보장된다.)

 

 

정리

getter 메서드가 있다고해서, setter 메서드를 무조건 만들지 말자. 클래스는 꼭 필요한 경우가 아니라면 불변이여야한다. 항상 모든 클래스를 불변으로 만들 수 없지만, 불변으로 만들 수 없는 클래스라면 변경할 수 있는 부분을 최소한으로 줄여야한다. 꼭 변경해야할 필드를 뺀 나머지를 모두 final 로 선언하자. 다른 합당한 이유가 없다면 모든 필드는 private final 이여야한다. 

 

생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다. 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public 으로 제공되서는 안된다. 

 

 

 

반응형

Designed by JB FACTORY