Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions item73/item73_추상화 수준에 맞는 예외를 던져라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
### item73 추상화 수준에 맞는 예외를 던져라

- 수행하려는 일과 관련이 없어보이는 예외가 튀어나올 때가 있다. 메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해버릴 때 종종 일어나는 일이다.
- 저수준 예외란 소프트웨어의 구체적인 구현 단계나 하위 계층(DB, 네트워크, 파일 시스템 등)에서 발생하는 예외이다. 쉽게 말해, 비즈니스 로직(무엇을 할지)과 상관없는 기술적인 세부 사항(어떻게 할지)에서 터진 에러들이다.

대표적으로 SQLException, IOException,SocketTimeoutException 등이 있다.

- 저수준 예외가 문제가 되는 더 큰 이유는 내부 구현 방식을 드러내어 윗 레벨 API를 오염시킨다. 다음 릴리스에서 구현 방식을 바꾸면 다른 예외가 튀어나와 기존 클라이언트 프로그램을 깨지게 할 수도 있는 것이다.
- 이 문제를 피하려면 상위 계층에서 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다. 이를 `예외 번역`이라고 한다.

예를 들어, 회원 가입을 하는데 이미 ID가 있어서 DB에서 에러가 난다고 했을 때,

```java
public void join(Member member) {
try {
repository.save(member);
} catch (SQLException e) {
// 1. 저수준 예외(SQLException)를 잡음 -> 서버가 SQLException에 대해서 알게 되어 캡슐화가 깨질 수 있음
// 2. 상황에 맞는 고수준 예외(DuplicateMemberException)로 '번역'해서 던짐
throw new DuplicateMemberException("이미 존재하는 회원입니다.", e);
}
}
```

- 다만 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 `예외 연쇄`를 사용하는 게 좋다. 예외 연쇄란 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다.
예를 들어, 회원 가입을 하는 데 오류가 발생했다고 가정을 했을 때,

```java
public void install() throws InstallException {
try {
startSetup();
} catch (IOException e) {
// 새로운 예외(InstallException)를 만들면서, 원본 예외(e)를 같이 넣어줍니다.
throw new InstallException("설치 중 오류가 발생했습니다.", e);
}
}
```

왜 이렇게 하는 것이냐? -> 그냥 예외가 발생하면 관리자는 어디서 문제가 발생했는지 파악하는데 시간이 걸린다. 그래서 SQL의 문법에 오류가 있는 것인지, ID가 중복된 것인지, 서버에 문제가 있는 것인지 정확히 파악하지 못 한다. 이때, 위처럼 오류의 원본을 함께 실어주면 IOException 즉, 디스크 용량이 부족해서 오류가 생겼다는 것을 단번에 알 수 있게 해준다.

- 고수준 예외의 생성자는 (예외 연쇄용으로 설계된) 상위 클래스의 생성자에 이 '원인'을 건네주어, 최종적으로 Throwable 생성자까지 건제니게 한다.

```java
class HighLevelException extends Exception {
HighLevelException(Thowable cause) {
super(cause);
}
}
```

- 대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있다. 예외 연쇄는 문제 원인을 프로그램에서 접근할 수 있게 해주며, 원인과 고수준 예외의 스택 추적 정보를 잘 통합해준다.

`물론 이 역시 남발하는 것은 좋은 방법이 절대 아니다! 주의하도록 하자.`

- 차선책도 알아보자. 아래 계층에서의 예외를 피할 수 없다면, 상위 계층에서 그 예외를 조용히 처리하여 문제를 API 호출자에까지 전파하지 않는 방법이 있다. 이 경우 발생한 예외는 java.util.logging같은 적절한 로깅 기능을 활용하여 기록해두면 좋다. 그렇게 해두면 클라이언트 코드와 사용자에게 문제를 전파하지 않으면서도 프로그래머가 로그를 분석해 추가 조치를 취할 수 있게 해준다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
### item80 스레드보다는 실행자, 태스크, 스트림을 애용하라

- 자바5에서 `java.util.concurrent` 패키지가 등장했다. 이 패키지는 실행자 프레임워크라고 하는 인터페에스 기반의 유연한 태스크 실행 기능을 담고 있다. 그래서 매우 간단한 코드로 작업 큐같은 기존의 어려웠던 코드를 쉽게 생성할 수 있게 되었다.
- 실행자 프레임워크란 복잡한 스레드 생성과 관리를 개발자가 직접 하지 않고, 프레임워크가 알아서 처리해주는 시스템이다.

```java
//실행자 서비스 생성
ExecutorService exec = Executors.newSingleThreadExcutor();
//실행자에게 태스크 넘기기
exec.execute(runnable);
//실행자 종료(이 작업 실패시 VM이 종료되지 않음)
exec.shutdown();
```

