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
51 changes: 51 additions & 0 deletions ch10/item71.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Item 71

## 필요 없는 검사 예외 사용은 피하라

### 검사 예외란?
> - 어플리케이션 수행 중에 일어날법한 예외를 검사하고 대비하라는 목적으로 사용.<br>
> - 반드시 예외 처리를 해야한다.<br>
> - 컴파일러가 발견해서 컴파일 오류를 발생시킨다.
> - try/catch, throws 방식이 존재
> - 처리를 강요하는 것이기 때문에 과하게 사용한다면 API 사용자에게 부담을 준다.
<br>

### 검사 예외를 피하는 방법
- 빈 Optional을 반환
- 상태 검사 메서드와 Unchecked Exception을 추가
<br>

#### 예시 1, 빈 Optional
```java
public static Optional<Database> create() {

try {
database.create(); // create() 메서드 안에선 throws IOException으로 컴파일 에러 발생 가정.
} catch (IOException e) {
return Optional.empty(); // 빈 옵셔널 반환
}
return Optional.of(Database);
}
```
- 예외가 발생한 부가 정보를 담을 수 없으므로 좋은 방법은 아님
<br>

#### 예시 2, 상태 검사 메서드와 Unchecked Exception
``` java
public static Optional<Database> create() {

if (database.canCreateFile()) { // 예외가 던져질 지 여부를 boolean값으로 반환
database.create();
} else {
throw new CustomDatabaseIOException();
}
}
```
- 여러 스레드가 동시에 접근한다면 canCreateFile와 create 호출부 사이에 외부 요인이 접근 가능.
- 그로 인해 객체의 상태가 변할 수 있다.
<br>

### 결론
1. API호출자가 예외 상황에서 복구할 방법이 없다 => 비검사 예외를 던진다.
2. API호출자가 예외 상황에서 복구할 방법이 있으며, 호출자가 처리하기를 원한다. => optional 반환 고려, 예외가 발생할 수 있음을 암시해야함.
3. API호출자가 예외 상황에서 복구할 방법이 있으며, 예외를 api에서 던진다. => 검사 예외를 던진다.
44 changes: 44 additions & 0 deletions ch10/item72.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Item 72

## 표준 예외를 사용하라
<br>

### 표준 예외란?
> - 자바 API에서 제공하는 예외. <br>
> - 가독성이 높아지고, 사용하기 쉽다. <br>
> - 메모리 사용량도 줄어들고, 클래스를 메모리에 적재하는 시간도 적게 걸린다. <br>
<br>

### 대표적인 표준 예외
- IllegalArgumentException
- 허용하지 않은 값이 인수로 전달되었을 때

- IllegalStateException
- 객체가 메서드를 사용하기에 적절하지 않은 상태일 때

- NullPointerException
- null을 허용하지 않는 메서드가 null을 인자로 받을 때

- IndexOutOfBoundsException
- 인덱스의 범위를 넘어설 때

- ConcurrentModificationException
- 허용되지 않은 동시 수정이 발견되었을 때

- UnsupportedOperationException
- 호출한 메서드가 지원하지 않을 때

<br>

### Exception, RuntimeException, Throwable, Error는 직접 재사용 하지 말 것
- 위 클래스들은 예외 클래스들의 상위 클래스이다. 즉, 구체적인 예외 정보를 알 수 없으므로 안정적으로 테스트할 수 없다.

<br>

### 주의사항

- 상황에 부합한다면 항상 표준 예외를 재사용하자. 단, API 문서를 참고하여 사용하고자 하는 Exception이 이름 뿐만아니라 맥락에도 부합하는지 확인해야한다.

- 더 많은 정보를 제공하고자 한다면 표준 예외를 확장해도 된다. 단, 예외는 직렬화할 수 있다는 것을 감안해야한다.

직렬화에는 많은 부담이 따르므로 이 사실만으로도 커스텀한 예외를 사용해야할 이유가 사라진다.
108 changes: 108 additions & 0 deletions ch11/item83.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Item 83

## 지연 초기화는 신중히 사용하라

### 지연초기화란?
> - 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법.<br>
> - 주로 최적화 용도로 사용.<br>
> - 클래스와 인스턴스 초기화 때 발생 하는 위험한 순환문제를 해결하는 효과도 있음.
<br>

### 지연초기화는 양날의 검이다.
- 지연초기화를 사용한다면 인스턴스 생성 시의 초기화 비용은 줄지만 그 대신 필드에 접근하는 비용은 커진다.
- 지연 초기화가 실제로는 성능을 느려지게 할 수 있다.
> - 초기화가 이뤄지는 비율은 어떠한가.<br>
> - 실제 초기화에 비용이 얼마나 드는가.<br>
> - 초기화된 각 필드를 얼마나 빈번히 호출되는가.
<br>

