객체 지향 프로그래밍에 있어서 불변객체(immutable object)는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 반대 개념으로는 가변(mutable) 객체로 생성 후에도 상태를 변경할 수 있다. 객체 전체가 불변인 것도 있고, C++에서 const 데이터 멤버를 사용하는 경우와 같이 일부 속성만 불변인 것도 있다. 또, 경우에 따라서는 내부에서 사용하는 속성이 변화해도 외부에서 그 객체의 상태가 변하지 않은 것처럼 보인다면 불변 객체로 보기도 한다. 예를 들어, 비용이 큰 계산의 결과를 캐시하기 위해 메모이제이션(Memoization)을 이용하더라도 그 객체는 여전히 불변하다고 볼 수있다. 불변 객체의 초기 상태는 대개 생성 시에 결정되지만 객체가 실제로 사용되는 순간까지 늦추기도 한다.

불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고, 성능 개선에도 도움을 준다. 하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우엔 불변이 오히려 부적절한 경우가 있다. 이 때문에 많은 프로그래밍 언어에서는 불변이나 가변 중 하나를 선택할 수 있도록 하고 있다.

배경 편집

대부분의 객체 지향 언어에서 객체는 참조(reference) 형태로 전달하고 받는다. Java, C++, Python, Ruby 등이 그 예이다. 객체가 참조를 통해 공유돼 있다면 그 상태가 언제든지 변경될 가능성도 커지므로 문제가 된다.

불변 객체는 객체를 복제할 때 객체 전체가 아니라 단순히 참조만 복사하고 끝난다. 참조는 보통 객체 그 자체보다 훨씬 작아서(전형적으로 포인터 크기), 메모리가 절감되며 프로그램의 성능에도 좋다. 가변 객체는 참조 복사 기법으로 다루기 곤란하다. 이유는 가변 객체의 참조를 가지고 있는 어떤 장소에서 객체를 변경하면 참조를 공유하는 모든 장소에서 그 영향을 받기 때문이다. 이것이 의도한 동작이 아니라면 참조를 가지고 있는 다른 장소에 변경 사실을 통지하고 대처하는 추가 대응이 필요하다. 이런 경우 비용은 조금 들지만 참조가 아닌 객체 전체를 방어적 복사(defensive copy) 하는 간단한 방법으로 대응할 수 있다. 또는, Observer 패턴을 가변 객체의 변경에 대처하는 방법으로 사용할 수 있다.

불변 객체는 멀티 스레드 프로그래밍에서도 유용하다. 데이터가 불변 객체에 저장돼 있다면 복수의 스레드에 의해서 특정한 스레드의 데이터가 변경될 우려없이 데이터에 접근할 수 있다. 즉, 배타 제어(mutual exclusion)를 할 필요가 없다. 쉽게 말해 불변 객체가 가변 객체보다 스레드 세이프(Thread-safe) 하다고 생각하면 된다.

객체 전체 대신 참조를 복제하는 기법은 인턴(intern, 문자열 객체를 만들면 매번 메모리에 새로운 객체가 만들어지는데 이를 인턴하여 문자열 풀 -String Pool-에 저장하고 그 뒤 같은 문자열이 호출되면 풀에서 참조를 복사해 반환하는 기법)으로 알려져 있다. 인턴이 사용되고 있다면 2개의 객체가 같다고 판단되는 경우는 참조가 같은 경우다.

구현 편집

불변이란, 객체가 컴퓨터의 메모리 내에서 쓰기를 할 수 없다는 뜻이 아니다. 오히려 불변은 컴파일 시의 문제이며, 프로그래머가 「무엇을 해야하는가」 이지 반드시 「무엇이 가능한가」가 아니다.

현대적인 하드웨어가 지원하고 있는 가변과 불변의 장점이 잘 섞인 기법은 카피 온 라이트(Copy-On-Write)다. 이 기법에서는 이용자가 시스템에 객체를 복제하도록 명하면 복사 대신 동일한 객체를 가리키는 참조를 만든다. 그리고 이용자가 그 참조를 통해 객체를 변경하면 그때 진짜 복제를 만들고 그것을 가리키는 참조를 다시 생성한다. 이에 의해 다른 이용자는 영향을 받지 않는다. 왜냐하면 여전히 원본 객체를 참조하고 있기 때문이다. 카피 온 라이트 환경에서 모든 이용자는 가변 객체를 갖고 있는 것처럼 보이지만 그 객체를 변경하지 않는한 불변 객체로서 실행 효율을 높일 수 있다. 카피 온 라이트는 가상 기억 시스템에서 자주 사용되며 프로그램이 다른 프로그램에 메모리를 수정할 걱정없이 메모리를 절약할 수 있다.

불변의 전형적인 예는 String 클래스의 인스턴스다.

String str = "ABC";
str.toLowerCase();

메서드 toLowerCase()는 변수 str의 값 "ABC"를 변경하지 않고 대신 새로운 String 객체가 생성되고 생성 시 "abc"라는 값이 주어진다. 이 String 객체에 대한 참조는 toLowerCase() 메서드가 반환한다. 변수 str에 값 "abc"를 갖게 하고 싶다면 아래와 같이 작성한다.

