Skip to content

Commit

Permalink
Create 76_가능한_한_실패_원자적으로_만들라_김세윤.md
Browse files Browse the repository at this point in the history
  • Loading branch information
ksy90101 authored Mar 21, 2021
1 parent 6992e22 commit d44213c
Showing 1 changed file with 79 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## 아이템 76. 가능한 한 실패 원자적으로 만들라

## 실패 원자적(failure-atomic)?

- 호출된 메소드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다는 특성

### 어떻게 하면 실패 원자적 특성을 가진 메서드를 만들수 있을까?

- 아주 간단한 방법은 불변 객체([아이템 17](https://github.com/Meet-Coder-Study/book-effective-java/blob/main/4%EC%9E%A5/17_%EB%B3%80%EA%B2%BD_%EA%B0%80%EB%8A%A5%EC%84%B1%EC%9D%84_%EC%B5%9C%EC%86%8C%ED%99%94%ED%95%98%EB%9D%BC_%EA%B9%80%EB%AF%BC%EA%B1%B8.md))로 만드는 것이다.
- 메서드가 실패하면 새로운 객체가 만들어 지지 않을 수 있으나, 기존 객체가 불안정한 상태에 빠질일은 없다.
- 왜냐하면 생성 시점에 고정되어 절대 변하지 않기 때문이다.

### 그럼 가변객체는?

- 가장 흔한 방법은 작업 수행에 앞서 매개변수를 검사하는 것([아이템 49](https://github.com/Meet-Coder-Study/book-effective-java/blob/main/8%EC%9E%A5/49_%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98%EA%B0%80_%EC%9C%A0%ED%9A%A8%ED%95%9C%EC%A7%80_%EA%B2%80%EC%82%AC%ED%95%98%EB%9D%BC_%EC%9C%A0%ED%9A%A8%EC%A0%95.pdf))이다.
- 쉽게 말하면 객체 내부 상태가 변하기 전에 잠재적 예외 가능성을 걸러내라는 의미이다.
- 간단한 Stack 코드를 보자.

```java
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
....
}
```

- 현재 pop 메서드를 실행하기 전에 Stack의 Size가 0이면 예외를 던지는걸 확인할 수 있다.
- 사실 이러한 Validation이 없어도 스택이 비었다면 예외를 던지게 되긴 한다. 그러나 그 이후 모든 작업에 대해 ArrayIndexOutOfBoundException을 던지게 되는 불상사가 발생하게 된다. 즉, 그 객체는 스택이 비어 있을때 호출 한 이후 정상적인 객체의 역할을 못하게 되는 것이다.
- 아울러 ArrayIndexOutOfBoundException은 추상화 수준이 상황에 어울리지 않는다고도 볼 수 있다.(아이템 73)

## 다른 방법은 또 없을까?

### 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞으로 두자.

- 즉, 계산을 수행하보기 전에 인수의 유효성을 검사해볼 수 없을 때 앞서 방식에 덧붙여 쓸수 있는 방식이라고 생각하며 된다.
- 이게 무슨말인가? 예시를 살펴보자.
- TreeMap은 원소들을 어떤 기준에 맞춰서 정렬한다.
- 이때 그 원소는 기준에 따라 비교할 수 있는 타입이어야 한다. 엉뚱한 타입이 들어오면 Tree를 변경하기 전에 ClassCastException을 던지게 될 것이다.

### 객체의 임시 복사본에서 작업을 한 이후 작업이 완료 되면 원래 객체와 교체하자.

- 데이터를 임시 자료구조에 저장해 작업하는게 더 빠르다면 적용하기 좋은 방식이다.
- 예를들어 어떤 정렬 메서드에서는 정렬을 수행하기 전에 입력 리스트의 원소들을 배열로 옮겨 담는다.
- 배열을 사용하면 정렬 알고리즘의 반복문에서 원소들에 빠르게 접근가능하기 때문이다.
- 이에 대한 이점은 성능도 있지만, 정렬에 실패해도 입력 리스트는 변하지 않는 효과를 덤으로 얻게 된다.

### 작업 도중 발생하는 실패를 가로채고 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법

- 주로 (디스크 기반내) 내구성을 보장해야 하는 자료구조에 쓰이는데, 자주 쓰이지는 않는다.

## 위 방법을 쓰면 항상 원자성을 보장할 수 있는가?

- 아쉽게도 항상 달성할 수 있는 것은 아니다.
- 자바는 멀티 스레드 방식으로 동작하기 때문에 두 스레드가 동기화 없이 같은 객체를 동시에 수정하면 일관성이 깨질 확률이 높다.
- 따라서 이럴 경우에는 ConcurrentModificationException을 잡아냈다고 해서, 그 객체에 여전히 사용할 수 있다고 가정하면 안된다.
- 또한, Error는 복구할 수 없으므로 AssertionError에 대해 실패 원자적으로 만들 필요는 없다.

## 그럼 최대한 실패 원자적으로 만들어야 하는가?

- 최대한 만들어야 하지만, 항상 그리 해야 하는 것은 아니라고 생각한다.
- 그 이유는 실패 원자적으로 만들기 위해 비용이나 복잡도가 굉장히 높을 수 있기 때문이다.
- 아울러 문제를 파악하면 실패 원자성을 꽁짜로 얻을수 있는 경우가 있다.
- 그러나 메서드 명세에 기술한 예외라면 혹시 예외가 발생하더라도 객체의 상태는 메서드 호출 전과 똑같은 상태가 유지되어야 한다는 것이 기본 규칙이다. 혹시라도 이 규칙을 못지킨다면 API 문서에 명시해야 한다.
- 그러나 아쉽게도 이 부분이 잘 지켜지지 않는게 현실이다.

## 결론

- 실패 원자적은 호출된 메소드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다는 특성이다.
- 이 특성을 갖기 위한 방법은 아래와 같다.
1. 불변객체
2. 실패할 가능성이 있는 코드를 앞으로 두자
3. 임시 복사본을 만들어 성공하면 원래 객체와 임시 복사본을 교체하자
4. 작업도중 실패하는 코드를 가로채고 복구 코드를 작성하자
- 그러나 원자성을 보장할 수 없을 때가 있다.
1. 두 스레드가 동기화 없이 같은 객체를 동시에 수정하는 경우
2. Error
- 원자성을 최대한 보장해줘야 하지만, 항상 그러한 것은 아니다. 비용이나 복잡성이 높다면 포기하는 것을 추천한다.
- 아울러 그러한 부분은 꼭 API 문서에 적어두자.

0 comments on commit d44213c

Please sign in to comment.