ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 17: Minimize mutability
    독서/Effective Java 2021. 12. 19. 14:11

    변경 가능성을 최소화하라.

     

    불변 클래스?

    불변 클래스는 단순히 인스턴스를 수정할 수 없는 클래스로, 각 인스턴스에 포함된 모든 정보는 Object의 수명 동안 고정되기에 변경 사항을 파악할 수 없다.

    Java 플랫폼 라이브러리에는 String, boxed 프리미티브 클래스, BigInteger 및 BigDecimal을 비롯한 많은 변경할 수 없는 클래스가 이미 존재한다.

    변경 불가능한 클래스는 변경 가능한 클래스보다 설계, 구현 및 사용에 용이하고 오류가 덜 발생하고 더 안전하다.

     

    불변 클래스 생성의 규칙

    • 객체의 상태를 수정하는 메서드(변경자)를 제공하지 않는다.
    • 클래스를 확장할 수 없는지 확인한다.
      • 부주의하거나 악의적인 서브클래스가 Object의 상태가 변경된 것처럼 행동하여 클래스의 불변 동작을 손상시키는 것을 방지한다. 서브클래싱을 방지하는 것은 일반적으로 클래스를 final로 만드는 것으로 달성되지만, 추가적인 방법도 존재한다.
    • 모든 필드를 final로 선언한다.
      • 설계자의 의도를 명확하게 드러낸다.
      • 생성된 인스턴스를 다른 스레드로 전달해도 값 Object이기 때문에 문제없이 동작한다.
    • 모든 필드를 private로 설정한다.
      • 클라이언트가 필드에서 참조하는 가변 Object에 접근해 직접 수정하는 것을 방지할 수 있다.
    • 모든 변경 가능한 구성 요소에 대한 독점적인 접근을 보장하자.
      • 클래스에 가변 Object를 참조하는 필드가 있는 경우 클라이언트가 해당 Object에 대한 참조를 얻을 수 없도록 하자.
      • 생성자, 접근자 및 readObject 메서드(Item 88)에서 방어 복사본(Item 50)을 만들어서 전달하도록 하자.

    예제로 보자.

     

    package com.tistory.povia.effectivejava.item17;
    
    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 plus(Complex c) {
        return new Complex(re + c.re, im + c.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);
      }
    
      // equals, hashCode, toString 영역
      @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)";
      }
    }

     

    불변 클래스의 이점

    불변 객체는 본질적으로 스레드로부터 안전하기에 동기화가 필요하지 않다. 동시에 액세스하는 여러 스레드에 의해 손상될 수 없고, 이것은 스레드 안전성을 달성하는 가장 쉬운 방법이다. 

    어떤 스레드도 불변 객체에 대한 다른 스레드의 영향을 관찰할 수 없기 때문에 불변 객체를 자유롭게 공유할 수 있다. 따라서 불변 클래스는 클라이언트가 가능한 경우 기존 인스턴스를 재사용하도록 권장해야 한다. 쉬운 방법은 일반적으로 사용되는 값에 대해 Public static 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);

    더 나아가서 불변 클래스는 자주 요청되는 인스턴스를 캐시하는 정적 팩토리(Item 1)를 제공하여 기존 인스턴스가 생성할 때 새 인스턴스 생성을 방지할 수 있다. 모든 boxed 기본 클래스와 BigInteger가 이 작업을 수행한다. 이러한 정적 팩토리를 사용하면 클라이언트가 새 인스턴스를 생성하는 대신 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
    새 클래스를 디자인할 때 공용 생성자 대신 정적 팩토리를 선택하면 클라이언트를 수정하지 않고도 나중에 캐싱을 추가할 수 있는 유연성을 얻을 수 있다.
    불변 객체는 방어적인 복사본을 만들 필요가 없다(항목 50). 불변 클래스에 복제 메서드 또는 복사 생성자(Item 13)를 제공할 필요가 전혀 없다. 
    불변 객체를 공유할 수 있을 뿐만 아니라 내부도 공유할 수 있다. 예를 들어 BigInteger 클래스는 내부적으로 부호 크기 표현을 사용한다. 부호는 int로 표시되고 크기는 int 배열로 표시된다. 부정 방법은 크기가 같고 부호가 반대인 새로운 BigInteger를 생성한다. 새로 생성된 BigInteger는 원본과 동일한 내부 배열을 가리킨다.
    불변 개체는 변경 가능하거나 변경할 수 없는 다른 개체를 위한 훌륭한 빌딩 블록을 만든다. 구성 요소 객체가 그 아래에서 변경되지 않는다는 것을 안다면 복잡한 객체의 불변성을 유지하는 것이 훨씬 쉬워진다. 가장 좋은 사용 방법은 맵의 key와 set의 원소로 쓰는 것이다.
    불변 객체는 그 자체로 실패 원자성을 제공한다(Item 76). 상태는 절대 변경되지 않으므로 일시적인 불일치 가능성이 없다.

     

    불변 클래스의 단점

    각 고유 값에 대해 별도의 객체가 필요하다. 특히 값의 범위가 큰 경우 비용이 많이 든다.

    Ex) 백만 비트 BigInteger의 하위 비트를 변경하는 경우

    BigInteger moby = ...;
    moby = moby.flipBit(0);

    flipBit 메서드는 100만 비트 길이의 새로운 BigInteger 인스턴스를 생성하며 원본과 단 1비트만 다른데 문제는 작업에는 BigInteger 크기에 비례하는 시간과 공간이 필요하다는 점이다. 반대로, java.util.BitSet은 BigInteger와 마찬가지로 임의의 긴 비트 시퀀스를 나타내지만 BigInteger와 달리 변경 가능하다. BitSet 클래스는 일정한 시간에 백만 비트 인스턴스의 단일 비트 상태를 변경할 수 있는 메서드를 제공한다.

    BitSet moby = ...;
    moby.flip(0);

    모든 단계에서 새 개체를 생성하는 다단계 작업을 수행하면 성능 문제가 확대된다.

     

    불변 클래스의 단점에 대한 대안

    • 일반적으로 필요한 다단계 작업이 추측 가능한 경우 다단계 연산의 속도를 높이는 데 사용하는 package-private 동반자 클래스를 사용하자. 
    • 다단계 작업의 추측이 불가능한 경우 변경 가능한 Public 클래스를 제공하자.
      • Ex) StringBuilder/StringBuffer -> String

     

    불변 클래스를 생성하는 디자인 대안

    불변성을 보장하기 위해 클래스는 자체적으로 하위 클래스화되는 것을 허용하면 안된다. 이것은 클래스를 final로 만들어 수행할 수 있지만 더 유연한 또 다른 대안이 존재한다. 변경할 수 없는 클래스를 final로 만드는 대신 모든 생성자를 private 또는 package-private로 만들고 공용 생성자 대신 Public Static Factory를 추가할 수 있다(Item 1).

    public final class Complex {
      // 불변으로 선언된 영역
      private final double re;
      private final double im;
    
      private Complex(double re, double im) {
        this.re = re;
        this.im = im;
      }
    
      // 생성자를 private로 바꾸고 valueOf을 통해서 값을 전달받도록 한다.
      public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
      }
    }

     

    주의: BigInteger, BigDecimal 설계 시

    BigInteger 및 BigDecimal의 모든 메서드가 재정의될 수 있기 때문에 인수의 불변성에 의존하는 클래스를 작성하는 경우 인수가 신뢰할 수 없는 하위 클래스의 인스턴스가 아니라 "실제" BigInteger 또는 BigDecimal인지 확인 후, 후자라면 변경 가능하다는 가정 하에 방어적으로 복사해야 한다(Item 50).

    public static BigInteger safeInstance(BigInteger val) {
      return val.getClass() == BigInteger.class ?
        val : new BigInteger(val.toByteArray());
    }

     

    규칙 완화

    불변 클래스에 대한 규칙 목록에는 어떤 메서드도 개체를 수정할 수 없고, 모든 필드는 final이어야 한다. 이러한 규칙은 필요한 것보다 약간 더 강력하며 성능을 향상시키기 위해 완화하는 것이 가능하다. 사실 어떤 메서드도 객체 상태에서 외부적으로 눈에 띄는 변화를 일으킬 수 없다. 그러나 일부 불변 클래스에는 처음 필요할 때 값비싼 계산 결과를 캐시하는 하나 이상의 nonfinal 필드가 존재한다. 동일한 값을 다시 요청하면 캐시된 값이 반환되어 재계산 비용이 절감된다. 이 트릭은 객체가 불변이기 때문에 정확하게 작동하고, 반복되는 경우 계산이 동일한 결과를 산출하도록 보장한다.
    Ex) PhoneNumber의 hashCode 메서드(Item 11)는 해시 코드가 처음 호출될 때 계산하고 다시 호출될 경우를 대비하여 캐시하며, 지연 초기화(Item 83)의 예인 이 기술은 String에서도 사용된다.

    정리

    모든 getter에 대해 setter를 작성하려는 충동에 저항하자. 클래스를 변경할 수 있는 아주 좋은 이유가 없는 한 클래스는 변경할 수 없어야 한다. 불변 클래스는 많은 이점을 제공하고, 단점이라고는 특정 상황에서의 성능 문제가 발생할 수 있다는 점이다. PhoneNumber 및 Complex와 같은 작은 값 개체(Value Obejct)는 항상 불변으로 만들자(java.util.Date 및 java.awt.Point와 같은 Java 플랫폼 라이브러리에는 변경 불가능해야 하지만 그렇지 않은 여러 클래스가 존재한다).

    String 및 BigInteger와 같은 더 큰 값 개체를 불변으로 만드는 것을 진지하게 고려해보자. 만족스러운 성능을 달성하는 데 필요하다는 것을 확인한 후에만 불변 클래스에 대한 공개 변경 동반자 클래스를 제공하도록 하자(Item 67).
    불변성이 효과적이지 않은 일부 클래스에는 가변성을 최대한 줄이자. 개체가 존재할 수 있는 상태의 수를 줄이면 더 쉽게 추론할 수 있고 오류 가능성이 줄어든다. 따라서 모든 필드를 최종 항목이 아닌 것으로 설정해야 하는 강력한 이유가 없는 한 모든 항목을 final로 설정하자. 이 항목과 Item 15의 내용을 결합하면, 달리 해야 할 합당한 이유가 없는 한 모든 필드를 private final으로 선언하는 것이 자연스러운 경향이 되어야 한다.
    생성자는 모든 불변량을 설정하여 완전히 초기화된 객체를 생성해야 한다. 해야할 이유가 없다면 생성자 또는 정적 팩토리와 별도로 공개 초기화 메서드를 제공하지 말자. 마찬가지로, 다른 초기 상태로 생성된 것처럼 Object를 재사용할 수 있도록 하는 "재초기화" 메서드를 제공하지 말자. 복잡성이 증가하는 대신 성능상의 이점을 거의 제공하지 않는다.
    CountDownLatch 클래스는 이러한 원칙을 보여준다. 변경 가능하지만 상태 공간은 의도적으로 작게 유지되고, 인스턴스를 만들고 한 번만 사용하면 끝이며, 카운트다운 래치의 개수가 0에 도달하면 재사용할 수 없다.

    댓글

Designed by Tistory.