str = str.toLowerCase();

String 클래스에는 인스턴스의 데이터를 변경하는 메서드가 없다.

객체가 불변하기 위해서는 가변적 필드가 있는지 여부와 별개로, 외부에서 그 필드를 변경하는 방법이 존재하면 안되며 또 가변적인 필드를 접근하는 방법도 있어서는 안된다. 아래는 Java에서 가변 객체의 예를 나타낸다.

class Cart<T> {
  private final List<T> items;

  public Cart(List<T> items) { this.items = items; }

  public List<T> getItems() { return items; }
  public int total() { /* return sum of the prices */ }
}

이 클래스의 인스턴스는 불변적이지 않다. 왜냐하면 인스턴스화 할 때 전달한 List 객체를 어딘가에서 보유하는 것으로 필드가 변경될 가능성이 있으며 getItems()를 호출해 가변적인 items 필드에 접근할 수 있기 때문이다. 아래 ImmutableCard 클래스는 부분적으로 불변하도록 작성한 예이다.

class ImmutableCart<T> {
  private final List<T> items;

  public ImmutableCart(List<T> items) {
    this.items = Arrays.asList(items.toArray());
  }

  public List<T> getItems() {
    return Collections.unmodifiableList(items);
  }
  public int total() { /* return sum of the prices */ }
}

아래는 Ruby에서의 비슷한 예다.

class Cart
  def initialize(items)
    @items = items.dup.freeze
  end

  def items
    @items.clone
  end

  def total
    # sum of the prices
  end
end

이제는 items를 변경할 수 없다. 하지만, 리스트 items의 요소도 불변이라는 보증은 없다. 해법 중 하나로는 Decorator 패턴으로 리스트의 각 요소를 랩핑할 수 있다.

C++ 에서는 Cart를 const-correct한 구현을 하는 것으로 인스턴스를 불변(const) 또는 가변 즉, 원하는대로 생성하도록 할 수 있다. 쉽게 말해 2개의 다른 getItems()를 제공한다.

template<typename T>
class Cart {
 private:
  std::vector<T> items;

 public:
  Cart(std::vector<T> v): items(v) { }

  std::vector<T>& getItems() { return items; }
  const std::vector<T>& getItems() const { return items; }
  int total() const { /* return sum of the prices */ }
};

위 C++의 예는 불변, 가변 겸용으로 만든 것이다. 이를 위해 두 개의 생성자(Constructor)를 준비할 필요는 없으며, 실제로 가능하지도 않다. Cart 타입의 변수를 선언할 때 const 선언 여부로 결정된다.

이전에 작성한 Java 코드가 불변할 수 없는 이유는 또 있는데 클래스를 상속할 수 있다는 것에서 기인한다. 서브 클래스에서 멋대로 items를 변경하는 setter 메서드가 구현될 수 있다.

따라서 class에 final 수식자를 부여한다. 혹시 모르니 메서드 인수에도 final 수식자를 부여한다. 이어서 "방어적 복사"라는 기법을 이용해 getItems()에서 반환 받은 List가 변경되더라도 ImmutableCard 클래스가 보유하는 items 필드의 내용이 변경되지 않도록 복사한다. 위에서는 List를 배열로 변환하고 되돌리는 방법이 사용됐지만 여기에서는 Object.clone(Object) 메서드를 이용해 해결한다.

final class ImmutableCart<T> {
  private final List<T> items;

  public ImmutableCart(final List<T> items) {
    //방어적 복사한 뒤 리스트 요소의 실행 시 타입 검사를 한다.
    //리스트의 요소를 final로 한다.
    this.items = Collections.unmodifiableList(
      Collections.checkedList(
        (List<T>) items.clone(), T.class
      )
    );
  }

  public List<T> getItems() {
    // 내부 필드의 방어적 복사
    return (List<T>) items.clone();
  }
  public int total() { /* return sum of the prices */ }
}

인수의 객체를 일단 clone()으로 복사하고 나서 필드에 대입함으로써 인수에 전달되기 전의 List 객체와 참조를 떼어 낼 수 있다. 따라서 인수에 전달되기 전의 List 객체를 변경해도 ImmutableCard에 있는 List 객체 items까지 영향이 미치지 않는다. 방어적 복사는 getItems() 메서드에서도 할 필요가 있다. 추가적으로 코드를 설명하자면 Collections.checkedList(List<T>, Class<T>)는 items가 다른 메서드에 전달 됐을 때 리스트에 T와 다른 타입이 대입 되는 것을 막기 위해 사용한다. Collections.unmodifiableList(List<T>)는 리스트의 요소를 변경하지 못하도록 final 하기 위해 사용한다.

또, 클래스가 불변인지 확인하는 하나의 방법으로 FindBugs라는 도구를 사용할 수 있다. 이는 버그의 원인이 되는 코드를 자동으로 검출해주는 도구인데 불변 클래스를 작성할 때에도 도움을 준다. 만약 자바스크립트에서 불변 객체를 운영하고 싶다면 페이스북의 [[Immutable.js|https://web.archive.org/web/20150809185757/http://facebook.github.io/immutable-js/%5D%5D%EB%A5%BC 이용한다.

참고자료 편집

외부 링크 편집