# 아이템79 과도한 동기화는 피하라

과도한 `동기화`는 성능을 떨어뜨리고, `교착상태`에 빠뜨리고, 심지어 예측할 수 없는 동작을 낳기도 한다. `응답 불가`와 `안전 실패`를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다. 예를 들어 동기화된 영역 안에서는 `재정의할 수 있는 메서드`는 호출하면 안 되며, 클라이언트가 넘겨준 `함수 객체`를 호출해서도 안 된다. (아이템 24) 이는 `예외`를 발생 시키거나 `교착상태`에 빠지거나 데이터를 훼손할 수도 있다.

다음은 어떤 집합(`Set`)을 감싼 래퍼 클래스이고, 이 클래스의 클라이언트는 집합에 원소가 추가되면 알림을 받을 수 있다.

```java
public class ObservableSet<E> extends ForwardingSet<E> {

    public ObservableSet(Set<E> set) {
        super(set);
    }

    private final List<SetObserver<E>> observers = new ArrayList<>();

    public void addObserver(SetObserver<E> observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }
    
    public boolean removeObserver(SetObserver<E> observer) {
        synchronized (observers) {
            return observers.remove(observer);
        }
    }
    
    private void notifyElementAdded(E element) {
        synchronized (observers) {
            for(SetObserver<E> observer : observers) {
                observer.added(this, element);
            }
        }
    }
    
    @Override
    public boolean add(E element) {
        boolean added = super.add(element);
        if(added) {
            notifyElementAdded(element);
        }
        return added;
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        boolean result = false;
        for (E element : c) {
            result |= add(element); //notifyElementAdded를 호출
        }
        return result;
    }
}
```

관찰자들은 `addObserver`와 `removeObserver` 메서드를 호출해 구독을 신청하거나 해지한다. 눈으로 보기에 `ObservableSet`은 잘 동작할 것 같다.

다음 프로그램은 0부터 99까지를 출력한다. 평상시에는 앞서와 같이 집합에 추가된 정숫값을 출력하다가, 그 값이 `23`이면 자기 자신을 제거하는 관찰자를 추가해보자.

```java
public static void main(String[] args) {
	ObservableSet<Integer> set = new ObservableSet<>(New HashSet<>());
	
	set.addObserver(new SetObserver<Integer>() {
		public void added(ObservableSet<Integer> s, Integer e) {
			System.out.println(e);
			if (e == 23) s.removeObserver(this);
		}
	});

	for (int i = 0; i < 100; i++) 
		set.add(i);
}
```

이 프로그램은 23까지 출력한 다음 `ConcurrentModificationException`을 던진다. 관찰자의 `added` 메서드 호출이 일어난 시점이 `notifyElementAdded`가 관찰자들의 리스트를 순회하는 도중이기 때문이다. `added` 메서드는 `ObservableSet`의 `removeObserver` 메서드를 호출하고, 이 메서드는 다시 `observers.remove` 메서드를 호출한다. 여기서 문제가 발생한다. 리스트에서 원소를 제거하려 하는데, 마침 지금은 이 리스트를 순회하는 도중이다. **즉 허용하는 동작이다.**

#### 이번엔 다른 예외를 시도해보자.

구독 해지 하는 관찰자를 작성하는데 `removeObserver`를 직접 호출하지 않고 실행자 서비스(`ExecutorService`, 아이템 80)를 사용해 다른 스레드한테 부탁할 것이다.

```java
set.addObserver(new SetObserver<Integer>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e);
        if (e == 23) {
            ExecutorService exec = Executors.newSingleThreadExecutor();
            try {
                exec.submit(() -> s.removeObserver(this)).get();
            } catch (ExecutionException | InterruptedException ex) {
                throw new AssertionError(ex);
            } finally {
                exec.shutdown();
            }
        }
    }
});
```

이 프로그램을 실행하면 예외는 나지 않지만 `교착상태`에 빠진다. 백그라운드 스레드가 `s.removeObserver`를 호출하면 관찰자를 잠그려 시도하지만 `락`을 얻을 수 없다. `메인 스레드`가 이미 락을 쥐고 있기 때문이다. 그와 동시에 `메인 스레드`는 `백그라운드 스레드`가 관찰자를 제거하기만을 기다리는 중이다. **`바로 교착상태다!`**

