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

불변 클래스란 그 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 클래스는 가변 클래스보다 오류가 생길 여지가 적고 훨씬 안전하다.

  • ex) String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal

불변 클래스 규칙

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

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

필드를 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) { // 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다.
		return new Complex(re + c.re, im + c.im);
	}

	public Complex minus(Complex c) {
		return new Complex(re - c.re, im - c.im);
	}

}

plusminus를 보면, 사칙연산을 수행하는 행동이 이 인스턴스 자체의 값을 수정하지는 않는다. Complex인스턴스를 새로 만들어 반환하는 것을 볼 수 있다.

이처럼 불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다. 따라서 안심하고 공유할 수 있다.

1. 자주 쓰이는 값들은 상수(public static final) 로 제공한다.

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

2. 정적 팩터리를 제공해서 GC 비용을 줄인다.

불변 클래스의 단점

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

값의 가짓수가 많다면, 그 모두 독립된 객체로 만들어야하기 때문에 큰 비용을 처리해야 한다.

BigInteger moby = new BigInteger("100");
moby = moby.flipBit(0);

BitSet bitSet = new BitSet(100);
bitSet.flip(0);
  • BigInteger 는 원본과 단지 한 비트만 다른데 새로운 BigInteger를 생성한다.
  • BitSet은 가변이기 때문에, 원하는 비트 하나만 상수 시간안에 바꿔준다.

가변 동반 클래스 (companion class)

multisep operation (다단계 연산) 을 제공한다.

String의 경우 StringBuilder가 가변 동반 클래스이다. 이 클래스를 우리는 아래와 같이 사용한다.

StringBuilder sb = new StringBuilder();
sb.append("첫 번째");
sb.append("두 번째");
String result = sb.toString();
System.out.print(result);

이렇게 불변클래스인 String을 계속 생성해서 추가하는게 아니라, StringBuilder의 도움을 받아서 효과적으로 문자열을 조작하는 것이다.

이와 비슷하게, BigInteger도 가변 동반 클래스가 있다. 하지만, package-private이므로 클라이언트가 접근할 수 없게 되어 있다.

화면 캡처 2021-02-28 141743

BitSieve, MutableBigInteger, SignedMutableBigInteger 와 같은 가변동반 클래스는 직접 사용하기에 더 어렵기 때문에 BigInteger 클래스 내부에서 사용하고 있다.

Q : 이럴거면 왜 BigInteger 내부에서 private 메소드로 두지 않았을까? </br> A : 저 들은 가변 클래스다. 가변 클래스를 불변 클래스 안에 두면 안된다는 것을 상기시키면 어느정도 이해가 간다.

2. 자신을 상속하지 못하게 한다.

  1. final 클래스로 선언한다.
  2. 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리로 제공한다.
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);
}

Summary

  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 성능 때문에 어쩔 수 없다면 (다단계 연산이 필요할 경우) 불변 클래스와 쌍을 이루는 가변 동반 클래스를 제공하도록 하자.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
    • 모두 private final로 만들자

댓글남기기