티스토리 뷰
* 이펙티브 자바 2/E를 읽고 공부하기 위해 기록한 게시글입니다.
15. 변경 가능성을 최소화하라
변경 불가능(immutable) 클래스는 그 객체를 수정할 수 없는 클래스이다.
- 대표적으로 String 클래스, Wrapper 클래스, BigInteger 클래스 등등...
- 객체 내부의 정보는 객체가 생성될 때 주어지며, 객체가 살아있는 동안 변경되지 않고 그대로 보존된다.
-> 변경 가능 클래스에 비해 설계하기 쉽고, 구현하기 쉬우며 오류 가능성도 적고 안전하다.
<변경 불가능 클래스를 만들 때 따라야 할 다섯가지 규칙>
1. 객체 상태를 변경하는 메서드(수정자, setter 등)를 제공하지 마라
2. 계승(상속)할 수 없도록 해라
- 잘못 작성되거나, 악의적인 하위 클래스가 객체상태가 변경된 것 처럼 동작해서 불변성을 깨뜨리는 일을 방지할 수 있다.
- 계승할 수 없도록 하기 위해서는 클래스를 final으로 선언하면 된다.
(+ 생성자를 private로 닫아두는 것 역시 상속을 방지할 수 있다.)
3. 모든 필드를 final으로 선언해라
- 시스템이 강제하는 형태대로 개발자의 의도가 분명하게 드러난다.
- 새로 생성된 객체에 대한 참조가 동기화(synchronization) 없이 다른 스레드로 전달되어도 안전하다.
4. 모든 필드를 private로 선언해라
- 클라이언트가 필드가 참조하는 변경 가능 객체를 직접 수정하는 일을 막을 수 있다.
- 필드를 public으로 선언하게 되면 나중에 클래스의 내부 표현을 변경할 수 없기 때문이다.
5. 변경 가능 컴포넌트에 대한 독점적 접근권을 보장해라
- 클래스에 포함된 변경 가능 객체에 대한 참조를 클라이언트는 획득할 수 없어야 한다. 그런 필드는 클라이언트가 제공하는 객체로 초기화 해서는 안되고, 접근자 또한 그런 필드를 반환해서는 안된다. 따라서 생성자나 접근자, readObject(?) 메서드 안에서는 방어적 복사본(Defencive copy)을 만들어야 한다.
* 위 원칙들을 모두 따르는 것은 사실 좀 과한 면은 있으며 성능 향상을 위해 완화할 수 있다.
- 어떠한 메서드도 외부에서 관측 가능한 상태 변화를 야기하지 않아야 한다는 원칙만 준수하면 된다.
변경 불가능 클래스는 함수적 접근법(functional approach)를 따르는 경우가 많다.
- 피연산자를 변경하는 대신, 연산을 적용한 결과를 새롭게 만들어 반환하는 것.
(메서드 리턴값으로 새로운 객체를 만들어서 리턴)
- 변경 불가능성을 보장하기 위함.
변경 불가능 객체는 단순하다. 단순하기에 생성될 때 부여된 한 가지의 상태만을 갖는다.
또한 어떠한 동기화도 필요 없으며 thread-safe 할 수 밖에 없다.
- 여러 스레드가 동시에 사용해도 상태가 훼손될 일이 없다.
-> 변경 불가능한 객체는 자유롭게 공유해서 재사용할 수 있다.
변경 불가능한 클래스 재사용을 적극 장려하는 방법으로는 자주 사용되는 값을 public static final 상수로 만들어 제공하는 것이다.
public static final Complex ZERO = new Complex(0, 0);
public static final Complex OME = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
또한 자주 사용하는 객체를 캐시하여 이미 있는 객체가 거듭 생성되지 않도록 하는 정적 팩토리를 제공할 수도 있다.
- 정적 팩토리를 사용하게 되면 클라이언트는 새로운 객체를 만드는 대신 기존 객체를 공유해서 사용하므로 메모리 요구량 및 GC 처리 비용이 감소한다. (클래스 설계 시 public 생성자 대신 정적 팩토리를 사용하면 별도의 클라이언트 코드 수정 없이도 캐시 기능을 추가할 수 있음)
- 캐시 기법이 가능한 이유는 바로 객체가 변경 불가능하기 때문이다.
public final class Integer extends ... {
...
// 정적 팩토리 메서드를 사용하면서 내부 캐시를 이용하는 모습
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
...
private static class IntegerCache
static final int low = -128;
static final int high;
static final Integer cache[];
...
}
{
변경 불가능한 객체를 자유롭게 공유할 수 있다는 것은 방어적 복사본을 만들 필요가 없다는 것이기도 하다. 아니 애초에
변경 불가능한 객체는 복사본을 만들 이유가 없다. 복사본을 만들어봤자 원본이랑 영원히 같은 상태이기 때문.
-> 변경 불가능 클래스에 clone 메서드나 복사 생성자를 만들 필요가 없다.
* 변경 불가능 객체는 객체 그 자체 뿐만 아니라 그 내부도 공유할 수 있다.
* 변경 불가능 객체는 다른 객체의 구성요소로도 훌륭하다
- 변경 불가능 객체는 맵의 키나 집합의 원소로 활용하기 좋다. 한번 집어넣고 나면 그 값이 변경되어 맵이나 컬렉션의 불변식이 깨질 일이 없기 때문.
* 변경 불가능 객체의 단점은 값마다 별도의 객체를 만들어야 한다는 것.
- 객체 생성 비용이 높을 가능성이 있다
* 변경 불가능 객체의 생성자는 완전히 초기화가 끝나서 모든 불변식이 만족되는 객체를 만들어야 한다.
특별한 이유가 없다면 생성자 이외의 public 초기화 메서드를 제공하지 마라. (재 초기화 메서드 역시 마찬가지)
* 변경 불가능 클래스를 구현하는 것은 클래스를 final로 선언하는 것 외에도 대안적인 방법들이 여럿 있다.
- 그 중 하나는 모든 생성자를 private 혹은 package-private(default)로 선언하고, 생성자 대신 public 정적 팩토리를 이용하는 것이다.
-> 여러 개의 default 구현 클래스를 활용할 수 있게 되므로 유연성이 좋다. 또한 해당 패키지 외부의 클라이언트 입장에서 보면 해당 변경 불가능 클래스는 final로 선언된 것이나 마찬가지인 효과를 갖는다. public이나 protected로 선언된 생성자가 없기 때문.
public class Complex {
private final double re;
private final double im;
// 재사용성 향상 (항상 같은 객체를 반환)
public static final Complex ZERO = new Complex(0, 0);
public static final Complex OME = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
// private 생성자로 생성 방지
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
// 정적 팩토리 메서드 1
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
// 정적 팩토리 메서드 2
public static Complex valueOfPolar(double r, double theta) {
return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}
// 접근자(getter) 메서드만 있고 수정자(setter) 메서드는 없음
public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
}
- 정적 팩토리를 이용함으로써 의도가 분명한 이름의 메서드를 활용할 수 있다.
(결론)
모든 get 메서드마다 그에 대응하는 set 메서드를 두는 것은 피하라
변경 가능한 클래스로 만들어야 할 타당한 이유가 없다면, 반드시 변경 불가능 클래스로 만들어라. (특히 값 객체)
변경 불가능 클래스로 만들 수 없다면 변경 가능성을 최대한 제한하라
특별한 이유가 없다면 모든 필드는 final으로 선언하라
'JAVA' 카테고리의 다른 글
기타 (0) | 2022.07.24 |
---|---|
상속 전에 데코레이터 패턴을 고려해보자 (0) | 2022.07.23 |
getter/setter 메서드를 활용하자 (0) | 2022.07.23 |
클래스와 멤버의 접근 권한을 최소화하자 (0) | 2022.07.22 |
자연적 순서가 있는 객체는 Comparable 구현 (0) | 2022.07.22 |