사실 관찰자가 자신을 `구독 해지` 하는 데 굳이 백그라운드 스레드를 이용할 이유가 없으니 좀 억지스러운 예지만, 실제 시스템에서도 동기화된 영역 안에서 외계인 메서드를 호출하여 교착상태에 빠지는 사례는 자주 있다.

> 외계인 메서드란 동기화된 영역 안에서 재정의 메서드를 호출하거나 클라이언트가 넘겨준 함수 객체를 호출하는 것을 뜻한다

다행히 이런 문제는 대부분 어렵지 않게 해결할 수 있다. `notifyElementAdded` 메서드 에서 관찰자 리스트를 복사해 쓰면 락 없이도 안전하게 순회할 수 있다. 이 방식을 적용하면 앞서의 두 예제에서 `예외 발생`과 `교착상태` 증상이 사라진다.

```java
private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized(observers) {
        snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer : snapshot)
        observer.added(this, element);
}
```

사실 외부 메서드 호출을 동기화 블록 바깥으로 옮기는 더 나은 방법이 있다. 자바의 `동시성 컬렉션 라이브러리`의 `CopyOnWriteArrayList`가 정확히 이 목적으로 특별히 설계된 것이다.

```java
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();

public void addObserver(SetObserver<E> observer) {
		observers.add(observer);
}

public boolean removeObserver(SetObserver<E> observer) {
		return observers.remove(observer);
}

private void notifyElementAdded(E element) {
		for (SetObserver<E> observer : observers)
				observer.added(this, element);
}
```

`동기화 영역` 안에서 호출된다면 그동안 `다른 스레드`는 보호된 자원을 사용하지 못하고 대기 해야만 한다. **기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.**

자바의 `동기화 비용`은 빠르게 낮아져 왔지만, `과도한 동기화`를 피하는 일은 오히려 과거 어느 때보다 중요하다. 과도한 동기화가 초래하는 진짜 비용은 락을 얻는 데 드는 `CPU 시간`이 아니다. 바로 경쟁 하느라 낭비하는 시간, 즉 **병렬로 실행할 기회를 잃고, 모든 코어가 메모리를 일관되게 보기 위한 지연 시간이 진짜 비용이다.**

#### 가변 클래스를 작성 하려거든 다음 두 선택지 중 하나를 따르자.

1. `**동기화`를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 `외부`에서 알아서 동기화하게 하자\*\*
2. **동기화를 `내부`에서 수행해 스레드 안전한 클래스로 만들자. (아이템 82)**

`java.util`은 (이제 구식이 된 `Vector`와 `HashTable`을 제외하고) 첫 번째 방식을 취했고, `java.util.concurrent`는 두 번째 방식을 취했다(아이템 81)

자바도 `초창기`에는 이 지침을 따르지 않은 클래스가 많았다. 예컨대 `StringBuffer` 인스턴스는 거의 항상 단일 스레드에서 사용했음에도 내부적으로 동기화를 수행했다. (뒤늦게 `StringBuilder`가 등장한 이유이기도 하다)

클래스를 내부에서 동기화 하기로 했다면, `락 분할(lock splitting)`, `락 스트라이핑(lock striping)`, `비차단 동시성 제어(nonblocking concurrecy control)` 등 다양한 기법을 동원해 동시성을 높여줄 수 있다.

여러 `스레드`가 호출할 가능성이 있는 메서드가 `정적 필드`를 수정한다면 그 필드를 사용하기 전에 반드시 `동기화`해야 한다. **그런데 클라이언트가 여러 스레드로 `복제`돼 구동되는 상황이라면 다른 클라이언트에서 이 메서드를 호출하는 걸 막을 수 없으니 외부에서 동기화할 방법이 없다.**

### 정리

`교착상태와` `데이터 훼손`을 피하려면 동기화 영역 안에서 `외계인 메서드`를 절대 호출하지 말자. 일반화해 이야기하면, `동기화 영역` 안에서의 작업은 `최소한`으로 줄이자. `가변 클래스`를 설계할 때는 스스로 `동기화`해야 할지 고민하자. 멀티 코어 세상인 지금도 과도한 동기화를 피하는 게 과거 어느 때보다 중요하다. **합당한 이유가 있을 때문 내부에서 동기화하고, 동기화했는지 여부를 문서에 명확히 밝히자**
