카테고리:

업데이트:

1. 들어가기

불변 클래스란, 간단히 말해 인스턴스의 내부 값을 수정할 수 없는 클래스입니다.

불변 클래스로부터 만들어진 인스턴스의 내부 값은 객체가 파괴되는 순간까지 절대 달라지지 않습니다.

그럼 불변 클래스를 왜 사용하는 걸까요?

불변 클래스의 장단점과 함께 불변 클래스를 어떻게 만들 수 있는지 알아봅시다.

2. 불변 클래스의 장점

  1. 불변 객체는 단순하다.

    불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직합니다.

    그래서 프로그래머는 다른 노력을 들이지 않더라도 영원히 불변으로 남습니다.

    반면, 가변 객체는 변경자 메서드로 인해 복잡한 상태에 놓일 수 있습니다.

  2. 불변 객체는 Thread-safe 하다.

    불변 객체는 값이 절대 변하지 않기 때문에 여러 스레드가 동시에 사용해도 절대 훼손되지 않습니다.

  3. 불변 객체는 자유롭게 공유할 수 있고 불변 객체끼리 내부 데이터를 공유할 수 있다.

    아무리 복사해봐야 원본과 똑같기 때문에 방어적 복사의 의미가 없습니다.

    그렇기 때문에 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋습니다.

    내부 데이터 공유는 BigInteger 클래스로 예시를 들어보겠습니다.

    BigInteger 클래스는 부호(int 변수)와 크기(int 배열)를 가지는데

    그 중 negate 메서드는 크기가 같고 부호만 반대인 새로운 BigInteger를 생성하는 메서드입니다.

     public BigInteger negate() {
         return new BigInteger(this.mag, -this.signum);
     }
    

    이 때 배열은 비록 가변이지만, 복사하지 않고 원본 인스턴스와 공유해서 사용하는 것을 볼 수 있습니다.

  4. 불변 객체는 그 자체로 실패 원자성을 제공한다.

    실패 원자성이란, ‘메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태여야 한다’는 의미로

    불변 객체의 메서드는 내부 상태를 변경하지 않기 때문에 이 성질을 만족합니다.

3. 불변 클래스의 단점

  1. 불변 객체는 값이 하나라도 다르면 반드시 새로운 객체로 만들어야 한다.

    예를 들어, 백만 비트의 BigInteger에서 비트 하나를 바꿔야 할 때,

    비트 하나의 값을 바꾸는게 아닌 새로운 객체를 생성해야 합니다.

    이는 시간과 공간을 잡아먹기 때문에 성능 문제로 이어질 수 있습니다.

4. 불변 클래스 생성 규칙

그럼 불변 클래스를 어떻게 생성하면 되는지 생성 규칙을 통해 알아봅시다.

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.

    불변 클래스는 내부 값이 변하지 않는 클래스이므로 객체의 상태를 변경하는 매개를 제공하면 안됩니다.

  2. 클래스를 확장할 수 없도록 한다.

    하위 클래스에서 부주의하게 또는 나쁜 의도로 객체의 상태를 변하게 만드는 것을 막아줍니다.

    대표적인 예로 클래스를 final로 선언하는 것입니다.

  3. 모든 필드를 final로 선언한다.

    시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법입니다.

    모든 필드의 값은 객체가 파괴되는 순간까지 절대 달라지지 않습니다.

  4. 모든 필드를 private으로 선언한다.

    필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아줍니다.

    public final로 선언해도 불변 필드가 되지만 이는 내부 표현을 바꾸지 못하는 단점이 있습니다.

5. 불변 클래스 예시

앞서 알아보았던 생성 규칙을 바탕으로 하나의 예시를 들어보겠습니다.

/* 불변 복소수 클래스 */
  public final class Complex {  // (2) 상속을 막기 위해 final 키워드를 사용한다.
    private final double re;    // (3, 4) final과 private 사용
    private final double im;    // (3, 4) final과 private 사용

    // (1) set은 제공하지 않는다.
    public double getReal() { return re; }
    public double getImaginary() { return im; }

    public Complex(double re, double im) {
      this.re = re;
      this.im = im;
    }

    // 주의 깊게 봐야 할 부분
    public Complex plus(Complex c) {
      return new Complex(re + c.re, im + c.im);
    }

    ...
  }

위의 예시에서 주의 깊게 봐야 할 부분을 봅시다.

해당 코드는 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환하고 있습니다.

이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만,

피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라고 합니다.

함수형 프로그래밍은 코드에서 불변이 되는 영역의 비율을 높일 수 있는 장점이 있습니다.

6. 또 다른 불변 클래스 생성 방법

앞서 불변 클래스를 생성할 때 상속을 막기 위해 final 키워드를 사용했습니다.

하지만, 이보다 더 유연한 방법이 있습니다.

바로, 모든 생성자를 private 또는 package-private으로 만들고 public 정적 팩터리를 제공하는 방법입니다.

앞서 사용했던 예시를 이에 맞게 조금 변형해보겠습니다.

/* 불변 복소수 클래스 */
   public class Complex { // final 키워드 제거
      private final double re;
      private final double im;

      private Complex(double re, double im) { // 생성자의 접근 범위를 private으로 선언
         this.re = re;
         this.im = im;
      }

      public static Complex valueOf(double re, double im) { // 정적 팩터리 메서드 제공
         return new Complex(re, im);
      }

      ...
   }

패키지 외부에서 바라본 이 클래스는 public이나 protected 생성자가 없기 때문에 사실상 final입니다.

하지만, 정적 팩터리를 통해 다수 구현 클래스를 활용한 유연성을 제공하고

객체 캐싱 기능을 추가해 성능을 끌어올릴 수도 있습니다.

7. 정리

이번 포스트는 불변 클래스의 장단점과 함께 불변 클래스를 생성하는 방법에 대해 알아보았습니다.

클래스는 꼭 필요한 경우가 아니라면 장점이 많은 불변 클래스로 만드는 것이 좋고,

불변으로 만들 수 없는 클래스라면 변경할 수 있는 부분을 최소한으로 줄여야 합니다.

그리고 다른 합당한 이유가 없다면 모든 필드는 private final이어야 합니다.

            
              📕 개인 기록용 블로그입니다.
              😊 오타나 잘못된 정보가 있을 경우 댓글이나 메일로 말씀해주시면 바로 수정하겠습니다! 😊
          

댓글남기기