이펙티브 자바 - item 17. 변경 가능성을 최소화하라

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

이펙티브 자바 - item 17. 변경 가능성을 최소화하라

불변 클래스

  • 불변 클래스란 그 인스턴스 내부 값을 수정할 수 없는 클래스
  • 객체가 파괴되는 순간까지 절대 달라지지 않는다.
    • ex. String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal ..
  • 가변 클래스보다 설계하고 구현하고 사용하기 쉽고 오류가 생길 여지가 적으며 훨씬 안전하다.
  • 멀티쓰레드 환경에서 유리하다.

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

  • 객체의 상태를 변견하는 메서드(변경자) 즉, setter를 제공하지 않는다.
  • 클래스를 확장 할 수 없도록 한다
    • 하위 클래스에서 객체의 상태를 변하게 만드는 상태를 막아준다. ( 상속을 허용하지 않는다.)
    • 상속을 막는 방법에는 final로 선언하는 방법과, 생성자를 제공하지 않고 정적 팩터리 메서드만 제공하는 방법이 있다.
  • 모든 필드는 final로 선언한다.
    • 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법
    • 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는데도 필요하다
  • 모든 필드를 private으로 선언한다.
    • 필드가 참조하는 가변 객체를 클라이언트에서 직접 수정하는 일을 막아준다.
    • 기술적으로 필드를 public final로 선언해도 불변 객체가 되지만 내부 표현을 바꾸지 못하므로 권장 X
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야한다.
    • 접근자 메서드가 그 필드를 그대로 반환하게 해서는 안된다.
    • 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행해라
public final class Person {

    private final Address address;

    public Person(Address address) {
        this.address = address;
    }

    public Address getAddress() {
        return address;
    }

    public static void main(String[] args) {
        Address seattle = new Address();
        seattle.setCity("Seattle");

        Person person = new Person(seattle);

        Address redmond = person.getAddress();
        redmond.setCity("Redmond");

        System.out.println(person.address.getCity());
    }
}
package me.whiteship.chapter04.item17.part1;

public class Address {

    private String zipCode;

    private String street;

    private String city;

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }
}

위와 같이 adress의 레퍼런스는 final이지만 adress의 정보들은 final이 아닐 경우가 발생할 수 있다. 이러한 경우 방어적 복사를 통해 해결하도록 하자.

public Address getAddress() {
        Address copyOfAddress = new Address();
        copyOfAddress.setStreet(address.getStreet());
        copyOfAddress.setZipCode(address.getZipCode());
        copyOfAddress.setCity(address.getCity());
        return copyOfAddress;
    }

불변 객체

  • 함수형 프로그래밍에 적합하다.
  • 불변 객체는 단순하다.
    • 생성 시점의 상태를 파괴될 때까지 간직한다.
    • 모든 생성자가 클래스 불변식(class invariant)을 보장한다면 별다른 노력 없이 영원히 불변으로 남는다. 반면 가변 객체는 복잡한 상태에 놓일 수도 있다.
  • 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 불변 객체는 근복적으로 스레드 안전하여 따로 동기화할 필요 없다.
    • 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 안심하고 공유할 수 있다.
  • 가장 쉬운 불변 클래스의 인스턴스 재활용 방법은 자주 쓰이는 값들을 상수(public static final)로 제공하는 것이다.
  • 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.
    • 정적 팩터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
  • 새로운 클래스를 설계할 때 public 생성자 대신 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙일 수 있다.
  • 불변 객체는 방어적 복사가 필요 없다.
  • 불변 객체는 그 자체로 실패 원자성을 제공한다.

실패 원자성이란 ‘메서드에서 예외가 발생한 후에도 그 객체는 여전히 (메서드 호출 전과 똑같은) 유효한 상태여야 한다’는 성질이다.

불변 객체의 단점

  • 불변 객체는 값이 다르다면 반드시 독립된 객체로 만들어야 한다.
    • 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 할 수 있다.
  • 이에 대처하는 방안은 두 가지이다. 첫 번째는 흔히 쓰일 다단계 연산(multistep operation)들을 예측하여 기본 기능으로 제공하는 방법이다.
  • 클라이언트가 원하는 복잡한 연산들을 정확히 예측할 수 있다면 이러한 다단계 연산 속도를 높여주는 package-private인 가변 동반 클래스(companion class)만으로도 충분하다.
  • 예측이 불가능하다면 가변 동반 클래스를 public으로 제공하는 게 최선이다.
    • ex. StringBuilder, StringBuffer ..

정리

  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 단순한 값 객체는 불변으로 만들도록 하자.
  • 성능 때문에 어쩔 수 없다면 불변 클래스와 함께 가변 동반 클래스를 public 클래스로 제공하도록 하자.
  • 모든 클래스를 불변으로 만들 수는 없다. 하지만 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이도록 하자.
  • 합당한 이유가 없다면 모든 필드는 private final이어야 한다.꼭 변경해야 하는 필드를 제외한 나머지 모두를 final로 선언하도록 하자.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.