Immutable(이하 불변) 클래스는 객체를 한 번 만들게 되면 상태(필드의 값)를 변경할 수 없는 클래스이다.
이 글에서는 불변 클래스의 구변 방법을 설명하고 특징을 설명한다.
불변 클래스 구현 방법
자바에서 객체 필드의 값을 변경하지 못하게 하는 final 키워드, 외부에서 접근 불가능하게 하는 접근 제어자 등을 사용하여 불변 클래스를 쉽게 구현할 수 있다.
먼저 자바에서 제공하는 클래스 중 불변 클래스인 Integer 클래스의 소스는 아래와 같다.
package java.lang;
...
// 2. final을 사용하여 다른 클래스에서 상속하지 못하도록 한다.
public final class Integer extends Number implements Comparable<Integer> {
...
// 1. 모든 필드를 final로 선언한다.
// 4. 모든 필드를 private로 선언한다.
private final int value;
public int intValue() {
return value;
}
...
// 1. 값을 변경하는 메서드를 제공하지 않는다.
// 5. 변경 가능한 객체에 대한 참조가 있다면, 외부에서는 해당 참조를 얻을 수 없도록 해야한다.
}
먼저 Integer에서 실제 값을 저장하는 value 필드는 한 번 값이 초기화되면 더 이상 수정할 수 없도록 1. final 키워드를 사용하였고, 외부에서 해당 필드를 접근하지 못하도록 4. private로 선언하였다. 또한 만약 value를 1. 수정할 수 있는 메서드가 제공된 다면 외부에서 해당 메서드를 이용하여 수정 가능하므로 제공하지 않았다.
또한 규칙 13에서 설명한 내용과 비슷하게 5. 변경 가능한 객체에 대한 참조가 있다면 외부에서는 해당 참조를 얻을 수 없도록 해야 한다. 만약 아래와 같이 Array 필드인 data를 외부에서 접근 가능하도록 getter 메서드를 제공한다면 참조 값은 변경할 수 없지만, 참조에 대한 객체는 수정이 가능하므로 불변 클래스가 깨지게 된다.
private static final Thing[] data = { ... };
또한 2. 클래스에 final을 선언하여 외부에서 Integer를 상속할 수 없도록 하였다. 상속을 허용한다면 하위 클래스에서는 메서드를 재정의할 수 있게 된다. 이 것이 문제가 될 수 있는데 예를 들어 intValue() 메서드는 변경 불가능한 값인 value를 리턴하는 메서드인데, 하위 클래스가 악의적으로 다른 값을 반환하게 재정의 한다면 마치 변경된 것처럼 보인다. 이처럼 잘못 작성되는 하위 클래스를 방지하기 위해 상속할 수 없도록 한다.
사실 상속할 수 없도록 하는 방법에는 클래스에 final로 선언하는 방법 말고 한 가지 더 유연한 방법이 있다. 바로 private나 package-private로 된 생성자만을 제공하고, 외부에는 규칙 1에서 설명한 정적 팩토리 메서드를 제공하는 것이다. public이나 protected로 선언된 생성자가 없기 때문에 외부에서는 상속을 할 수 없다. 또한 그렇기 때문에 외부에서는 객체를 생성자로 생성할 수 없으므로 정적 팩토리 메서드를 제공한다.
불변 클래스 특징
불변 클래스의 장점을 설명하기 위해 이번엔 자바에서 제공하는 또 다른 불변 클래스인 BigInteger에 대해 먼저 설명한다. 소스는 다음과 같다.
package java.math;
...
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum;
final int[] mag;
...
// 1. 불변 클래스의 객체는 자유롭게 공유될 수 있다.
public static final BigInteger ZERO = new BigInteger(new int[0], 0);
public static final BigInteger ONE = valueOf(1);
...
public BigInteger negate() {
// 2. 불변 클래스안에 필드들은 서로 공유 가능하다.
return new BigInteger(this.mag, -this.signum);
}
public BigInteger flipBit(int n) {
if (n < 0)
throw new ArithmeticException("Negative bit address");
int intNum = n >>> 5;
int[] result = new int[Math.max(intLength(), intNum+2)];
for (int i=0; i < result.length; i++)
result[result.length-i-1] = getInt(i);
result[result.length-intNum-1] ^= (1 << (n & 31));
return valueOf(result);
}
private static BigInteger valueOf(int val[]) {
// 3. 불변 클래스는 값을 변경할 수 없기때문에 새로운 객체를 만든다.
return (val[0] > 0 ? new BigInteger(val, 1) : new BigInteger(val));
}
...
}
BigInteger 클래스는 불변 클래스이지만 Integer 클래스에서 설명했던 내용과 다른 부분이 존재한다. 먼저 필드를 private로 선언하지 않고 package-private로 선언하였다. 사실 위에서 설명한 원칙들은 다소 과하기 때문에 필요하다면 일부 완화할 수 있다. package-private도 높은 수준의 접근 제어자이기 때문에 어느 정도 수용 가능하다.
하지만 private로 선언하지 않은 것과는 다르게 BigInteger를 개발할 때 큰 실수를 하였는데, 바로 상속을 못하도록 방어하지 않았다. 불행히도 하위 호환성 때문에 이는 고쳐지지 않고 사용되고 있다. 그러나 나머지 구현 부분은 불변 클래스의 규칙을 따른다.
1. 불변 클래스의 객체는 자유롭게 공유할 수 있다.
불변 클래스의 객체는 절대 변하지 않는다. 따라서 외부에 공유해도 해당 객체를 수정할 수 있는 방법이 없기 때문에 문제가 없다. BigInteger는 자주 사용되는 값(ZERO, ONE 등)을 미리 선언하여 재사용하는데, 이 필드들은 public으로 선언되어 있다. public으로 선언되면 외부에서 수정하는 문제가 발생할 수 있겠지만 BigInteger는 불변 클래스이므로 수정이 불가능하므로 자유롭게 공유할 수 있다.
2. 불변 클래스 안에 필드들은 서로 공유 가능하다.
불변 클래스 안에 필드들은 당연하게도 절대 변하지 않는다. 따라서 객체들끼리 필드를 공유하여도 변경되지 않으므로 서로 공유가 가능하다. 예를 들어 BigInteger에서 negate() 메서드는 양수이면 음수를, 음수이면 양수로 변환하여 BigInteger 객체를 생성하는 메서드이다. 이때 msg 필드는 배열이기 때문에 서로 공유하면 문제가 될 수 있다. 하지만 BigiInteger는 불변 클래스이기 때문에 msg 배열은 절대 변경되지 않으므로 새로운 객체와 원래 객체가 같은 배열을 참조해도 어떠한 문제도 발생하지 않는다.
3. 불변 클래스는 값을 변경할 수 없기 때문에 새로운 객체를 만든다.
불변 클래스는 값을 변경할 수 없기 때문에 대부분 새로운 객체를 만들어 반환되도록 작성한다. 이는 불변 클래스의 유일한 단점인데, 만약 객체를 생성하는 비용이 크다면 문제가 발생한다. BigInteger의 flipBit() 메서드는 특정 위치의 bit 값을 반대로 변경하는 메서드이다. 만약 BigInteger의 값이 매우 큰 값이어서 bit 배열이 엄청 크다면, 새로운 BigInteger의 값을 생성하기 위해서 동일한 크기의 bit 배열을 하나 새롭게 만들고, 특정 위치의 bit 값을 변경해주고 객체를 생성해야 한다. 이렇게 되면 bit 배열만큼의 시간과 공간이 소요되기 때문에 문제가 발생한다.
이외에 불변 클래스는 단순하며 스레드에 안전하다. 불변 클래스는 값을 변경할 수 없다. 따라서 값이 혹시 null인지, 비정상 범위의 값인지에 대해서 객체를 생성할 때 미리 검사한다면, 클라이언트는 두려워하지 않고 사용하기만 하면 된다. 또한 값이 변경되지 않는다는 것은 스레드에 안전하다.
또한 불변 클래스는 다른 불변 클래스의 필드로 사용될 수 있다. 따라서 새로운 불변 클래스를 작성할 때 기조 불변 클래스에 변경 가능성에 대해 걱정할 필요 없다.
요약
- 변경 가능한 클래스로 만들 이유가 없다면 불변 클래스로 먼저 만들어 보자.
- 변경 가능한 클래스를 만들어야 한다고 해도, 최대한 변경 가능성을 제한하자.
'Java > [책] 이펙티브 자바' 카테고리의 다른 글
[이펙티브자바] 규칙19. 상수 인터페이스(Constant Interface)는 안티패턴이다 (0) | 2022.06.20 |
---|---|
[이펙티브자바] 규칙16. 계승보다 구성 (= extends보다 필드선언) (0) | 2022.03.20 |
[이펙티브 자바] 규칙14. getter 메서드 (0) | 2022.02.23 |
[이펙티브 자바] 규칙13. 접근 제어(Access Control)는 최소화하자. (0) | 2022.02.14 |
[이펙티브 자바] 규칙12. Comparable, Comparator 인터페이스 (0) | 2022.01.07 |