- 실행자 서비스의 기능
1. 특정 태스크가 완료되기를 기다린다.
2. 태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다.
3. 실행자 서비스가 종료하기를 기다린다.
4. 완료된 태스크들의 결과를 차례로 받는다.
5. 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다.
- 큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩토리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.
- 가변 스레드 풀 실행자: `Executors.newCachedThreadPool();`
- 이 실행자는 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다. 하지만, 무거운 프로덕션 서버에는 좋지 못하다.

이 실행자는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임 돼 실행된다. 가용한 스레드가 없다면 새로 하나를 생성한다. 서버가 아주 무겁다면 CPU 사용률이 100%로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더욱 악화시킨다.

- 고정 스레드 풀 실행자: `newFixedThreadPool();`
- 위와 같은 문제가 나타나면 스레드 개수를 고정하거나 완전히 통제할 수 있는 `ThreadPoolExecutor(수동모드)`를 직접 사용하는 편이 좋다. 다만 일반적으로는 수동모드의 사용을 권장하지 않는다.
- 자바7이 되면서 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원하도록 확장됐다.
- `ForkJoinTask`의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다. 이렇게 하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다. 물론 포크-조인에 적합한 형태의 작업이어야 한다.
- 더 많은 실행자 프레임워크의 기능은 `자바 병렬 프로그래밍`을 참고하길 바란다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
### item84 프로그램의 동작을 스레드 스케줄러에 기대지 말라

- 잘 작성된 프로그램이라면 스레드 스케줄링 정책에 좌지우지 돼서는 안 된다. 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램은 다른 플랫폼에 이식하기 어렵다.
- 좋은 프로그램을 작성하는 가장 좋은 방법은 실행 가능한 `스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 하는 것이다. 또 실행 준비가 된 스레드들은 맡은 작업을 완료할 때까지 계속 실행되도록 만들자.` (대기 중인 스레드는 실행 가능하지 않다.)
- 실행 가능한 스레드 수를 적게 유지하는 방법은 각 스레드가 무언가 유용한 작업을 완료한 후에는 다음 일거리가 생길 때까지 대기하도록 하는 것이다. `스레드가 당장 처리해야 할 작업이 없다면 실행돼서는 안 된다.`
- 스레드는 절대 바쁜 대기(busy waiting) 상태가 되면 안 된다.
-> 공유 객체의 상태가 바뀔 때까지 쉬지 않고 검사해서는 안 된다는 뜻이다.
- 예시

```java
public class SlowCountDownLatch {
private int count;

public SlowCountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException(count + " < 0");
this.count = count;
}

public void await() {
while(true) { // 여기서 바쁜 대기 상태가 발생
synchronized(this) {
if (count == 0)
return;
}
}
}

public sychronized void countDown() {
if (count != 0)
count--;
}
}
```

- 하나 이상의 스레드가 필요도 없이 실행 가능한 상태인 시스템은 흔하게 볼 수 있다. 이런 시스템은 성능과 이식성이 떨어질 수 있다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
### item90 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

- 직렬화: 데이터를 전송하기 위해 바이트 형태로 변환하는 기술
- 생성자가 아닌 직렬화를 통해 생성된 객체는 버그와 보안 문제가 일어날 가능성이 커진다. 이 위험을 줄이기 위해 있는 방법이 바로 `직렬화 프록시 패턴`이다.
- 직렬화 프록시 패턴은

1. 바깥 클래스의 핵심 데이터만 들고 있는 중첩 클래스를 설계해 private static으로 선언한다. 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시다.(중첩 클래스의 생성자는 단 하나여야 하고, 바깥 클래스를 매개변수로 받아여 한다.)

```java
public class Period implements Serializable { //바깥 클래스
private final Date start;
private final Date end;

public Period(Date start, Date end) {
if (start.compareTo(end) > 0) throw new IllegalArgumentException();
this.start = start;
this.end = end;
}

private static class SerializationProxy implements Serializable { //중첩 클래스(직렬화 프록시)
private final Date start;
private final Date end;

SerializationProxy(Period p) { //논리적 상태 정밀
this.start = p.start;
this.end = p.end;
}
}
}
```

2. 바깥 클래스에 writeReplace 메서드를 추가한다.

```java
private Object writeReplace() { //자바 직렬화 시스템이 이 메서드를 발견하면, Period 대신 Proxy를 저장한다.
return new SerializationProxy(this);
}
```

3. 보안상의 이유로 readObeject 메서드를 바깥 클래스에 추가한다.

```java
//공격자가 Period를 직접 역직렬화 하려는 것을 막아준다.
private void readObeject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvaildObjectException("프록시가 필요합니다.")
}
```

4. 역직렬화 시 진짜 객체인 Period로 바꾸기 위한 readResolve 메서드를 SerializationProxy 클래스에 추가하면 끝이다.

```java
private Object readResolve() {
return new Period(start, end);
}
```

- 직렬화 프록시 패턴의 한계

1. 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
2. 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다. 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하려 하면 ClassCastException이 발생할 것이다. 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어진 것이 아니기 때문이다.
3. 이 패턴이 강력함과 안전성을 주는 만큼 성능의 저하가 있을 수 있다.