티스토리 뷰

반응형

불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스를 말한다. 불변 클래스로 생성된 인스턴스는 생성자로 인해 객체가 생성된 이후부터 객체가 가비지 컬렉터에 의해서 해제될 때까지 절대 변하지 않는다. 우리는 이미 다양한 불변 클래스를 사용하고 있고 그 대표적인 예로 String, BigInteger, Boolean 등이 존재한다.

 

불변 클래스의 가장 큰 장점은 설계하고 구현하기 쉬우며 오류가 생길 여지가 적고 훨씬 안전하다는 점이다.

 

그러면 불변 클래스를 만들기 위한 규칙에 대해서 살펴보자.

  1. 객체의 상태를 변경하는 메서드(Ex. Setter 메서드)를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다. 즉 해당 클래스를 상속받을 수 없도록 만들어야 한다.
  3. 모든 인스턴스 필드를 final 로 선언한다.
  4. 모든 인스턴스 필드를 private 로 선언한다.
    이렇게 함으로써 생성자를 통해 초기화된 이후로 해당 클래스의 인스턴스 필드는 수정될 수 없다. (1번에 의해서 객체의 상태를 변경하는 메서드가 존재하지 않고, 3번에 의해서 final 로 명시적으로 선언되어있기 때문이다)
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 해야한다.
    아이템 15에서 잠시 등장했던 개념인데, 가변 객체에다가 private final 키워드를 붙여봤다 해당 객체 자체에 대해서는 수정할 수 있다. 따라서 해당 인스턴스 필드에 직접 접근할 수 없도록 해야한다.

 

public final class Complex {
    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 plux(Complex c) {
    	return new Complex(re + c.re, im + c.im);
    }
    ...
}

이 클래스는 복소수를 표현한다. plus 메서드에서 인스턴스 자기 자신을 수정하는 것이 아니라 새로운 Complex 인스턴스를 만들어 반환하는 모습을 자세히 보자. 이렇게 작성하는 것을 함수형 프로그래밍이라고 한다.

 

함수형 프로그래밍에 익숙하지 않다면 조금 부자연스러워 보일 수 있지만, 이 방식으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아진다는 장점이 생기게 된다. 불변 객체는 단순하다. 불변 객체는 생성된 시점의 상태를 가비지 컬렉터에 의해서 해제될 때까지 그대로 유지하기 때문이다.

 

또한 불변 객체는 스레드에 안전하여 따로 동기화해줄 필요도 없다. 어차피 Read 만할 뿐이지 객체를 수정할 일이 없기 때문이다. 따라서 불변 객체는 안심하게 공유할 수 있고 개발자는 이러한 부분을 고민하지 않아도 된다.

 

뿐만 아니라 String 클래스와 같이 한 번 생성된 불변 클래스의 인스턴스는 중복해서 생성되지 않도록 정적 팩토리 메서드를 제공할 수 있다. 이렇게 함으로써 메모리 사용량과 가비지 컬렉션 비용을 줄일 수 있다. 불변 클래스는 이처럼 중복된 인스턴스가 생성되는 것을 피하려고 해야 한다. 따라서 clone, 복사 생성자와 같이 중복된 인스턴스를 생성하기 위한 방법들을 제공해서는 안된다.

 

 

하지만 이러한 불변 클래스에도 단점이 존재하는데, 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이다. 따라서 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용이 사용될 수 있다.

 

위 예제에서는 final 키워드를 붙여 상속을 방지했지만 이번엔 private 생성자를 통해서 유연하게 상속을 방지하는 예제를 살펴보자.

 

public class Complex {
    private final double re;
    private final double im;
    
    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    public static Complex valueOf(double re, double im) {
    	return new Complex(re, im);
    }
    ...
}

이 방식이 최선일 때가 많다. 바깥에서 볼 수 없는 package-private, private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 훨씬 유연하다. public, protected 접근제어자를 가지는 생성자가 존재하지 않으니 패키지 바깥의 클라이언트에서 바라본 이 불변 클래스는 사실상 final 이라고 볼 수 있다.

 

이렇게 구성하면 똑같은 값을 다시 요청했을 때 캐시해둔 값을 반환하여 계산 비용을 절감하도록 캐싱을 구현할 수 있다. 이 또한 해당 객체가 불변이기 대문에 사용할 수 있는 것이다.

정리


불변 객체의 장점은 정말 많다. 하지만 단점은 특정한 상황에 성능 저하가 유발될 수 있다는 것 뿐이다.

 

항상 클래스를 설계할 때 불변 클래스로 만들 수 있는지 여부를 고민해야 한다. 단순한 값을 다루는 클래스는 불변 클래스로 만들어야 하며, String, BigInteger 와 같이 무거운 값을 다루는 클래스도 불변으로 만들 수 있는 고민해봐야 한다. 성능 때문에 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공하도록 하자.

(가변 동반 클래스가 뭐지....)

 

하지만 모든 클래스를 불변 클래스로 만들 수는 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이는 것이 중요하다. 객체가 가질 수 있는 상태의 수를 줄이면 예측하기 쉬워지고 그에 따라 자연스럽게 오류가 생길 가능성이 줄어든다.

 

다른 합당한 이유가 없다면 모든 인스턴스 필드는 private final 이여야 한다!!
확실한 이유가 없다면 생성자와 정적 팩토리 외에는 그 어떤 초기화 메서드도 public 으로 제공해서는 안된다. 여기서 초기화 메서드란 clone 이나 복사생성자와 같이 객체를 생성해주는 메서드를 말한다.


 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   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
글 보관함