Java/이펙티브 자바

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

PHM 2023. 8. 15. 10:07

핵심 정리 - 불변 클래스

- 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

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

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

          ▷ setter를 제공하지 않는다.

    ㆍ클래스를 확장할 수 없도록 한다

           상속을 할 수 없게 만든다. ( 1. final class 2. private contructor )

    ㆍ모든 필드에 final로 선언한다.

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

           public이면 필드 참조를 할 수도 있기에 우리는 필드 참조를 원치않는다.

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

           getter를 만들어 주지 않거나 방어적인 복사를 해서 사용

public final class Person {
    private final Address address;  // address가 가진 정보들 까지 final이 아니다.

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

    public Address getAddress() {
//        return address;
        Address copyOfAddress = new Address();
        copyOfAddress.setCity(address.getCity());
        copyOfAddress.setZipCode(address.getZipCode());
        copyOfAddress.setStreet(address.getStreet());
        return copyOfAddress;   // Person의 정보가 바뀌지 않게 된다.
    }
}

 

핵심 정리 - 불변 클래스의 장점과 단점

- 함수형 프로그래밍에 적합하다. ( 피연산자에 함수를 적용한 결과를 반환하지만 피연산자가 바뀌지는 않는다. )

- 불변 객체는 단순하다.

- 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.

- 불변 객체는 안심하고 공유할 수 있다. ( 상수, public static final )

- 불변 객체 끼리는 내부 데이터를 공유할 수 있다.

           ex) BigInteger의 negate

- 객체를 만들 때 불변 객체로 구성하면 이점이 많다.

           ex) Set - 컬렉션들은 구성요소들 까지 같아야 같다.

// 불변이라면
Set<Integer> numbers = Set.of(1,2,3);

// 불변이 아니라면
final Set<Point> points = new HashSet<>();
Point firstPoint = new Point(1, 2);
points.add(firstPoint);
firstPoint.x = 10;

- 실패 원자성을 제공한다. ( 아이템 76, p407 )

           예외가 발생하더라도 파라미터나 필드가 바뀌지 않는다.

- 단점) 값이 다르다면 반드시 별도의 객체로 만들어야 한다.

    ㆍ"다단계 연산"을 제공하거나, "가변 동반 클래스"를 제공하여 대처할 수 있다.

           가변동반클래스 ex) String

public static void main(String[] args) {
    String name = "whiteship";
    StringBuilder nameBuilder = new StringBuilder(name);
    nameBuilder.append("phm");
}

 

핵심 정리 - 불변 클래스 만들 때 고려할 것

- 상속을 막을 수 있는 또 다른 방법

    ㆍprivate 또는 package-private 생성자 + 정적 팩토리

    ㆍ확장이 가능하다. 다수의 package-private 구현 클래스를 만들 수 있다.

    ㆍ정적 팩터리를 통해 여러 구현 클래스중 하나를 활용할 수 있는 유연성을 제공하고 객체 캐싱 기능으로 성능을 향상 시킬 수도 있다.

- 재정의가 가능한 클래스는 방어적인 복사를 사용해야 한다.

// BigInteger은 상속을 막아두지 않았기에 방어적인 복사를 사용
public static BigInteger safeInstance(BigInteger val) { 
    return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
}

- 모든 "외부에 공개하는" 필드가 final이어야 한다.

    ㆍ계산 비용이 큰 값은 해당 값이 필요로 할 때 (나중에) 계산하여 final이 아닌 필드에 캐시해서 쓸 수도 있다.

 

완벽 공략 요약

- p105, 새로 생성된 불변 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작 (JLS 17.5)

- p106, readObject 메서드 (아이템 88)에서 방어적 복사를 수행하라.

- p112, 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면... (아이템 88)

- p113, java.util.concurrent 패키지의 CountDownLatch 클래스

 

완벽 공략 - final 과 자바 메모리 모델(JMM)

final을 사용하면 안전하게 초기화할 수 있다.

- JMM과 final을 완벽히 이해하려면 JLS 17.4와 JLS 17.5를 참고하라

- JMM

    ㆍ자바 메모리 모델은 JVM의 메모리 구조가 아니다.

    ㆍ적법한(legal) 프로그램을 실행 규칙

    ㆍ메모리 모델이 허용하는 범위내에서 프로그램을 어떻게 실행하든 구현체(JMM)의 자유이다.

       ( 이 과정에서 실행 순서가 바뀔 수도 있다. )

- 어떤 인스턴스의 final 변수를 초기화 하기 전까지 해당 인스턴스를 참조하는 모든 스레드는 기다려야 한다.(freeze)

class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample f;
    
    public FinalFieldExample() {
        x = 3;
        y = 4;
    }
    
    static void writer() {
         f = new FinalFieldExample();
    }
    
    static void reader() {
        if (f != null) {
            int i = f.x;	// guaranteed to see 3
            int j = f.y;	// could see 0
    	}
    }
}

           반드시 필드의 값이 설정되고 사용되길 바란다면 final 키워드를 추가한다.

 

완벽 공략 - CountDownLatch

java.util.concurrent 패키기

병행(concurrency) 프로그래밍에 유용하게 사용할 수 있는 유틸리티 묶음

- 병행(Concurrency)과 병렬(Parallelism)의 차이

- 병행은 여러 작업을 번갈아 가며 실행해 마치 동시에 여러 작업을 동시에 처리하듯 보이지만, 실제로는 한번에 오직 한 작업만 실행한다. CPU가 한개여도 가능하다.

- 병렬은 여러 작업을 동시에 처리한다. CPU가 여러개 있어야 가능하다.

- 자바의 concurrent 패키지는 병행 애플리케이션에 유용한 다양한 툴을 제공한다.

    ㆍBlockingQueue, Callable, ConcurrentMap, Executor, ExecutorService, Future ...

 

CountDownLatch

다른 여러 스레드로 실행하는 여러 오퍼레이션이 마칠 때까지 기다릴 때 사용할 수 있는 유틸리티

- 초기회 할 때 숫자를 입력하고, await() 메서드를 사용해서 숫자가 0이 될 때까지 기다린다.

- 숫자를 셀 때는 countDown() 메서드를 사용한다.

- 재사용할 수 있는 인스턴스가 아니다. 숫자를 리셋해서 재사용하려면 CyclicBarrier를 사용해야 한다.

- 시작 또는 종료 신호로 사용할 수 있다.