diff --git a/developLog/programming-lanuage/java/effective-java/10/item-75.md b/developLog/programming-lanuage/java/effective-java/10/item-75.md index d99f286..a951980 100644 --- a/developLog/programming-lanuage/java/effective-java/10/item-75.md +++ b/developLog/programming-lanuage/java/effective-java/10/item-75.md @@ -78,8 +78,6 @@ public class CustomIndexOutOfBoundsException extends IndexOutOfBoundsException { -1. - *** #### **추가 설명 및 부연** @@ -265,7 +263,7 @@ public class Example { * 런타임에 발생하며, 반드시 처리하지 않아도 된다. * 프로그램적 접근이 드물지만, `toString`을 통해 디버깅 정보를 제공하는 것이 좋다. -## 🗂️ 정리: 실패 포착의 중요성 +## 📚 정리: 실패 포착의 중요성
diff --git a/developLog/programming-lanuage/java/effective-java/10/item-76.md b/developLog/programming-lanuage/java/effective-java/10/item-76.md index 094a7e6..2607617 100644 --- a/developLog/programming-lanuage/java/effective-java/10/item-76.md +++ b/developLog/programming-lanuage/java/effective-java/10/item-76.md @@ -147,7 +147,7 @@ map.put("key2", 123); // ClassCastException 발생 * 메서드가 실패하더라도 객체 상태가 이전 상태를 유지해야 한다는 것을 **명시적으로 문서화**해야 한다. * 예외가 발생할 경우 객체 상태가 변경될 수밖에 없는 상황이라면, 이를 API 문서에 반드시 기술해야 한다. -## 🗂️ **정리** +## 📚 **정리**
diff --git a/developLog/programming-lanuage/java/effective-java/10/item-77.md b/developLog/programming-lanuage/java/effective-java/10/item-77.md index bf20346..2d15d61 100644 --- a/developLog/programming-lanuage/java/effective-java/10/item-77.md +++ b/developLog/programming-lanuage/java/effective-java/10/item-77.md @@ -109,7 +109,7 @@ public void process() throws IOException { 4. **예외를 처리할 수 없다면 전파하라**: * 예외를 적절히 처리하지 못할 경우, **바깥으로 전파**하여 상위 호출자가 처리하도록 한다. -### 🗂️ **결론** +### 📚 **결론** * 예외는 **문제 상황을 포착하고 적절히 대처하기 위해 존재**한다. * 예외를 무시하거나 잘못 처리하면 프로그램의 신뢰성과 안정성을 저하시킬 위험이 크다. diff --git a/developLog/programming-lanuage/java/effective-java/11/item-78.md b/developLog/programming-lanuage/java/effective-java/11/item-78.md index 3ea0186..065343b 100644 --- a/developLog/programming-lanuage/java/effective-java/11/item-78.md +++ b/developLog/programming-lanuage/java/effective-java/11/item-78.md @@ -239,7 +239,7 @@ public static long generateSerialNumber() { -## 🗂️ 핵심 정리 +## 📚 핵심 정리 > 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야 한다. **Synchronized**와 **volatile**은 자바의 동기화에서 필수적이다. diff --git a/developLog/programming-lanuage/java/effective-java/11/item-79.md b/developLog/programming-lanuage/java/effective-java/11/item-79.md index 2a48783..d37fb97 100644 --- a/developLog/programming-lanuage/java/effective-java/11/item-79.md +++ b/developLog/programming-lanuage/java/effective-java/11/item-79.md @@ -2,3 +2,236 @@ > 아이템 78에서 충분하지 못한 동기화의 피해를 다뤘다면, 이번 아이템에서는 반대 상황을 다룬다. **과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠뜨리 고, 심지어 예측할 수 없는 동작을 낳기도 한다.** +## 1. 동기화 메서드와 외계인 메서드 + +교착상태와 안전 실패를 방지하려면 동기화된 메서드나 동기화 블록 내부에서 클라이언트에 제어를 절대 양도하면 안 된다. 특히 동기화된 영역 내부에서는 다음과 같은 행동을 피해야 한다 + +### 1) 동기화된 영역 내부에서 피해야 할 행동 (외계인 메서드) + +1. **재정의 가능한 메서드 호출 금지** +2. **클라이언트가 제공한 함수 객체 호출 금지** (예: `아이템 24`) + +이러한 메서드들은 `외계인 메서드(alien method)`라고 부른다. 외계인 메서드는 어떤 동작을 수행할지 예측할 수 없고, 통제도 불가능하다. 동기화된 영역에서 외계인 메서드를 호출하면 다음과 같은 문제가 발생할 수 있다 + +### 2) 동기화 된 영역에서 외계인 메서드 호출 시 발생하는 문제 + +1. 예외 발생 + +* 동기화된 영역에서 외계인 메서드를 호출하면 예기치 않은 상태가 발생하거나 예상치 못한 예외가 발생할 수 있다. 이는 프로그램의 안정성을 심각하게 위협할 수 있다. + +2. **교착상태** + +* 교착상태는 두 개 이상의 스레드가 서로의 자원을 기다리며 무한히 멈춰 있는 상태를 말한다. +* 동기화된 영역에서 외계인 메서드를 호출하면, 락을 쥔 상태로 다른 락을 기다리게 될 경우 교착상태에 빠질 위험이 크다. 이는 시스템이 응답하지 않게 되는 치명적인 결과를 초래할 수 있다. + +3. 데이터 손상 + +* 동기화된 데이터에 접근하는 동안 외계인 메서드가 예기치 않게 데이터를 수정하면 일관성이 깨질 수 있다. +* 이는 데이터의 부정확성을 초래하며, 특히 동시성 환경에서 치명적인 버그로 이어질 가능성이 크다. + +외계인 메서드 호출로 인한 문제를 방지하려면 동기화된 영역 내부에서 수행되는 작업을 최소화해야 한다. + +> 동기화 블록은 락을 얻고, 공유 데이터를 검사하거나 수정한 후, 곧바로 락을 해제하는 방식으로 설계되어야 한다. + +*** + +### 3) 잘못된 코드 예제: 외계인 메서드를 호출하는 경우 + +다음은 `집합(Set)`을 감싸는 래퍼 클래스이다. 이 클래스는 관찰자 패턴을 사용하여 집합에 원소가 추가될 때 알림을 보낸다. 이 예제는 잘못된 방식으로 동기화된 영역 내부에서 외계인 메서드를 호출하는 상황을 보여준다. + +```java +public class ObservableSet extends ForwardingSet { + // 관찰자를 저장하는 리스트 + private final List> observers = new ArrayList<>(); + + // 생성자: 기존 Set 객체를 감싼다 + public ObservableSet(Set set) { + super(set); + } + + // 관찰자를 추가하는 메서드 + public void addObserver(SetObserver observer) { + synchronized (observers) { + observers.add(observer); + } + } + + // 관찰자를 제거하는 메서드 + public boolean removeObserver(SetObserver observer) { + synchronized (observers) { + return observers.remove(observer); + } + } + + // 새로운 원소가 추가되었음을 관찰자들에게 알리는 메서드 + private void notifyElementAdded(E element) { + synchronized (observers) { + for (SetObserver observer : observers) { + observer.added(this, element); // 외계인 메서드 호출 + } + } + } + + // Set 인터페이스의 add 메서드를 재정의하여 알림 기능 추가 + @Override + public boolean add(E element) { + boolean added = super.add(element); + if (added) { + notifyElementAdded(element); + } + return added; + } +} + +// 관찰자 인터페이스: 원소가 추가되었을 때 호출될 메서드 정의 +@FunctionalInterface +public interface SetObserver { + void added(ObservableSet set, E element); +} +``` + +**문제 상황** + +다음 코드는 `ObservableSet`의 관찰자를 추가하고, 특정 조건에서 관찰자를 제거한다. 이 과정에서 문제가 발생한다. + +```java +ObservableSet set = new ObservableSet<>(new HashSet<>()); +// 관찰자 추가 +set.addObserver(new SetObserver<>() { + @Override + public void added(ObservableSet s, Integer e) { + System.out.println(e); + if (e == 23) { + s.removeObserver(this); // 외계인 메서드 호출 + } + } +}); + +// 0부터 99까지 추가 +for (int i = 0; i < 100; i++) { + set.add(i); +} +``` + +**실행 결과** + +* `0`부터 `23`까지 출력한 뒤 +* `ConcurrentModificationException`이 발생한다. + +**원인** + +* `notifyElementAdded` 메서드가 관찰자 리스트를 순회하면서 외계인 메서드(`added`)를 호출한다. +* 외계인 메서드는 다시 `removeObserver`를 호출하여 리스트를 수정하려 한다. +* 리스트를 순회 중에 수정했기 때문에 문제가 발생한 것이다. + +*** + +### 개선 방법 1: 동기화 블록 밖으로 외계인 메서드 이동 + +외계인 메서드를 호출하기 전에 관찰자 리스트를 복사하여 동기화 블록 밖에서 순회하도록 수정하면 문제가 해결된다. + +```java +private void notifyElementAdded(E element) { + // 관찰자 리스트 복사 + List> snapshot; + synchronized (observers) { + snapshot = new ArrayList<>(observers); + } + // 복사된 리스트를 사용해 순회하며 메서드 호출 + for (SetObserver observer : snapshot) { + observer.added(this, element); + } +} +``` + +#### 개선된 동작 + +* 관찰자 리스트를 복사한 후 동기화 블록 바깥에서 순회한다. +* **`ConcurrentModificationException` 발생하지 않는다.** + +이 방식은 복사본을 사용하는 만큼 약간의 메모리 오버헤드가 발생할 수 있지만, 안전한 동작을 보장한다. + +**추가 설명** + +복사된 리스트는 동기화와 독립적으로 처리되므로, 관찰자가 호출 중 다른 작업을 수행하더라도 원본 리스트에는 영향을 주지 않는다. 이를 통해 동기화 관련 예외 상황을 완전히 방지할 수 있다. + +*** + +### 개선 방법 2: CopyOnWriteArrayList 사용 + +`CopyOnWriteArrayList`는 리스트 복사를 생략하면서도 안전하게 동작할 수 있도록 설계된 자바의 동시성 컬렉션이다. 수정 작업이 발생할 때마다 새로운 복사본을 생성하여 내부 일관성을 유지한다. 이를 활용하면 동기화 문제를 간단히 해결할 수 있다. + +#### 수정된 코드 + +```java +import java.util.concurrent.CopyOnWriteArrayList; + +public class ObservableSet extends ForwardingSet { + // CopyOnWriteArrayList를 사용해 관찰자 저장 + private final List> observers = new CopyOnWriteArrayList<>(); + + // 생성자: 기존 Set 객체를 감싼다 + public ObservableSet(Set set) { + super(set); + } + + // 관찰자를 추가하는 메서드 + public void addObserver(SetObserver observer) { + observers.add(observer); + } + + // 관찰자를 제거하는 메서드 + public boolean removeObserver(SetObserver observer) { + return observers.remove(observer); + } + + // 새로운 원소가 추가되었음을 관찰자들에게 알리는 메서드 + private void notifyElementAdded(E element) { + for (SetObserver observer : observers) { + observer.added(this, element); // 안전한 호출 + } + } +} +``` + +#### CopyOnWriteArrayList의 장점 + +* **동기화 필요 없음**: 읽기 작업은 동기화 없이 안전하게 수행된다. +* **코드 단순화**: 리스트 복사를 제거하여 코드가 더 간결해진다. +* **안전한 수정**: 수정 작업이 복사본에서 이루어지기 때문에 동기화 문제가 발생하지 않는다. + +#### 추가적인 활용 방안 + +* `CopyOnWriteArrayList`는 다중 스레드 환경에서 주로 읽기 작업이 많고 쓰기 작업이 적을 때 최적의 성능을 발휘한다. +* GUI 이벤트 리스너와 같은 상황에서도 사용될 수 있으며, 이벤트 호출 중 발생할 수 있는 동기화 문제를 방지한다. + +#### 단점 + +* 수정 작업이 빈번한 경우 성능 저하가 발생할 수 있다. +* 메모리 사용량이 증가할 수 있다. + +*** + +### 📚 핵심 정리 + +교착상태와 데이터 손상을 방지하려면 동기화 블록 내부에서 외계인 메서드를 호출하지 말아야 한다. 이를 위해 다음 지침을 따른다: + +1. **동기화 블록 내부 작업 최소화**: + * 락을 얻고 데이터를 검사 및 수정한 뒤, 바로 락을 해제한다. +2. **외계인 메서드는 열린 호출(Open Call)로 처리**: + * 동기화 블록 외부에서 호출되도록 설계한다. +3. **적절한 동시성 도구 사용**: + * `CopyOnWriteArrayList`와 같은 동시성 컬렉션을 적극 활용한다. + +멀티코어 환경에서는 과도한 동기화를 피하는 것이 특히 중요하다. 내부 동기화는 필요할 때만 사용하고, 이를 명확히 문서화해야 한다. 동기화 설계에서 실수는 치명적인 결과를 초래할 수 있으므로, 철저한 검토가 필요하다. + +**부록: CopyOnWriteArrayList와 ArrayList 비교** + +| 특징 | CopyOnWriteArrayList | ArrayList | +| ---------- | -------------------- | ------------- | +| 쓰기 작업 중 동작 | 새로운 복사본 생성 | 기존 리스트 수정 | +| 읽기 작업의 안전성 | 동기화 없이 안전 | 동기화 필요 | +| 성능 | 읽기 작업 많을 때 유리 | 쓰기 작업 많을 때 유리 | +| 메모리 사용량 | 높음 | 낮음 | + +적절한 선택을 통해 동시성 문제를 예방하고 효율적인 코드 작성을 실현할 수 있다.