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

들어가며

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

불변으로 만들기 위한 법칙

  1. 객체의 상태를 변경하는 메소드를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다. 즉 상속을 할 수 없도록 한다.
  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private로 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 가변 객체를 참조하는 필드가 하나라도 있다면 방어적 복사를 통해 반환하라.

불변 클래스의 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.java.effective.chapter4.item17;

public final class Complex {
private final double re;
private final double im;
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 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 static Complex valueOf(double re, double im) {
return new Complex(re, im);
}

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

//복소수의 곱셈 표준
//단, 반올림이나 NaN 혹은 무한대를 제대로 처리하지 못하는 불안정한 코드이다.
public Complex times(Complex c) {
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}

//복소수의 나눗셈 표준
//단, 반올림이나 NaN 혹은 무한대를 제대로 처리하지 못하는 불안정한 코드이다.
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 (o == this) return true;
if (!(o instanceof Complex)) return false;
Complex c = (Complex) o;
return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
}

@Override
public int hashCode() {
return 31 * Double.hashCode(re) + Double.hashCode(im);
}

@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}

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

불변 객체의 장점

불변 객체는 단순하다. 불변 객체는 생성된 시점의 상태를 파괴될 때 까지 간직한다. 모든 생성자가 클래스 불변식을 보장한다면 그 클래스를 사용하는 프로그래머가 다른 조취를 하지 않아도 불변으로 남는다.

불변 객체는 근본적으로 thread safe하여 따로 동기화할 필요가 없다. 여러 스레드가 동시에 사용해도 절대 훼손되지 않는다. 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변 객체는 안심하고 공유가 가능하다. 따라서 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용하기를 권한다.

또한 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.

불변 객체는 방어적 복사가 필요없다. 아무리 복사해도 원본과 같으니 복사 자체가 의미가 없다. 그렇기 때문에 clone 메소드나 복사 생성자는 제공하지 않는게 좋다.

불변 객체는 자유롭게 공유할 수 있음은 물론 불변 객체끼리는 내부 데이터를 공유할 수 있다. 예를들어 BigInteger 클래스를 보자. BigInteger는 내부에서 값의 부호와 크기를 따로 표현한다. 만약 negate 메소드가 크기가 같고 부호만 반대인 새로운 BigInteger를 생성한다 하자. 이 때는 복사하지 않고도 원본 인스턴스와 공유해도 된다.

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

불변 객체의 단점

값이 다르면 반드시 독립된 객체로 만들어야 한다. 이 말은 불변이라면 당연하다. 하지만 이 내용이 단점이 될 수 있다. 예를 들어 100만 비트짜리 BigInteger에서 비트 하나를 바꿔야 한다고 해보자. 그럼 이를 만드는데 너무 큰 비용을 처리해야 한다.

상속을 막기 위한 방법

상속을 막기 위한 방법엔 class를 final로 선언하는 방법 외에도 생성자를 private로 생성하는 방법도 있다. 그렇게 한 뒤 인스턴스는 스태틱 빌더 패턴으로 제공하면 된다.

주의사항

BigInteger나 BigDecimal의 인스턴스를 인수로 받는다면 주의해야 한다. 이 값들이 불변이여야 클래스의 보안을 지킬 수 있다면 인수로 받은 객체가 진짜 BigInteger나 BigDecimal인지 반드시 확인해야 한다.

또한 모든 클래스를 불변으로 만들 수는 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화로 꾸미자. 객체가 가질 수 있는 상태가 작아지면 그 객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다.

다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다. 또한 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/08/Languages/Effective%20JAVA/item17/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.