따라서, 성능을 생각한다면 지연초기화는.
- 해당 클래스의 인스터스 중 그 필드를 사용하는 인스턴스의 비율이 낮고
- 그 필드를 초기화하는 비용이 크다면 사용하자.
- 즉. 초기화 시점 해당 필드 or 클래스가 필요할 때 시키는 것이다!
- 그러나, 성능을 측정해보기 전까지는 어떤 것이 더 나은지 확신하지 못함.

또한, 멀티스레드 환경에서는 지연초기화 하는 필드를 둘 이상의 스레드가 공유한다면
어떠한 형태로든 반드시 동기화를 해야함!

#### 예시 1, 일반적인 초기화 방법
```java
public class TestClass {
private final Member member = createMember();

private Member createMember() {
return new Member("KJJ");
}
}
```
- 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
<br>

#### 예시 2, 인스턴스 필드의 지연초기화
``` java
public class TestClass {
private Member member;

public synchronized Member getMember(){
if(member==null){
member = createMember();
}
return member;
}
private Member createMember() {
return new Member("KJJ");
}
}
```
- 지연 초기화가 초기화 순환성(initialization circularity) 을 깨뜨릴 것 같으면 synchronized 를 단 접근자를 사용하자.
<br>

#### 예시 3, 정적 필드용 지연 초기화 홀더 클래스 관용구
``` java
public class TestClass {

static final private Member member = createMember();

private static Member createMember() {
System.out.println("init");
return new Member("KJJ");
}

public static Member getMember(){
return TestClass.member;
}
}
```
- 해당 방식은 클래스는 클래스가 처음 쓰일 때 비로소 초기화 된다는 특성을 이용한 관용구다.
- 이러한 방식은 getMember 메서드가 필드에 접근하면서 동기화를 전혀 하지않으니 성능이 느려질 거리가 전혀 없다.
<br>

#### 예시 4, 인스턴스 필드 지연 초기화용 이중검사 관용구
``` java
public class TestClass {

private volatile Member member;

private Member createMember() {
return new Member("KJJ");
}

public Member getMember() {
Member result = member;
if(result!=null){
return result;
}
synchronized (this){
if(member == null){
member = createMember();
}
return member;
}
}
}
```
- 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사 관용구를 사용하라.
- 해당 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.
- 필드가 초기화 된 후로는 동기화 하지 않으므로 해당 필드는 반드시 volatile로 선언한다.
- result 지역변수는 필드가 이미 초기화된 상황에서는 이 필드를 딱 한번만 읽도록 보장하는 역할을 한다
<br>
26 changes: 26 additions & 0 deletions ch11/item84.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Item 84

## 프로그램의 동작을 스레드 스케줄러에 기대하지 말라

### 스레드 스케줄러란?
> - 스레드의 실행 시점을 관리하는 역할을 한다.<br>
> - 사용자가 제어할 수 없다.<br>
> - 제한된 자원을 여러 프로세스가 효율적으로 사용하도록 다양한 정책(Policy)을 가지고 할당한다.
<br>

### 당장 처리해야할 작업이 없다면 실행하지 마라
- 스레드 풀 크기를 적절히 설정하고, 작업은 짧게 유지해야 한다.
- 단, 너무 짧으면 오히려 작업 분배에 부담이가서 성능을 떨어뜨릴 수 있다.
- 바쁜 대기(busy waiting) 상태가 되면 안된다
<br>

### Tread.yield에 의존하지 마라
- Tread.yield 사용하여 성능을 개선시킬 수 있다.
- 하지만, 어느정도 개선이 될 수 있지만, 효과가 없을수도 있고 오히려 느려질 수 있다.
- Tread.yield는 테스트할 수단도 없다.
- 애플리케이션 구조를 바꿔 동시에 실행가능한 스레드 수가 적어지도록 조치하는 것이 올바른 방법이다.
<br>

### 스레드 우선 순위에 의존하지 마라
- 스레드 우선순위는 자바에서 이식성이 가장 나쁜 특성에 속한다
- 심각한 응답 불가 문제를 스레드 우선순위로 해결하려는 시도는 진짜 원인을 찾아 수정하기 전까지 같은 무넺가 반복해서 터져 나올 것이다.
69 changes: 69 additions & 0 deletions ch12/item89.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Item 89

## 인스턴스 수를 통제해야한다면 readResolve보다는 열거 타입을 사용하라

