Java

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

jun9.com 2022. 4. 16. 00:27

1. 불변 클래스란?

  • 인스턴스 내부 값을 수정할 수 없는 클래스
  • 즉 인스턴스에 저장된 정보는 객체가 해제되기 전까지 절대 변하지 않는다.
  • 장점
    • 설계, 구현하기 쉽고, 사용하기도 쉽다.
    • 값이 변하지 않기 때문에 오류가 생길 여지가 적어 안전하다.
  • ex) String, Integer, Boolean, BigInteger, BigDecimal ...

 


2. 불변 클래스 만들 때 지켜야할 규칙

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
    • setter메서드 같이 외부에서 객체의 내부 상태를 바꿀 수 있는 메서드를 제공하면 불변이 깨지기 때문 
  2. 클래스를 확장할 수 없도록 한다.
    • 하위클래스에서 객체의 상태를 바꿀 수 있기 때문에. 
    • 클래스를 final로 선언하거나, 생성자를 private으로 바꾸고 public 정적 팩터리 메서드를 제공(아래 5번에서 다시 설명)하여 클래스를 확장할 수 없도록 한다.
  3. 모든 필드를 final로 선언한다.
    • 변경할 수 없는 필드라는 설계자의 의도를 명확히 드러내는 방법이다.
    • 새로 생성된 인스턴스를 동기화없이 다른 스레드로 건네도 문제없이 동작함이 보장된다. 
  4. 모든 필드를 private로 선언한다.
    • 필드가 참조하는 가변객체를 클라이언트에서 직접 수정할 수 없도록 한다.
    • public static 만으로도 불변 객체가 되지만 -> 내부 표현을 바꾸려면 외부 API 변경이 동반되므로 권하지는 않는다. (아이템 15,16)
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    • 객체 내부에서 가변 컴포넌트를 참조하는 필드가 있다면, 클라이언트에서 그 객체를 참조할 수 없도록 해야 한다.
    • 생성자, 접근자, readObject 메서드에서 방어적 복사를 수행해라!

 


3. 불변 객체의 특징

  1. 불변 객체는 단순하다. 
    • 앞서 말한대로 생성된 시점의 상태를 파괴될 때까지 간직한다.
    • 예상치 못한 상황에 놓일 수 있는 가변 객체와 다르게, 프로그래머가 별다른 노력을 들이지 않아도 영원히 불변이다.
  2. thread-safe하여 따로 동기화할 필요 없다.
    • 불변이기 때문에 여러 스레드가 동시에 사용해도 훼손되지 않는다. 때문에 문제없이 공유할 수 있다.
      • 웬만하면 한 번 만든 인스턴스를 재활용하여 메모리 사용량과 가비지 컬렉션 비용을 줄이자.
      • ex) 자주 쓰이는 값은 상수로 제공해라. 어차피 몇 번 계산해도 같은 값이기 때문에 상수로 넣어놓을 수 있다! 
  3. 자유롭게 공유할 수 있고, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
    • ex) BigInteger 클래스 (-> 불변 객체)
      • int 부호
      • int[] 크기
    • negate()메서드는 크기는 같고 부호만 반대는 새로운 BigInteger 인스턴스를 생성하는데,
    • 배열은 가변이지만 복사하지 않고 원본 인스턴스와 공유해도 된다.
    • 실제로 새로 만든 인스턴스도 원본이 가리키는 내부 배열을 그대로 가리킨다.
  4. 객체를 만들 때 다른 불변 객체들을 구성요소로 사용할 수 있다. 
    • 내부가 아무리 복잡해도 많은 불변 객체로 이루어져 있다면 불변식을 유지하기 훨씬 쉬워진다.
    • 맵이나 집합에서 불변객체를 사용하기 좋다. (값이 바뀌면 안되는데 불변 객체는 이를 충족하기 때문에)
  5. 예외가 발생해도 객체가 같은 상태이다. (= 실패 원자성을 제공한다.)
    • 상태가 절대 변하지 않으니 불일치 상태에 빠질 가능성이 아예 없다.

4. 불변 객체의 단점은??

  • 값이 다르다면 반드시 독립된 객체로 만들어야 한다.
    • 값의 가짓수가 많다면 모두 새로 만드는데 많은 비용이 든다.

sol) 가변 동반 클래스 (ex: StringBuilder)

 

 


5. 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 정적 팩터리 메서드를 제공한다.
    • public, protected 생성자가 없기 때문에 다른 패키지에서는 이 클래스를 확장하는게 불가능
  • 바깥에서는 볼 수 없는 구현 클래스를 원하는 만큼 만들어 활용할 수 있다.
    • 유연하다. 다음 릴리스에서 객체 캐싱 기능을 추가해 성능을 끌어올릴수도 있다. 

 


6. 기타 팁들

  • BigInteger, BigDecimal은 하위 객체가 있을 수 있으므로 주의하자.
    • 당시 final로 구현되어야 한다는 사실이 알려져 있지 않았기 때문에, 하위 클래스가 있을 수 있다. 
  • getter가 있다고 무조건 setter를 만들 필요는 없다.
    • 꼭 필요한 경우가 아니라면 불변이어야 하는데 setter로 값을 바꿀 수 있기 때문이다.
  •  불변 클래스가 아니더라도 변경할 수 있는 부분을 최소한으로 줄이자.
    • 변경해야 할 필드를 뺀 나머지 모두를 final로 선언. -> 합당한 이유가 없다면 모든 필드는 private final 
  • 불변식 설정, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
    • 명확한 이유가 없다면 생성자, 정적 팩터리 메서드 외에는 그 어떤 메서드도 public으로 제공해서는 안된다.