Java

[Effective Java] 아이템 17. 변경 가능성을 최소화하라

jwKim96 2023. 1. 4. 00:37

예제 코드는 여기서 보실 수 있습니다.

0. 들어가며

이번 아이템은 불변 클래스(Immutable Class) 에 대한 내용입니다.

클래스를 불변으로 만들기 위한 규칙과 장단점 등에 대해 알아볼 예정입니다.

1. 불변 클래스

불변 클래스란 말 그대로 내부의 값을 수정할 수 없는 클래스를 의미합니다.

생성된 시점에 상태가 확정되어, 소멸되기 전까지 달라지지 않습니다.

이렇게 불변 클래스로 만들면 설계, 구현, 사용이 쉬워지며 오류 발생 여지도 줄어들게 됩니다.

이 대표적인 예가 바로 java.util.Datejava.time.LocalDate 입니다.

java.time 패키지 TMI(Joda time)
  Java 8 릴리즈 전까지 사용되던 Date 는 가변 클래스입니다.
  이 때문에 위에서 말한 여러 문제들이 있어 불변클래스의 필요성이 대두되었습니다.
  그래서 Joda time 이라는 라이브러리가 이 문제를 해결하였고, 널리 사용되었습니다.
  결국 Joda time 은 Java 공식 스펙이 되어 Java 8 에서 `java.time` 패키지로 병합되었습니다.

아래에서 불변 클래스의 예시가 필요할 때 이 두 클래스를 비교하며 살펴보겠습니다.

1.1 불변 클래스의 다섯가지 규칙

이펙티브 자바에서는 이러한 불변 객체를 만들기 위해서는 다음 다섯가지 규칙을 만족해야 한다고 합니다.

  • 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
  • 모든 필드를 final 로 선언한다.
  • 모든 필드를 private 으로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

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

일반적으로 객체는 자바 빈 규약의 ‘getter, setter 를 제공해야 한다’ 라는 규약에 따라 getter, setter 를 제공합니다.

그래서 이전에는 여러 자바 기술들이 자바 빈 규약에 따라 설계되었습니다.

하지만 불변 객체의 중요성이 대두되며 setter 사용을 지양하는 흐름으로 변화되었는데요.

Date 클래스는 다음과 같은 많은 변경 메서드들이 있었습니다.

image

하지만 변경할 수 없고, 멀티 스레드에서도 안전한 날짜 & 시간 관련 클래스가 필요했습니다.

그래서 등장한 LocalDate 클래스는 setter 를 제공하지 않는 불변 클래스 입니다.

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

클래스를 상속하여 메서드를 오버라이딩 하거나 추가하면 변경이 가능하도록 변할 수 있습니다.

이 때문에 하위 클래스에서 객체의 상태를 변경시킬 수 없도록 상속을 방지하는것이 필요한데요.

그래서 LocalDate 클래스는 final 클래스로 선언하여 상속을 방지하고 있습니다.

image

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

먼저, final 키워드를 이용하면 언어 차원에서 변경을 제한하기 때문에 강력합니다.

그리고 자바 언어에서는 final 로 선언된 필드는 멀티스레드에서도 안전하다고 합니다.

(Java Language Specification, final field)

Date 클래스 내부적으로 CalendarDate 라는 클래스가 사용되는데요.

CalendarDate 클래스의 경우 모든 필드가 변경 가능한 필드인데 반해, LocalDate 는 모두 final 이 붙어있습니다.

image

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

이펙티브자바에서는 가능한 모든 필드를 private 으로 선언할 것을 요구합니다.

private 으로 선언하면 클라이언트가 직접 접근하여 수정하는 일을 방지할 수 있습니다.

하지만 public 을 붙여도 final 을 붙여도 불변이 되기는 하는것 아니냐는 생각을 할 수 있는데요.

이에 대해서 책에서는 public final 필드를 수정할 경우 파급효과가 크기 때문에 권하지는 않는다고 합니다.
(더 자세한 내용은 아이템 15, 16 참고)

5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

클래스 내에 가변 객체가 있다면 이를 절대 외부에 노출하면 안된다고 합니다.

그리고 노출 하더라도 복사를 통해 전달해야하며 반드시 방어적 복사를 수행해야 합니다.
(생성자, Getter, readObject 메서드(아이템 88 참조) 모두에서 방어적 복사를 해야함)

이를 Date 와 LocalDate 클래스를 통해 살펴보겠습니다.

// Date.setDate 메서드
public void setDate(int date) {
  getCalendarDate().setDayOfMonth(date);
}

// CalendarDate.setDayOfMonth 메서드
public CalendarDate setDayOfMonth(int date) {
    if (dayOfMonth != date) {
        dayOfMonth = date;
        normalized = false;
    }
    return this;
}

// ---------------------------------------------------------------------------

