변경 가능성을 최소화하라

수박참외메론·2022년 11월 30일
0

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스를 말한다. 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.

Java 플랫폼 라이브러리에도 대표적으로 String, 기본타입 박싱클래스, BigInteger, BigDecimal 등이 불변클래스이다.

불변클래스의 규칙

  • 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
  • 모든 필드를 final로 선언한다.
  • 모든 필드를 private으로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야한다.
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 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) {
    	...
    }
    
    public Complex times(Complex c) {
    	....
    }
    public Complex dividedBy(Complex c){
    	...
    }
    
	@Override public boolean equals(Object o){
    	...
	}
    @Override public int hashCode() {
    	...
    }
    
    @Override public String toString() {
    	...
    }
}

위의 코드에서 사칙연산 메서드들이 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환하는 모습에 주목하자.

이렇게 피연산자에 함수를 적용해 그 결과는 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴은 보통 함수형 프로그래밍에서 많이 나타난다. 이 방식으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있어 좋다.

불변객체의 장점

  • 불변 객체는 단순하다
  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.
    그래서 이는 클래스를 스레드 안전하게 만드는 가장 쉬운 방법이다.
    그리고 그에 따라 한번 만든 인스턴스를 최대한 재활용하기를 권한다.
  • 따라서 방어적 복사가 필요가 없다
  • 불변 객체끼리는 내부 데이터를 공유할 수 있다.
    예를들어 BigInteger 클래스는 내부에서 부호와 크기를 따로 표현하여 negate 메서드를 이용해 새로운 BigInteger을 생성할때, 부호만 다른 같은 내부 int배열을 사용한다.
  • 불변 객체는 그 자체로 실패 원자성을 제공한다.
    상태가 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.

값이 다르면 반드시 독립된 객체로 만들어야 한다

불변클래스의 단점으로 한 클래스에 다양한 값들이 존재한다면 이들을 매번 모두 만들어야하고, 그만큼 비용이 소모된다는 점이다.

예를들어 백만비트짜리 BigInteger에서 비트 하나를 바꿔야 한다고 해보자.

BigInteger moby = ...;
moby = moby.flipBit(0);

flipBit 메서드는 딱 한 비트만 다른 새로운 BigInteger 인스턴스를 만들게 되는데 이는 BigInteger 크기에 비례해 O(N)만큼의 시간과 공간을 잡아먹는다.

이와 달리 BitSet은 BigInteger처럼 임의 길이의 비트 순열을 표현하지만, BigInteger와는 달리 가변이라, 원하는 비트 하나만 상수시간안에 바꿔주는 메서드를 제공한다.

BitSet moby = ,,,;
moby.flip(0);

이 말고도 원하는 객체를 완성하기까지의 단계가 많고, 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거지곤 한다. 이 문제에 대해 어떻게 대처해야할까

가변 동반 클래스

Java에서는 대표적으로 String <-> StringBuilder 처럼 복잡한 연산들을 상수시간내에 연산이 가능하도록 바꾸는 클래스가 있다.

불변 클래스를 만드는 쉬운 방법

final 클래스로 선언하지만, 더 유연한 방법으로는
모든 생성자를 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);
    }
}

바깥에서 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 훨씬 유연하다..?

public 이나 protected 생성자가 없으니 다른 패키지에서는 이 클래스를 확장하는게 불가능하기 때문에 사실상 이 불변객체는 final이다.

정리

  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
  • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
profile
하루하루는 성실하게 인생전체는 되는대로

0개의 댓글