### readResolve 란?
> - 인스턴스 통제 목적으로 사용함 .<br>
> - 단 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야한다.<br>
> - 그렇지 않으면 readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 여지가 남는다.
<br>

### transient 란?
-
-
<br>

### 싱글톤을 직렬화하는 경우 문제점
- 클래스에 implements Serializable 을 추가하는 순간 더 이상 싱글턴이 아니게 된다.
- 기본 직렬화를 쓰지 않고, 명시적인 readObject를 제공하더라도 소용없다.
- 어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다.

###

#### 예시 1, 싱글톤 객체
```java
public final class Elvis implements Serializable {
private static final Elvis INSTANCE = new Elvis();

private static Elvis() {
}

public static Elvis getINSTANCE() {
return INSTANCE;
}

private String[] favoriteSongs = {"Sandman", "aquaman"}

public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs);
}

private Object readResolve() {
return INSTANCE;
}
}
```
- 위 예시에서는 favoriteSongs를 transient로 선언할 것이 아니라면 enum타입으로 만드는 것이 좋다.
- 싱글턴이 non-transient 참조필드를 가지고 있으면 이 내용은 readResolve가 실행되기 전에 역직렬화 된다.
- 그렇게 되면 조작된 스트림을 사용하여 해당 참조 필드의 내용이 역직렬화되는 시점에 참조를 훔쳐올 수 있다.
<br>

``` java
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = {"sandman", "aquaman"};

public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs);
}
}
```
- 이렇게 enum 타입으로 하면 된다.
<br>

##결론
- 직렬화가 필요하며 인스턴스 수가 하나임을 보장하고 싶을 때, enum 타입을 가장 먼저 고려해라.
- 만약 enum 타입을 사용하는 것이 불가능하며, 인스턴스 수를 제한해야 한다면, readResolve를 구현해라.
- 또한, 해당 클래스의 모든 필드를 원시타입 또는 임시타입으로 정의되어야 한다.


78 changes: 78 additions & 0 deletions ch12/item90.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Item 90

## 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
- Serializable을 구현하기로 결정한 순간, 언어의 생성자 이외의 방법으로도 인스턴스를 생성할 수 있게 된다.
- 버그 및 보안 문제가 일어날 가능성이 커진다.
- 직렬화 프록시 패턴 (Serialization Proxy Pattern)을 이용하면 이러한 위험을 크게 줄여줄 수 있다.
- <br>

### 직렬화 프록시 패턴이란?
> - 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단해준다.<br>
> - 필드들을 final로 선언해도 되므로 Period 클래스를 진정한 불변으로 만들 수 있다.<br>
> - 역직렬화때 유효성 검사를 하지 않아도 된다.
> - 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다.
<br>

#### 예시 1, 직렬화 프록시 패턴
```java
public final class Period implements Serializable {

private final Date start;
private final Date end;

public Period(Date start, Date end) {
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
}
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}

//자바의 직렬화 시스템이 바깥 클래스의 인스턴스 말고 SerializationProxy의 인스턴스를 반환하게 하는 역할
//이 메서드 덕분에 직렬화 시스템은 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다
//"프록시야 너가 대신 직렬화되라"
private Object writeReplace() {
return new SerializationProxy(this);
}

//불변식을 훼손하고자 하는 시도를 막을 수 있는 메서드
//"Period 인스턴스로 역직렬화를 하려고해? 안돼"
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다");
}

//바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스(Period의 직렬화 프록시)
private static class SerializationProxy implements Serializable {

private static final long serialVersionUID = 234098243823485285L;

private final Date start;
private final Date end;

//생성자는 단 하나여야 하고, 바깥 클래스의 인스턴스를 매개변수로 받고 데이터를 복사해야 함
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}

//역직렬화시 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해줌.
//역직렬화는 불변식을 깨뜨릴 수 있다는 불안함이 있는데,
//이 메서드가 불변식을 깨뜨릴 위험이 적은 정상적인 방법(생성자, 정적 팩터리, 다른 메서드를 사용)으로 역직렬화된 인스턴스를 얻게 한다.
//"역직렬화 하려면 이거로 대신해"
private Object readResolve() {
return new Period(start, end);
}
}
}
```

### 직렬화 프록시 패턴의 한계?
- 클라이언트가 조건없이 확장할 수 있는 클래스에는 적용할 수 없다.<br>
- 객체그래프 순환이 있는 클래스에는 적용할 수 없다.<br>
- 방어적 복사보다 느리다.<br>


## 결론
- 확장할 수 없는 클래스라면 가능한 직렬화 프록시 패턴을 사용하자.