// LocalDate.plusDays 메서드
public LocalDate plusDays(long daysToAdd) {
    if (daysToAdd == 0) {
    return this;
  }
  long dom = day + daysToAdd;
  if (dom > 0) {
    if (dom <= 28) {
      return new LocalDate(year, month, (int) dom);
    } else if (dom <= 59) { // 59th Jan is 28th Feb, 59th Feb is 31st Mar
      long monthLen = lengthOfMonth();
      if (dom <= monthLen) {
        return new LocalDate(year, month, (int) dom);
      } else if (month < 12) {
        return new LocalDate(year, month + 1, (int) (dom - monthLen));
      } else {
        YEAR.checkValidValue(year + 1);
        return new LocalDate(year + 1, 1, (int) (dom - monthLen));
      }
    }
  }

  long mjDay = Math.addExact(toEpochDay(), daysToAdd);
  return LocalDate.ofEpochDay(mjDay);
}

// LocalDate.ofEpochDay 메서드
public static LocalDate ofEpochDay(long epochDay) {
  ... 생략 
  return new LocalDate(year, month, dom);
}

위는 두 클래스의 날짜를 변경하는 메서드 인데요.

Date 클래스는 내부 캘린더 객체의 날짜 필드를 변경하는 메서드를 호출합니다.

반면에 LocalDate 의 경우에는 return 문에서 항상 새로운 객체를 생성하여 반환합니다.

1.2 불변 클래스의 특징

  • 불변 객체는 단순함
  • 멀티 스레드 환경에서도 동기화할 필요가 없음
  • 원자성을 제공
  • 값이 달라지면, 별도의 객체를 만들어야 한다는 것

1) 불변 객체는 단순함

가변 객체는 상태가 변할 수 있기 때문에, 이를 고려하여 여러 안전장치가 필요합니다.

반면에 불변 객체는 한번 설정된 상태를 그대로 간직합니다.

그 때문에 불변 객체는 내부에서나 외부에서나 관련된 로직이 단순해질 수 밖에 없습니다.

이는 다음 특징인 2번과도 연관이 깊습니다.

2) 멀티 스레드 환경에서도 동기화할 필요가 없음

불변 객체는 값이 변경되면 새로운 객체로 반환하기 때문에 멀티 스레드 환경에서도 안전합니다.

// 가변 객체
class MutablePoint {
    private int x;
    private int y

    public MutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    getter, setter ...

}

// 불변 객체
class ImmutablePoint {
    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    getter ...

    public ImmutablePoint setX(int x) {
        return new ImmutablePoint(x, this.y);
    }

    public ImmutablePoint setY(int y) {
        return new ImmutablePoint(this.x, y);
    }
}

image

위 처럼 가변, 불변의 특징을 가진 두 Point 클래스가 있습니다.

MutablePoint 의 경우에는 멀티 스레드 환경에서 getter, setter 에서 race condition 이 발생할 수 있습니다.

반면에 ImmutablePoint 의 경우에는 setter 에서 항상 다른 객체를 반환합니다.

그래서 멀티 스레드 환경에서도 안전하게 객체를 다룰 수 있습니다.

3) 원자성을 제공

원자성은 위 2번의 예제에서 같이 설명이 가능합니다.

ImmutablePoint 의 setter 메서드의 경우 각 작업은 동일한 조건이라면 동일한 결과를 반환합니다.

4) 값이 달라지면, 별도의 객체를 만들어야 한다는 것

이번 예제 또한 2번의 예제에서 잘 설명되어있습니다.

ImmutablePoint 는 setter 에 의해 상태가 변경되면, 새로운 객체를 생성해서 반환합니다.

하지만, 만약 객체를 생성하는 과정의 비용이 크다면 어떨까요?

  • 생성과정에서 일회성으로 생성되고 소모되는 객체가 많은 경우
  • 복잡한 계산 로직임 많이 들어가는 경우

등등의 경우에는 잠재적인 성능 문제를 야기할 수 있습니다.

그래서 이펙티브 자바에서는 다단계 연산 이라는 방법을 해결책으로 제시합니다.

ECT. 다단계 연산

다단계 연산(multi-step operation) : 여러 단계를 거쳐 연산을 수행하는 방식

다단계 연산은 동반클래스를 두는 방식으로 많이 구현하며 대표적인 예로 아래 클래스들이 있습니다.

  • String ←→ StringBuilder, StringBuffer
  • BigInteger ←→ MutableBigInteger

BigInteger 의 내부 연산 과정에서 값이 어려번 바뀌는 경우가 있습니다.

이런 경우에 MutableBigInteger 를 이용해서 새로운 객체를 생성하지 않고 값을 변경한 다음

최종 결과를 다시 BigInteger 로 변환하여 반환하기도 합니다.

image

토론하고싶으신 부분이나 질문이 있으시면 편하게 남겨주세요.