불변 클래스
불변 클래스란 해당 인스턴스 내부의 값을 수정할 수 없도록 구현된 클래스다. 불변 인스턴스에 설정된 정보는 객체 생성 시점에 고정되어, 해당 객체가 파괴되는 순간까지 절대 달라지지 않는다.
대표적인 예로 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal 등이 있다.
불변 클래스는 가변 클래스보다 오류가 생길 여지가 적고, 훨씬 안전하다.
불변 클래스를 만드는 다섯 가지 규칙
객체의 상태를 변경하는 메소드(ex. setter)를 제공하지 않는다.
당연한 말이다.
클래스를 확장할 수 없도록 한다.
하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하지 않도록 막아준다.
상속을 막는 대표적인 방법으로 클래스를 final로 선언하는 방법, private(or default) 생성자와 정적 팩토리 메소드를 사용하는 방법이 있다.
➕ private(or default) 생성자와 정적 팩토리 메소드를 사용하여 상속을 막는 방법
public class Person { private String name; private Person(String name) { // private 생성자 this.name = name; } public static Person nameOf(String name) { // public 정적 팩토리 메소드 return new Person(name); } public String getName() { return name; } }
모든 필드를 final로 선언한다.
값을 바꿀 수 없기 때문에, 명시적으로 값을 불변으로 만들 수 있다.
➕
또한, 모든 필드를 final로 선언하여 불변 객체를 만들면 동기화 없이 다른 스레드로 인스턴스를 넘길 수 있으며, 이로 인해 멀티 스레드 환경에서 발생할 수 있는 문제를 방지할 수 있다.
원래 여러 스레드가 동시에 하나의 객체에 접근할 때, 데이터의 일관성과 정확성을 보장하기 위해 동기화를 사용한다. 하지만 모든 필드가 final로 선언되면 불변 객체가 되어 동기화를 신경쓰지 않아도 된다.
모든 필드를 private로 선언한다.
클래스의 필드를 private로 선언하면, 외부에서 접근할 수 없으므로 캡슐화 원칙을 지킬 수 있다.
➕ public final / private final
public final로 선언해도 불변 객체를 만들 수 있지만 private final로 선언하는 것이 더 권장된다. public final로 선언하면 해당 필드의 이름을 변경할 수 없다.
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
클래스에 가변 객체를 참조하는 필드가 있다면 해당 필드를 직접 노출하지 않도록 하고, 클라이언트에서 해당 객체의 참조를 얻을 수 없도록 막아야 한다. 이를 위해서는 생성자, 접근자, readObject 메소드(item 88)에서 방어적 복사를 진행해야 한다.
아래 코드는 가변 객체인 List를 필드로 가지고 있는 Person 클래스이다.
public class Person {
private final String name;
private final List<String> hobbies; // private
public Person(String name, List<String> hobbies) {
this.name = name;
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public List<String> getHobbies() { // 방어적 복사
return Collections.unmodifiableList(new ArrayList<>(hobbies));
}
}
불변 객체 예시
아래 코드의 plus, minus, times, dividedBy 메소드는 자신의 필드를 수정하지 않고 Complex 인스턴스를 만들어 반환한다. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라고 한다.
public final class Complex { // final 선언
private final double re; // private final 선언
private final double im; // private 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);
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);
}
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;
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)";
}
}
불변 클래스 특징
✍ 장점
스레드 안전하여 따로 동기화할 필요 없다.
안전하게 공유할 수 있다. (+ 내부 데이터도 공유할 수 있다.)
불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않도록 정적 팩토리를 제공할 수 있다.
방어적 복사가 필요 없다. (공유할 수 있으므로..)
따라서 clone 메소드나 복사 생성자를 제공하지 않는 게 좋다. (String의 사용을 줄이도록 하자)
실패 원자성을 제공한다(item 76)
즉, 불일치 상태에 빠질 가능성이 없다.
✍ 단점
연속된 연산을 수행할 때 매 단계마다 객체가 새로 생성된다.
메모리 사용량과 가비지 컬렉션 비용을 증가시키고, 성능 문제로 이어질 수 있다.
🥕 해결 방법 : 다단계 연산들을 예측하여 기본 기능을 제공한다.
다단계 연산을 기본으로 제공한다면 각 단계마다 객체를 생성하지 않아도 된다. 그 예로 다단계 연산 속도를 높여주는 가변 동반 클래스를 사용하면 된다.(ex. StringBuffer)
총 정리
- getter가 있다고 무조건 setter을 생성하는 것은 좋지 않다.
- 클래스는 꼭 필요한 경우가 아니라면 불변인 것이 좋다.
- 불변으로 만들기엔 성능 저하가 심하다면 가변 동반 클래스를 public 클래스로 제공하자.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 즉, 꼭 변경해야 할 필드를 뺀 나머지 모두를 private final로 선언하자.
- 생성자는 불변식 설정이 모두 완료된 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 26. Raw 타입은 사용하지 말라. (0) | 2023.05.09 |
---|---|
[effective java] 아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라. (0) | 2023.05.09 |
[effective java] 아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라. (0) | 2023.03.25 |
[effective java] 아이템 15. 클래스 멤버의 접근 권한을 최소화하라. (0) | 2023.03.25 |
[effective java] 아이템 14. Comparable을 구현할지 고려하라. (0) | 2023.03.25 |