728x90
17. 변경 가능성을 최소화하라
불변 클래스
- 인스턴스의 내부 값을 수정할 수 없는 클래스
String
, 내부 값으로 박싱된 클래스들,BigInteger
,BigDecimal
....- 블변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
불변 클래스로 만드는 방법
- 1. 객체의 상태를 변경하는 메서드(변경자-
setter
)를 제공하지 않는다. - 2. 클래스를 확장할 수 없도록 한다. (final 클래스)
- 하위 클래스에서 상위 클래스의 불변 조건을 깨뜨릴 수 있다.
- 하위 클래스에서 가변 필드를 추가하거나 오버라이드하여 불변성을 해칠 수 있다.
- 3. 모든 필드를
final
로 선언한다. - 4. 모든 필드를
private
으로 선언한다.- 외부에서 접근하지 못하게 하면 내부 구현 변경시에 api는 변경하지 않아도 된다.
- 5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 컴포넌트 : 독립적으로 동작할 수 있는 모듈, 재사용, 상호작용 가능
- 클래스의 필드, 내부 클래스, 컬렉션
- 클라이언트가 제공한 객체 참조를 필드가 가져서도 안되고, 접근자 메서드가 필드를 그대로 반환해서도 안된다.
- 그대신, 방어적 복사(외부 객체와의 연결을 끊기) 를 수행하자.
- 컴포넌트 : 독립적으로 동작할 수 있는 모듈, 재사용, 상호작용 가능
- 예시 코드(불변 복소수 클래스)
package effectivejava.chapter4.item17;
// 코드 17-1 불변 복소수 클래스 (106-107쪽)
public final 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);
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);
}
// 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
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);
}
public Complex times(Complex c) {
return new Complex(re * c.re - im * c.im,
re * c.im + im * c.re);
}
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;
// == 대신 compare를 사용하는 이유는 63쪽을 확인하라.
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)";
}
}
setter
는 제공하지 않는다.- 사칙연산 메서드들이 인스턴스 자신은 수정하지 않고 새로운 인스턴스를 만들어서 반환한다.
- 함수형 프로그래밍 : 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
- 절차적 혹은 명령형 프로그래밍 : 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변한다.
- 사칙연산 메서드에서 동사(
add
) 대신 전치사(plus
,minus
)를 사용하여 객체의 값을 직접 변경하지 않고 새로운 인스턴스를 만들어 반환함을 암시한다.
성능을 위해 불변 클래스 완화하기
- 불변 클래스 규칙 목록에 따르면 모든 필드가
final
이고 어떤 메서드도 그 객체를 수정할 수 없어야 한다. - 성능을 위해 지연 초기화를 수행해도 된다.
- 계산 비용이 큰 값을 처음 쓰일 때 계산하여
final
이 아닌 필드에 캐시해 놓을 수 있다.
- 계산 비용이 큰 값을 처음 쓰일 때 계산하여
public final class String {
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) { // 해시값 계산
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
불변 객체는 단순하다.
불변 객체
는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.가변 객체
는 임의의 복잡한 상태에 놓일 수 있어 믿고 사용하기 어려울 수도 있다.
불변 객체는 근본적으로 스레드 안전하다.
- 따로 동기화할 필요가 없다.
- 여러 스레드가 동시에 사용해도 훼손되지 않는다.
- 불변 객체는 안심하고 공유할 수 있다.
- 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용하자.
- 자주 쓰이는 값들을 상수로 제공하자.
- 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩토리 메서드를 제공하자.
- 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
숫자 클래스
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true
Boolean a = Boolean.valueOf(true);
Boolean b = Boolean.valueOf(true);
System.out.println(a == b); // true
BigInteger a = BigInteger.valueOf(10);
BigInteger b = BigInteger.valueOf(10);
System.out.println(a == b); // true
Integer
는 -128~127 사이의 정수는 캐시되어 재사용된다.Boolean
은 true, false값은 항상 같은 인스턴스를 반환한다.BigInteger
는 자주 사용되는 작은 값들(-16~16)을 캐시한다.
새로운 클래스를 설계할때 public 생성자 대신 정적 팩토리 메서드를 만들어 두면, 필요에 따라 캐싱 기능을 덧붙일 수 있어 좋다.
- 불변 객체는
clone
메서드나복사 생성자
(가변 객체의 독립적인 복사본 만들 때 사용)를 제공하지 않는 게 좋다.- 방어적 복사도 필요 없다.
- 복사해봤자 원본과 동일하기 때문이다.
String의 복사 생성자는 되도록 사용하지 말자. 잘못 만들어진 것이다.
String str1 = "hello";
String str2 = str1; // 참조를 공유
불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
- 내부 데이터의 필드가 배열이라 가변일지라도, 불변 객체는 내부 데이터를 변경하지 않기 때문에 내부 데이터를 공유해도 된다.
BigInteger
에서 크기를 나타내는int 배열
의 참조를 서로 공유한다.- 존재하는 배열을 그대로 다른 객체를 만들때 사용한다.
- 이는 매우 큰 숫자를 다룰 때, 배열을 복사하지 않고 공유함으로써 메모리와 성능을 절약하게 해준다.
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
BigInteger(int[] magnitude, int signum) {
this.signum = (magnitude.length == 0 ? 0 : signum);
this.mag = magnitude;
if (mag.length >= MAX_MAG_LENGTH) {
checkRange();
}
}
객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
- 구조가 복잡하더라도 불변식을 유지하기 훨씬 수월하다.
- map의 key와 집합의 원소로 사용하기 좋다.
불변 객체는 그 자체로 실패 원자성을 제공한다.
- 실패 원자성: 메서드 실행 중 예외가 발생하더라도 객체의 상태가 메서드 호출 전의 상태로 유지되는 특성
- 불변 객체는 생성된 이후로 상태가 변하지 않기 때문에 작업이 실패하더라도 일관된 상태를 유지한다.
- 불변 객체는 항상 유효한 상태를 유지하므로 실패 원자성을 제공한다.
- 가변 객체
public class MutablePerson {
private String name;
private int age;
public void updatePerson(String newName, int newAge) {
this.name = newName;
if (newAge < 0) throw new IllegalArgumentException("Age cannot be negative");
this.age = newAge; // 예외 발생 시 객체는 불일치 상태가 될 수 있음
}
}
- 불변 객체 (실패 원자성을 갖는다)
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
this.age = age;
}
public ImmutablePerson updatePerson(String newName, int newAge) {
return new ImmutablePerson(newName, newAge); // 예외가 발생하더라도 원본은 변경되지 않음
}
}
불변 클래스의 단점
값이 다르면 반드시 독립된 객체로 만들어야 한다.
- 불변 객체 vs 가변 객체
- 불변 객체
BigInteger
는 백만 비트 중 한 비트를 바꾸기 위해 또다른 백만 비트짜리 인스턴스를 만들어야한다. - 가변 객체
BitSet
은 원하는 비트 하나만 상수 시간 안에 바꿀 수 있다.
- 불변 객체
BitSet bits = new BitSet(16); // 16비트 크기로 초기화
// 특정 위치의 비트를 1로 설정 (상수 시간 O(1))
bits.set(3); // [0,0,0,1,0,0,0,0....]
- 불변 객체를 만들기 위해 단계적으로 만들어지는 객체들이 버려진다면 성능 문제가 발생한다.
해결 방법 : 다단계 연산을 예측하여 기본 기능으로 제공하면 된다.
- 각 단계마다 객체를 생성할 필요가 없다.
BigInteger
는 가변 동반 클래스를package-private
으로 두어 사용한다.- 중간 결과는 가변 동반 클래스로 처리하고, 최종 결과만 불변 클래스로 변환하여 반환한다.
- 중간 연산에서 새 객체 생성을 줄여 성능을 개선할 수 있다.
public class BigInteger {
// 최대 공약수 계산
public BigInteger gcd(BigInteger val) {
if (val.signum == 0)
return this.abs();
else if (this.signum == 0)
return val.abs();
MutableBigInteger a = new MutableBigInteger(this);
MutableBigInteger b = new MutableBigInteger(val);
MutableBigInteger result = a.hybridGCD(b);
return result.toBigInteger(1);
}
}
- String도 가변 동반 클래스인
StringBuilder
를 사용하여 불필요한String
객체 생성을 줄인다.
public String replace(CharSequence target, CharSequence replacement) {
// ...
StringBuilder sb = new StringBuilder(resultLen);
sb.append(replStr);
for (int i = 0; i < thisLen; ++i) {
sb.append(charAt(i)).append(replStr);
}
return sb.toString();
}
}
불변 클래스의 상속을 막는 방법
1. final
클래스로 선언한다.
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;
}
}
2. 모든 생성자
를 private 혹은 package-private으로 만들고 public 정적 팩토리 메서드를 제공한다.
- 정적 팩토리를 통해서만 인스턴스 얻기가 가능하다.
- 생성자를
private
으로 둘 경우 상속 자체가 불가능하고,package-private
으로 둘 경우 다른 패키지에서 상속이 불가능하다. package-private
는 구현 클래스를 원하는 만큼 만들어 활용이 가능하므로 1번 방법보다 유연하다.
- 정적 팩토리를 사용하므로 후에
캐싱
도 가능하다.
public final class Complex {
private static final Complex ZERO = new Complex(0, 0);
public static Complex zero() {
return ZERO; // 미리 생성된 객체 재사용
}
}
주의할 점
BigInteger
와 BigDecimal
은 상속을 받을 수 있어 불변성이 깨질 수 있다.
final
클래스가 아니기 때문이다.- 믿을 수 없는 클라이언트로부터
BigInteger
나BigDecimal
을 받을 경우 가변이라 가정하고 방어적으로 복사해 사용해야 한다.
직렬화시 불변이 깨질 수 있다.
Serializable
을 구현한 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면?- 직렬화 (객체->바이트스트림)시 내부 가변 객체의 참조를 그대로 직렬화한다.
- 역직렬화시 공격자가 이 참조를 조작해서 불변성을 깨뜨릴 수 있다.
readObject
나readResolve
메서드를 반드시 제공하자.
readObject , readResolve
- readObject : 객체의 역직렬화 과정 커스터마이즈
public final class ImmutablePerson implements Serializable {
private final String name;
private final Date birthDate;
// 역직렬화 시 호출됨
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 기본 역직렬화 수행
// birthDate에 대한 방어적 복사 수행
this.birthDate = new Date(birthDate.getTime());
}
}
- readResolve : 역직렬화 후 객체 참조를 다른 객체로 대체(역직렬화된 객체 대신 사용)
public final class ImmutablePerson implements Serializable {
private static final ImmutablePerson INSTANCE = new ImmutablePerson();
private Object readResolve() {
return INSTANCE;
}
}
- 또는 ObjectOuputStream.writeUnshared와 ObjectInputStream.readUnshared를 사용해야한다.
- 같은 객체라도 매번 새로운 인스턴스로 처리하여 반환한다.
- 그렇지 않으면 공격자가 가변 인스턴스를 만들어 낼 수 있다.
결론
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
getter
가 있다고 해서 무조건setter
를 만들지는 말자.- 단순한 값 객체는 항상 불변으로 만들자.
String
과BigInteger
처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심하자.- 성능을 고려하여 불변 클래스와 쌍을 이루는 가변 동반 클래스를
public
으로 제공해도 된다.
- 성능을 고려하여 불변 클래스와 쌍을 이루는 가변 동반 클래스를
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최대한으로 줄이자.
- 객체가 가질 수 있는 상태의 수를 줄이면 그 객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다.
- 꼭 변경해야 할 필드를 뺀 나머지 모두를
final
로 선언하자. - 다른 합당한 이유가 없다면 모든 필드는
private final
이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
- 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도
public
으로 제공해서는 안된다.- 객체의 초기화는 생성 시점에만 일어나는 것이 상태가 일관적이고 예측하기 쉽다.
- 상태를 다시 초기화하는 메서드도
public
으로 제공해서는 안된다.- 새로운 상태가 필요하다면 새 객체를 생성하자.
- 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도
728x90
'Java > effective java' 카테고리의 다른 글
[Effective Java] Item 18. 상속보다는 컴포지션을 사용하라 (1) | 2025.02.05 |
---|---|
[Effective Java] Item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라. (0) | 2025.02.05 |
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2025.02.05 |
[Effective Java] Item 14. Comparable을 구현할지 고려하자. (1) | 2025.01.20 |
[Effective Java] Item 13. clone 재정의는 주의해서 진행하라 (1) | 2025.01.20 |