티스토리 뷰

JAVA

변경 가능성을 최소화하라

세댕댕이 2022. 7. 23. 01:02

* 이펙티브 자바 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으로 선언하라

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함