[Effective Java] Item17. 변경 가능성을 최소화하라

2022년 03월 17일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

불변 클래스란?

  • 인스턴스의 내부 값을 수정할 수 없는 클래스이다.

    • 인스턴스에 있는 정보는 고정되어 객체가 파괴되는 순간까지 바뀌지 않는다.
  • 자바에서 제공하는 불면 클래스는 String, Wrapper class, BigInteger, BigDecimal 등이 있다.

이러한 불변 클래슨느 가변 클래스보다 설계, 구현, 사용이 쉽고 오류가 생길 여지가 적어 훨씬 안전하다는 장점이 있다.

불변 클래스를 만드는 5가지 규칙

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다.

    • 상속을 한 하위 클래스에서 발생할 수 있는 문제점을 막아준다.
    • 클래스를 final로 선언하는 방법이 대표적이다.
  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private로 선언한다.

    • 필드가 참조하는 가변 객체를 클라이언트가 직접 접근하는 일을 막는다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

    • 클래스에서 가변 객체를 참조하는 필드가 하나라도 있으면 클라이언트에서 해당 객체의 참조를 얻을 수 없도록 해야한다.

함수형 프로그래밍과 절차적/명령형 프로그래밍

// 함수형 프로그래밍 예시
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 Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
  ....
}
  • 함수형 프로그래밍

    • 피연산자에 함수를 적용해 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍이다.
    • 메서드 이름을 전차사를 사용한다. (ex. plus)
  • 절차적/명령형 프로그래밍

    • 함수형 프로그래밍과 다르게 메서드를 실행하면 자기 자신을 수정해 상태가 바뀌게 된다.
    • 메서드 이름을 동사를 사용한다. (ex. add)

함수형 프로그래밍으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아진다.

불변 객체의 장단점

장점

  • 불변 객체는 스레드끼리 영향을 줄 수 없는 thread-safe하여 따로 동기화를 할 필요가 없다. 그래서 안심하고 공유를 할 수 있다.
  • 불변 클래스는 인스턴스를 최대한 재활용하기를 권한다.

    1. 상수 사용: 자주 쓰이는 값들을 public static final의 상수로 제공을 하여 재사용할 수 있다.
    2. 정적 팩터리 제공: 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리 메서드를 제공할 수 있다. (정적 팩터리를 사용하면 클라이언트가 인스턴스를 공유해, 메모리 사용량과 가비지 컬랙션 비용이 줄어든다.)
  • 불변 객체는 복사를 해도 원본과 똑같아 의미가 없어 clone메서드나 복사 생성자를 제공하지 않는 것이 좋다.
  • 불변 객체는 자유롭게 공유할 수 있고, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 객체를 만들 때 다른 불변 객체들을 구성 요소로 사용하면 이점이 많다.

    • 구성 요소들이 바뀌지 않아 구조가 아무리 복잡하더라도 불변식을 유지하기 수월하다. (ex. Map의 Key로 사용)
  • 불변 객체는 그 자체로 실패 원자성을 제공한다.

    • 실패 원자성은 예외가 발생한 후에도 그 객체는 여전히 이전과 유요한 상태인 것을 의미한다.

단점

  • 값이 다르면 반드시 독립된 객체로 만들어야 한다는 단점이 있다.

    • 값의 가지수가 많아지면 이들을 모두 만드는 데 큰 비용이 든다.
    • 클라이언트가 복잡한 연산들을 정확히 예측할 수 있으면 package-private의 가변 동반 클래스로 충분하며 그렇지 않을 경우 클래스를 public으로 제공하는 것이 좋다.

불변 객체의 설계 방법

클래스가 불변이라 상속하지 못하게 하는 것을 보장하는 가장 쉬운 방법은 final을 선언하는 것이다. 하지만 더 유연한 방법이 있다.

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);
    }
  ...
}

모든 생성자를 private혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법이 있다. 해당 방법은 다음과 같은 이점이 있다.

  • 밖에서 볼 수 없는 package-private클래스를 원하는 만큼 만듣어 활용할 수 있다.(유연)
  • 패키지 밖에서 봤을 때 해당 클래스는 final과 같다.
  • 정적 팩터리 방식이 다수의 구현 클래스를 활용한 유연성을 제공하고, 객체 캐싱 기능을 추가해 성능을 끌어올릴 수도 있다.

핵심 정리

  • getter가 있다고 해서 무조건 setter를 만들지 말아야 한다.
  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이는 것이 좋다.

    • 객체를 예측하기 쉬워지고 오류의 가능성도 줄일 수 있다.
  • 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다.

    • 이유가 없다면, 생성자와 정적 팩터리 외에는 public을 가진 초기화 메서드를 제공하면 안된다.
    • 객체를 재활용할 목적으로 다시 초기화하는 메서드도 불가하다.
Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh