Study/Effective-Java

[item#17] 변경 가능성을 최소화하라

hongeeii 2023. 11. 29. 14:59
728x90
반응형

변경 가능성을 최소화하라

불변 클래스 : 인스턴스의 내부 값을 수정할 수 없는 클래스(String, BingInteger, BigDecimal...)

=> 불변클래스가 가지고 있는 정보는 객체가 파괴되는 순간까지 절대 달라지지 않음

=> 설계와 구현, 사용이 쉽고 오류도 적다.

클래스를 불변으로 만드는 규칙

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

    => 하위클래스에서 객체의 상태를 변하게 만드는 사태를 막아줌.(예 filnal)
  3. 모든 필드를 final로 선언

    => 시스템이 강제하는 수단을 이용해 설계자의 의도를 드러냄.

    => 새로 생선된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게 끔 보장하는데도 필요함.(자바 언어 명세 책 참고)
  4. 모든 필드를 private로 선언.

    => 필드가 참조하는 가변 객체를 클라이언드에서 직접 수정하는일을 막음
    => public final 로 해도 불변이지만 이렇게하면 다름 릴리스에서 내부 표현을 바꾸지 못하므로 권장 X
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 함.

    => 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트가 그 객체의 참조를 얻을 수 없도록 해야함.

    => 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행

예시코드

public final class Complex{
  private final double re;
  private final douvle im;
  
  public Complex(double re, double in){
    this.re = re;
    this.im = im;
  }
  
  public double realPart(){return re;}
  public double imaginaryPart(){return im;}
  
  public Complex plus(complex c){
    return new Complex(re + c.re, im + c.im);
  }
  public Complex minus(Complex c){
    return new Complex(re - c.re, im - c.im);
  }
  public Complex timex(Complex c){
    return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
  }
  public Complex dividedBy(Complex c){
    double tmp = c.re * c.re + c.im * c.im;
    return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
  }
  
  @Override
  public boolean equals(Object o){
    if(o == this) return true;
    if(!(o instanceof Complex)) return false;
    Complex c = (Complex) o;
    return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
  }
  @Override
  public int hashCode(){
    return 31 * Double.hashCode(re) + Double.hashCode(im);
  }
  @Override
  public String toString(){
    return "(" + re + " + " + im + ")";
  }
}
  • 위의 사칙연산 메서드를 보면 자신을 변경하지 않고 새로운 인스턴스를 리턴함.

    => 피연산자는 그대로이지만 함수를 피연산자에 적용해 결과를 반환하는 것을 함수형 프로그래밍이라 함
  • 메서드 명도 동사(add) 대신 전치사(plus)를 사용하여 객체의 값이 변경되지 않는다는 사실을 강조함

    불변객체의 장점

  1. 단순하다.
    생성된 시점부터 파괴 시까지 객체를 그대로 간직함. => 믿고 사용할 수 있음.

  2. 스레드에 안전하여 동기화할 필요 없다.
    여러 스레드가 동시에 사용해도 훼손되지 않음. => 안심하고 공유할 수 있음.

    => 한번 만든 인스턴스를 재활용하는 것을 권장.

    자주 사용하는 인스턴스를 캐싱해 정적팩터리로 제공 가능

    => 메모리 사용량과 가비지 컬렉션 비용을 줄임

    => 클라이언트를 수정하지않고 필요에 따라 캐시 기능을 덧불일 수 있음.

    => 어차피 복사해도 똑같으니 방어적 복사도 필요없음.

    => 불변 클래스는 clone 이나복사 생성자를 제공하지 않는게 좋음.

  3. 자유로운 공유와 불변 객체끼리는 내부 데이터를 공유할 수 있다.

    => 예를들어 BigInteger는 부호와 크기를 나눠 관리하고 크기에 int 배열을 사용하는데

    부호만 다른 새로운 객체를 생성할때(negate), 이때 배열은 가변이지만 복사하지 않고 인스턴스를 공유해도된다.

  4. 객체를 만들때 불변 격채들을 구성요소로 사용하면 이점이 많다.

  5. 객체를 만들때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.

    => 구조가 복잡해도 불변식을 유지하지 수월함.

    => 맵의 키와 set의 원소로 쓰기에 알맞다.

  6. 실패 원자성을 제공한다.

    메서드에서 예외가 발생한 후에도 그 객체는 여전히 호출전과 똑같이 유효한 상태여야 한다는 성질

단점

  1. 값이 다르면 반드시 독립된 객체로 만들어야 한다.

    => 백만 비트짜리 BigInteger를 하나의 비트만 다른 인스턴스로 만들어야한다 해도 새로운 인스턴스를 만들어야함.

    => 원하는 객체를 완성하기까지 단계가 많고 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 생김.

    대응책

  • 다단계 연산(multistep operation)을을 예측해 기본기능으로 제공.(가변동반클래스(companion class)사용)

불변 클래스를 만드는 다른 설계 방법

클래스가 불변임을 보장하려면 상속을 못하게 해야함(final 클래스)

=> 더 유연한 방법이 있는데 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법임.

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

밖에서 볼 수 없는 package-private 구현 클래스를 원하는만큼 만들어 활용할 수 있어 훨씬 유연함.

=> 패키지 밖에서 보기에는 사실상 final이나 다름 없음.(다른 패키지에서 확장이 불가능하기 때문)

=> 다수의 구현 클래스를 활용한 유연성을 제공하고 다음 릴리스에서 객체 캐싱 기능을 추가해 성능을 끌어올릴 수 있음

BigInteger 나 BigDecimal은 설계 당시 사실상 final 이어야 한다는 생각이 퍼지지않아 재정의할 수 있게 설계됨.

=> 하위 호환성이 발목으 잡아 지금가지도 문제를 고치치 못함.

=> 신뢰할 수 없는 클라이언트로 부터 이 객체를 인수로 받을 때는 인수로 받은 객체가 진짜 인지 확인해야함.

=> 신뢰할 수 없다고 생각되면 방어적 복사를 사용해야함.

public static BigInteger safeInstance(BigInteger val){
  return val.getClass() == BigInteger.class ? 
          val : new BigInteger(val.toByteArray());
}

불변클래스 완화버전

초입에 말한 불변 클래스 규칙이 과한 감이 있어서 다름과 같이 완화할 수 있음.

=> "어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다."

  • 어떤 클래스는 계산 비용이 큰 값을 처음 사용할 때 계산하여 final이 아닌 필드에 캐시해놓기도 함.

    => 캐시한 값을 재사용하여 비용을 절감.(객체가 불변이기에 가능)

정리

  • getter 가 있다고 해서 무조건 setter 를 만들지는 말자

    => 클래스는 필요한 경우가 아니라면 불변이어야 한다.

    => 단순 값 객체는 불변으로 만들자.

    => 무거운 값 객체도 불변으로 만들 수 있는지 고민하고 성능이 문제라면 가변 동반 클래스를 public 클래스로 제공하자.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.

    => 객체가 가질 수 있는 상태의 수를 줄이면 객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어듬.

    => 꼭 변경이 필요한 필드가 아니면 final로 하자.

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

생성자는 초기화가 완료된 객체를 생성해야한다.

예)java.util.concurrent.CountDownLatch

  1. final과 자바 메모리 모델
  2. CountDownLatch 클래스

final과 자바 메모리 모델

JMM : JVM 메모리 구조 X

적합한 프로그램 실행을 알려주는 규칙.

한 스레드 내에서 작동이 유효한지

https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4 (JSL 17.4)



image

메모리 모델 규정상 스레드마다 위에처럼 실행될지 아래처럼 실행될지 모름

그래서 멀티 스레드에서는 차마 할당되기 전에 참조를 하게 될 수도 있음(이론상)



하지만 final 필드는 할당이 되어야 사용할 수 있음을 보장함

CountDownLatch 클래스

  • 병행(Concurrency)과 병렬(Parallelism)의 차이
    병행은 프로세스를 시분할 하여 빠르게 번갈아 실행해 동시에 실행되는 것처럼 보이는 것.

    병렬은 실제로 동시에 실행되는 것.(멀티코어에서만 가능함)

    image
728x90
반응형