Search

이펙티브 자바 3판

1장. 객체 생성과 파괴

아이템1~7
아이템8. finalizer와 cleaner의 사용을 피하라
자바에서 제공하는 두 가지의 객체소멸자 (느리고 위험하고 불필요하다)
GC가 이미 충분히 그 기능을 해주고 있다.
직접 객체를 소멸시키려면 AutoClosable을 구현하고 try-resources 또는 close()를 호출하자
finalizer와 cleaner는 2차 안전망 같은 것 (GC가 놓친걸 언제가 될지 모르나 처리해줌)
아이템9. try-finally보다는 try-with-resource를 사용하라
try-finally를 중첩하여 사용하게 되면 두번째 예외에 의해 첫번째 예외가 무시돼 디버깅이 어려워질 수 있다
try-with-resource를 사용하면 무시되는 예외는 suppressed가 붙어서 표시된다.
AutoClosable 인터페이스를 구현한 것만 try-with-resource를 사용할 수 있다.
inputstream, outputstream, connection 등이 대표적인 자원을 반환해야 하는 예이다.

2장. 모든 객체의 공통 메서드

아이템10. equals는 일반 규약을 지켜 재정의하라
일반 규약
반사성 : x.equals(x)는 true
대칭성 : x.equals(y)가 true면 y.equals(x)도 true
추이성 : x.equals(y)가 true고 y.equals(z)가 true면 z.equals(x)도 true
일관성 : x.equals(y)는 항상 true거나 false
non-null : x.equals(null)은 항상 false
아이템11. equals를 재정의하려거든 hashCode도 재정의하라
애플리케이션 실행 중엔 객체의 hashCode 메서드는 항상 같은 값을 반환해야함 애플리케이션이 재실행되면 달러져도됨
논리적으로 같은 객체는 같은 hashCode를 반환해야함
객체가 달라도 굳이 다른 값을 반환하지 않아도 되지만 다른 값을 반환하는게 성능에 좋음
핵심필드를 생략하지말 것
hashCode의 생성규칙은 공개하지말 것 (클라이언트가 의존적이게 될 수도 있고 계산 방식이 바뀔 수도 있음)
좋은 hashCode 작성요령
1.
int result; 선언하고 c로 초기화
2.
핵심필드들마다 각 해시코드를 계산
3.
각 result를 31 * result + c 로 합산
아이템12. toString을 항상 재정의하라
toString 규약 : 모든 하위클래스에서 이 메서드를 재정의하라
toString은 그 객체가 가진 주요 정보를 모두 읽기 좋은 형태로 반환하는게 좋다
포맷을 명시하든 아니든 개발자의 의도를 명확히 밝혀야한다
모든 구체 클래스에서 Object의 toString을 재정의하자
아이템13. clone 재정의는 주의해서 진행하라
Cloneable 인터페이스는 복제 가능한 클래스를 명시하며 Object 클래스의 protected인 clone의 동작 방식을 결정한다.
clone 메서드는 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.
super.clone()을 통해 Object의 clone 메서드를 사용하고 이 때 재정의를 하지 않았다면 Object 타입으로 반환한다.
불 필요한 하위 클래스에서의 clone을 막으려면 final 한정자를 사용한다.
배열은 clone 기능을 제대로 사용하는 유일한 예이다.
일반적으로 clone보다 복사 생성자와 복사 팩토리가 더 나은 객체 복사 방식을 제공한다.
아이템14. Comparable을 구현할지 고려하라
compareTo 메서드의 일반규약
1.
이 객체와 주어진 객체의 순서를 비교한다. (이 객체가 작으면 음수, 크면 양수, 같으면 0)
2.
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
3.
x.compareTo(y) > 0 && x.compareTo(z) > 0 이면 x.compareTo(z) > 0
4.
x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
5.
(x.compareTo(y) == 0) == x.equals(y) 은 필수는 아니지만 지키지 않을 시 명시해야줘야함
타입이 다른 객체는 ClassCastException을 던진다
Comparable을 구현한 클래스는 자연적 순서가 존재하며 Arrays.sort로 쉽게 정렬할 수 있다.
equals와 똑같이 반사성, 추이성, 대칭성을 지켜야하며 기존 클라스를 확장할 시 기존 클래스의 인스턴스를 반환하는 뷰 메서드를 만들어야 한다.
compareTo를 구현할 때 관계 연산자 <, >를 사용하는거보단 정적 메서드 compare나 비교자 생성 메서드 Comparator.comparingInt 등을 사용하라

3장. 클래스와 인터페이스

아이템15. 클래스와 멤버의 접근 권한을 최소화하라
정보 은닉의 장점
1.
여러 컴포넌트를 개별적으로 개발할 수 있어 속도를 높인다
2.
각 컴포넌트를 빨리 파악해 디버깅하고 교체하는 부담이 적어 관리 비용을 낮춘다
3.
정보은닉 자체가 성능을 높이진 않지만 성능최적화에 도움이 된다
4.
소프트웨어 재사용성을 높인다
5.
개별 컴포넌트 동작을 검증할 수 있어 큰 시스템을 제작하는 난이도를 낮춘다
각종 접근 제한자
1.
private : 멤버를 선언한 가장 바깥의 클래스에서만 접근 가능
2.
package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근 가능
3.
protected : package-private을 포함하며 이 멤버를 선언한 클래스의 하위 클래스에서도 접근 가능
4.
public : 모든 곳에서 접근 가능
공개 API를 public으로 정하고 그 외 모든 것은 private으로 제한하라 그 후 같은 패키지 내에서 접근해야하는 멤버에 한해서 package-private으로 풀어줘라
public 클래스의 인스턴스가 가변 객체를 참조하거나 final이 아니라면 그 클래스는 인스턴스를 제한할 힘을 잃고 스레드 안전하지 않으므로 public 클래스의 인스턴스는 public이 아니어야 한다
불변 객체인 상수는 public static final로 공개해도 되지만 불변 배열은 private static final로 제한하고 이 필드를 반환하는 메서드를 제공해선 안된다
1.
private 불변 배열을 만들고 public으로 불변 리스트를 추가하는 방법
2.
private 불변 배열을 만들고 public으로 불변 배열의 클론을 반환하는 방법
아이템16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
public 클래스에서는 가변 필드를 노출하게 되면 캡슐화가 제대로 되지않고 변경에 취약해진다
public 클래스에서 필드를 불변으로 만들고 노출시키는건 괜찮아도 안심해선 안된다
package-private이나 private 클래스에선 코드가 깔끔해지기 때문에 필드를 노출하는게 나을 때도 있다
아이템17. 변경 가능성을 최소화하라
불변 객체는 단순하다. 불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다
불변 객체는 여러 스래드가 동시에 사용해도 훼손되지 않기 때문에 스래드 안전하다
불변 클래스의 단점은 값이 다르면 반드시 독립된 객체로 만들어야한다는 점이다
String, 기본 타입의 박싱된 클래스들, BigDecimal, BigInteger는 불변 클래스다
불변 클래스로 만드는 규칙
1.
객체의 상태를 변경하는 메서드를 제공하지 않는다
2.
클래스를 확장할 수 없도록 한다
3.
모든 필드를 final로 선언한다
4.
모든 필드를 private으로 선언한다
5.
자신 외에는 내부에 가변 컴포넌트에 접근할 수 없도록 한다
아이템18. 상속보다는 컴포지션을 사용하라
같은 패키지 안에서의 상속(extend)이나 확장할 목적 또는 문서화가 잘 된 상속은 안전하다 (implement는 해당되지 않음)
기존 클래스가 새로운 클래스의 구성요소로 쓰이는 것을 컴포지션이라고 하고 데코레이터 패턴이라고도 하며 래퍼 클래스라고도 한다
컴포지션 설계 방법
1.
집합 클래스 자신은 전달 메서드만으로 이루어진 재사용이 가능한 전달 클래스를 구현한다.
2.
이러한 전달 클래스는 해당 집합 클래스의 모든 기능을 정의한 인터페이스를 구현하며(예를 들면 Set) 생성자를 통해 집합 클래스로부터 전달 받아 집합 클래스의 인스턴스 변수로 갖는다.
래퍼 클래스는 단점이 거의 없지만 콜백 프레임워크와는 맞지 않는다 (콜백은 자기자신의 참조를 넘기는데 래퍼의 내부 객체는 래퍼의 존재를 모르기 때문)
상속을 하기 전 체크리스트
1.
클래스 A와 B가 is-a 관계인가 (불변식이 깨질 수도 있다)
2.
확장하려는 클래스의 API에 결함이 없는가 (상속은 결함까지 상속한다)
아이템19. 상속을 고려해 설계하고 문서화하라 그러지 않았다면 상속을 금지하라
상속용 클래스는 재정의 할 수 있는 메서드(public과 protected 중 final이 아닌 모든 메서드)들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
@implSpec 어노테이션을 사용하면 자바독이 implementation requirements 형태로 만들어준다.
상속용 클래스를 설계할 때 어떤 메서드를 protected로 공개할지는 직접 하위 클래스를 만들어보는게 유일한 방법이다
상속용 클래스의 생성자는 상위 클래스의 생성자에서 먼저 실행되므로 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해선 안된다
clone과 readObject 또한 생성자와 비슷한 이유로 재정의 가능 메서드를 호출해선 안된다
상속용으로 설계하지 않은 클래스는 상속을 금지하라
상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다
아이템20. 추상 클래스보단 인터페이스를 우선하라
추상 클래스가 정의한 타입을 구현하는 클래스는 추상 클래스의 하위 클래스가 되어야하므로 새로운 타입을 정의하는데 큰 제약을 얻게 된다
인터페이스는 선언한 메서드를 모두 정의하고 일반 규약을 잘 지킨 클래스라면 어떤 클래스를 상속하든 같은 타입으로 취급되므로 기존 클래스에 손쉽게 새로운 인터페이스를 구현해넣을 수 있다
인터페이스는 믹스인 정의에 적합하다 (믹스인 정의란 타입의 주된 기능 외에 선택적 기능을 제공한다고 선언하는 것)
인터페이스는 인스턴스 필드를 가질 수 없고 public이 아닌 정적 멤버를 가질 수 없다(private 정적 메서드는 예외) 또한 내가 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다
인터페이스와 추상 클래스의 장점을 합친 인터페이스+추상 골격 구현 클래스는 템플릿 메서드 패턴이라고 불리며 인터페이스로 타입과 디폴트 메서드를 정의하고 골격 구현 클래스로 나머지 메서드를 정의한다 (골격 구현 클래스의 이름은 관례상 AbstractInterface로 짓는다)
아이템21. 인터페이스는 구현하는 쪽을 생각해 설계하라
자바 8 전까지는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 수 없었다.
자바 8에서부터 기존 인터페이스에 메서드를 추가할 수 있는 디폴트 메서드를 소개했다
하지만 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다
인터페이스를 설계할 땐 세심한 주의를 기울여 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 만들어야한다
아이템22. 인터페이스는 타입을 정의하는 용도로만 사용하라
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다
상수를 정의하기 위한 인터페이스는 안티패턴이다
열거 타입을 사용하거나 해당 클래스나 인터페이스 자체에 추가해야한다.
유틸리티 클래스에 정의할 경우 기본 생성자를 private로 만들어 인스턴스화를 방지해야한다
아이템23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
태그(//)가 달린 클래스는 여러 구현이 한 클래스에 혼합돼 있어서 쓸데 없는 코드로 인한 메모리 낭비, 나쁜 가독성, 오류 가능성, 비효율적이라는 문제를 갖고 있다
클래스 계층구조로 바꾸는 방법
1.
Root가 될 추상 클래스를 정의
2.
동작이 달라지는 메서드는 Root 클래스의 추상 메서드로 선언
3.
동작이 일정한 메서드는 Root 클래스의 일반 메서드로 선언
4.
하위 클래스에서 공통으로 사용하는 데이터 필드는 모두 Root 클래스에 선언
아이템24. 멤버 클래스는 되도록 static으로 만들라
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며 그렇지 않을 경우엔 톱레벨 클래스로 만들어야 한다
중첩 클래스의 종류 : 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스
정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다
비정적 멤버 클래스는 static의 유무차이로 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스, 독립적으로 존재할 수 없다면 비정적 멤버 클래스로 만들어야 한다
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여 정적 멤버 클래스로 만들어라 (비정적 멤버 클래스의 경우 바깥 인스턴스의 참조를 갖고 있기 때문에 시간과 공간을 낭비하고 GC 되지 않으므로 메모리 누수와 직결)
익명 클래스는 멤버와 달리 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다 또한 어디서든 만들 수 있고 비정적 문맥에서는 바깥 클래스의 인스턴스까지 참조할 수 있다 (instanceof 불가능, 인터페이스 구현 불가능, 클래스 상속 불가능)
지역 클래스는 지역변수를 사용할 수 있는 곳 어디든 사용할 수 있고 유효범위 또한 지역범위와 같으며 멤버 클래스처럼 이름이 있어서 반복 사용 가능하고 비정적 문맥에서만 바깥 클래스의 인스턴스를 참조할 수 있고 정적 멤버를 가질 수 없다
생성 방법
1.
메서드 밖에서도 사용해야 하거나 너무 길다면 멤버 클래스(바깥 인스턴스를 참조하면 비정적, 아니면 정적)
2.
한 메서드에서만 쓰이며 생성하는 곳이 한 곳이고 비슷한 타입의 클래스나 인터페이스가 이미 있다면 익명 클래스
3.
아니면 지역 클래스
아이템25. 톱레벨 클래스는 한 파일에 하나만 담으라
하나의 소스 파일(.java)에 여러 클래스를 정의하면 컴파일 단계에서 어떤 클래스를 먼저 정의하냐에 따라 동작이 달라질 수 있다

4장. 제네릭

아이템26. 로 타입은 사용하지 말라
로 타입이란 List<E> 와 같이 제네릭 타입을 지정하지 않은 순수한 Collection을 말한다
로 타입엔 어떤 타입이 들어갈지 모르므로 컴파일 때 발견하지 못하고 런타임 예외가 일어나기 쉽다 그럼에도 로 타입이 남아있는 이유는 호환성 때문이다
? 타입은 와일드카드로 어떤 타입이든 담을 수 있다
로 타입을 비교하는 올바른 방법은 instanceof Set 이고 이후에 형변환 해야한다
Collection<?> 엔 null 이외의 어떤 원소도 넣을 수 없다
아이템27. 비검사 경고를 제거하라
제네릭을 사용하게 되면 비검사 형변환, 비검사 메서드 호출, 비검사 매개변수화 가변인수 타입, 비검사 변환 등의 경고를 마주칠 수 있다
비검사 경고를 제거하는 것은 쉬우며 할 수 있는 한 모든 비검사 경고를 제거하라
만약 경고를 제거할 수 없지만 타입이 안전하다고 생각하면 @SuppressWarnings("unchecked") 를 달아서 경고를 숨겨라
@SuppressWarning 어노테이션은 가능한 한 좁은 범위에 사용하라 그리고 사용할 땐 안전한 근거를 주석으로 남겨라
아이템28. 배열보다는 리스트를 사용하라
배열과 제네릭의 차이는 공변과 불공변이다.
상속관계가 있다면 공변은 Sub[]가 Super[]의 하위타입이 된다. 하지만 List<Sub>는 List<Super>의 하위타입이 아니다.
또 다른 차이는 실체화이다.
Object[] arr = new Long[1]을 선언하고 String을 담는 코드는 컴파일은 되지만 런타임에서 예외가 발생한다
List<Object> list = new ArrayList<Long>()을 선언하고 String을 담는 코드는 컴파일에서부터 실패한다
실체화는 첫번째 예시처럼 런타임에서도 타입을 인지하고 확인하는 것이다.
두번째 예시는 제네릭 타입이 소거되기 때문에 런타임에서는 타입을 인지하고 확인하지 못하는 것이다
E, List<E>, List<String>과 같은 실체화 불가 타입이 있고 E[]처럼 제네릭으로 배열을 만들 수도 없다.
결론적으로 배열은 런타임에는 타입 안전하지만 컴파일에서는 그렇지 않고 제네릭은 반대다.
둘을 섞어쓰다가 에러를 만난다면 배열을 리스트로 대체하라
아이템29. 이왕이면 제네릭 타입으로 만들라
일반 클래스를 제네릭 클래스로 만드는 첫번째 방법
1.
클래스 선언에 타입 매개변수를 추가하라 (Object[] -> E[])
2.
E[]는 컴파일에서 에러가 날 것이다. 이것을 (E[]) new Object[]로 바꿔 형변환하라
3.
2번 방법은 타입 안전하지 않기 때문에 비검사 형변환이 안전한지 직접 확인하고 안전하다면 @SuppressWarnings로 경고를 숨겨라
일반 클래스를 제네릭 클래스로 만드는 두번째 방법
1.
이번엔 Object[]를 E[]로 형변환 하지 않고 원소를 반환할 때마다 바로 (E)로 형변환하고 경고를 없애라
첫번째 방법은 처음에 배열 자체를 형변환하기 때문에 가독성이 좋고 코드도 짧다. 두번째 방법은 원소를 반환할 때마다 형변환을 한다.
첫번째 방법을 선호하긴 하지만 E의 런타임 타입이 Object가 아닐 경우 힙 오염을 일으킨다
배열보다 리스트를 우선하라 했지만 ArrayList도 결국은 기본 타입인 배열로 이루어져 있다
제네릭 타입엔 어떤 타입이든 사용할 수 있지만 기본 타입은 사용할 수 없으므로 래퍼 클래스를 사용해야 한다
상속관계가 있는 타입의 경우 제네릭 타입에 extends를 사용해 형변환 없이 바로 부모 클래스의 메서드를 호출할 수 있다.
아이템30. 이왕이면 제네릭 메서드로 만들라
메서드에서 타입 안전하지 않다면 unchecked 경고가 발생할 것이다. 이럴 땐 윈소 타입을 타입 매개변수로 선언하면 된다.
항등 함수를 사용하면 타입 매개변수로 형변환을 해도 타입 안전하다
재귀적 타입 한정인 <E extends Comparable<E>>는 모든 타입 E는 자신과 비교할 수 있다로 상호 비교하다는 것이다
형변환을 해주어야 한다면 제네릭 메서드로 만드는게 훨씬 편하다
아이템31. 한정적 와일드카드를 사용해 API 유연성을 높이라
매개변수(parameter)는 메서드 선언에 정의한 변수이고 인수(argument)는 실제로 넘어가는 값이다
Number는 Integer의 상위 클래스이지만 Stack<Number>에 Iterable<Integer> 를 넘기면 에러가 난다. 이 때 Stack은 Stack<E>이다.
E의 Iterable이 아닌 E의 하위 클래스의 Iterable을 인수로 넘겨야하기 때문에 <? extends E> 를 사용해야 한다.
Object는 모든 클래스의 상위 클래스이다. Collection<Object>에 Stack<Number>의 원소를 넣으려 하면 에러가 발생한다. 이 때도 Stack은 Stack<E>이다.
Collection에 넣는 메서드의 매개변수를 E의 Collection이 아닌 E의 상위 클래스의 Collection으로 해야하기 때문에 <? super E>를 사용해야 한다.
producer-extends, consumer-super 라고 하는 PECS 원칙을 기억하라
클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API는 뭔가 잘못된 것이다.
메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하여도 된다.
하지만 List<?>에는 null말고 담을 수 없다는 것을 기억하라 다른걸 담고 싶다면 private의 도우미 메서드를 만들고 List<E>로 받으면 이 메서드는 ?가 E인걸 알게 된다
아이템32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수(varargs) 메서드를 호출하면 가변인수를 담을 제네릭 배열을 자동으로 하나 만들어지지만 이 배열은 외부에 노출되어 있어 위험하다.
List<String> ...의 가변인수를 담기위한 Object[]에 List<Integer>를 넣으면 꺼낼 때 형변환 에러가 발생한다. 즉, 제네릭 varargs 배열 매개변수에 값을 넣는 것은 위험하다.
만약 메서드가 제네릭 배열에 아무것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는(외부에서 접근할 수 없)다면 타입 안전하다.
@SafeVarargs는 해당 메서드가 타입 안전함을 보장하는 장치이므로 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달아라. (static, final, private에 가능)
아래의 두 가지를 지키지 않는 제네릭 varargs 메서드는 수정하라
1.
varargs 매개변수 배열에 아무것도 저장하지 않는다.
2.
그 배열을 신뢰할 수 없는 코드에 노출하지 않는다.
아이템33. 타입 안전 이종 컨테이너를 고려하라
Set<E>엔 1개, Map<K, V>엔 2개처럼 매개변수화 할 수 있는 타입의 수는 정해져있지만 DB의 Column처럼 임의 개수의 타입을 가지는 경우도 존재한다.
이런 경우 컨테이너 대신 키를 매개변수화하면 되는데 이 방식을 타입 안전 이종 컨테이너 패턴이라고 한다.
타입 토큰인 Class<?>를 키로 사용하면 되는데 T<?>엔 null말고 담을 수 없다고 알고 있지만 이건 Map이 아닌 키이기 때문에 와일드 카드가 중첩돼있어서 가능하다
Class<?>를 키로 하고 Object를 값으로 Map에 담게되면 키와 값 사이의 타입 링크가 끊어지게 된다
하지만 꺼낼 때 Class.cast(Object)처럼 cast 메서드를 사용하면 동적 형변환을 해주기 때문에 타입 링크를 다시 이어 타입 안전해지게 된다
이럼에도 문제점이 2가지 존재한다
1.
Class 객체를 제네릭이 아니라 로 타입으로 보내면 HashSet<Integer>에 String을 가진 HashSet도 담을 수 있게 되어 타입 불변식이 깨지게 되고 checkedList, checkedSet, checkedMap 등을 사용하면 추적이 가능하다
2.
List<String>과 같은 실체화 불가 아이템은 사용할 수 없다. List<String>과 List<Integer>는 같은 List.class이다. 이 경우엔 슈퍼 타입 토큰과 같은 우회로가 있지만 완벽한 우회로는 존재하지 않는다.
Class<?>는 모든 타입을 다 받기 때문에 비한정적 타입 토큰이다. 원하는 타입만 받고 싶다면 한정적 타입 토큰을 사용하면 된다.
한정적 타입 토큰이란 한정적 타입 매개변수나 한정적 와일드 카드를 사용하여 표현 가능한 타입을 말한다.

5장. 열거 타입과 어노테이션

아이템34. int 상수 대신 열거 타입을 사용하라
정수 열거 패턴은 타입 안전하지 못하고 namespace가 없기 때문에 충돌나기 쉽고 문자열로 출력하기 어렵다.
문자열 열거 패턴은 하드코딩이기 때문에 좋지 않다.
자바에서 제공하는 enun 열거 타입은 하나의 클래스이며 각 상수는 자신의 인스턴스를 만들어 public static final로 공개한다.
enum의 상수가 공개하는 인스턴스는 통제되고 싱글톤이기 때문에 1개씩 밖에 존재하지 않는다.
enum의 장점
1.
컴파일타임 타입 안전성 제공
2.
namespace가 존재해 이름이 같은 상수끼리도 충돌이 발생하지 않음
3.
임의의 메서드나 필드 추가 가능
4.
인터페이스 구현 가능
각 상수의 특징을 특정 데이터와 연결짓고 싶다면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
각 필드는 근본적으로 private final이다
상수별로 메서드를 다르게 동작하게 하고 싶다면 switch 대신 상수별 구현 메서드를 사용하자
1.
열거 타입에 추상메서드 선언
2.
각 상수별 클래스 몸체에 재정의
일부 상수가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자
1.
기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch문이 좋은 선택이 될 수 있다.
아이템35. ordinal 메서드 대신 인스턴스 필드를 사용하라
ordinal은 enum 타입에서 해당 상수가 몇번째 위치인지 반환하는 메서드이다.
상수의 선언 순서를 바꾸게 되면 오동작하게 되며 이미 사용 중인 정수와 같은 값은 추가할 수 없고 값을 중간에 비우는 것도 불가능하다.
해결법은 ordinal 메서드 대신 상수의 인스턴스 필드에 값을 저장하고 사용하는 방법이다.
아이템36. 비트 필드 대신 EnumSet을 사용하라
비트 연산자를 이용해 정수값을 갖는 상수들을 하나의 집합으로 모을 수 있고 이것을 비트 필드라고 한다
집합 연산에서는 좋은 성능을 보이지만 정수값 열거 상수의 모든 단점과 추가로 정수보다 해석하기가 더 어렵다는 문제점을 갖는다
이 때 EnumSet을 사용하면 Set 인터페이스를 구현할 뿐만 아니라 타입 안전하고 비트 필드에 견줄만한 성능을 가진다.
그 이유는 EnumSet 또한 내부에 비트 벡터로 구현되어 있고 산술 연산을 사용해 대량의 비트 처리도 효율적으로 가능하기 때문이다.
아이템37. ordinal 인덱싱 대신 EnumMap을 사용하라
enum과 함께 배열이나 리스트를 사용할 때 ordinal 메서드를 통해 정수값을 인덱스로 사용할 수 있다.
하지만 배열은 제네릭과 호환되지 않아 비검사 형변환을 일으키고 타입 안전하지 못하며 예외를 일으킬 수도 있으며 직관적이지 못하다
EnumMap을 사용하면 해당 배열의 원소를 value로 하고 enum의 값을 key로 사용할 수 있다.
EnumMap 내부에서는 동일하게 배열을 사용하기 때문에 성능은 비등하다
다차원 관계일 경우를 생각해보면 이차원 배열에서 값이 하나가 추가되면 중첩배열의 크기가 많이 늘어나게 되고 실수할 확률도 증가하게 된다.
하지만 EnumMap을 사용하면 EnumMap<..., EnumMap<...>> 처럼 사용하면 된다.
아이템38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
Enum 열거 타입은 확장 불가능하게 설계되어 있다. 확장성을 높이면 고려할 요소가 늘어나 설계와 구현이 더 복잡해지기 때문이다.
이럴 땐 인터페이스를 implements해서 Enum 열거 타입을 확장할 수 있다.
리턴 타입에 <T extends Enum<T> & 인터페이스> 를 적으면 Enum 타입인 동시에 인터페이스의 하위타입이어야 한다는 의미이다.
아이템39. 명명 패턴보다 어노테이션을 사용하라
현재 @Test로 테스트를 지정하는 JUnit은 버전3까지는 메서드 이름 앞에 test를 붙어야만 테스트로 인식했었다.
이 방법의 문제점
1.
실수하기 쉽다
2.
클래스 안의 테스트를 위해 클래스 이름을 Test...로 지어도 의도한대로 동작하지 않는다.
3.
프로그램 요소를 매개변수로 전달할 방법이 없다.
@Test 어노테이션을 붙이면 @RunTests에 의해 테스트들이 시작된다.
@ExceptionTest는 Throwable을 확장한 class의 리터럴을 받아 해당 Exception이 터져야 테스트가 성공하도록 한다
@Repeatable 메타 어노테이션은 해당 어노테이션을 여러 번 사용할 수 있게 해주고 사용하기 위해선 해당 어노테이션의 배열을 갖는 컨테이너 어노테이션이 필요하다.
아이템40. @Override 어노테이션을 일관되게 사용하라
@Override는 메서드에만 달 수 있으며 상위 타입의 메서드를 재정의했음을 의미한다.
Object의 equals를 재정의하려면 매개변수 타입을 Object로 해야한다.
어노테이션을 붙였다면 컴파일 단계에서 잘못된 재정의를 알려준다.
상위 클래스의 메서드를 재정의하는 경우엔 모든 메서드에 어노테이션을 붙이자.
상위 클래스의 추상 메서드를 재정의하는 경우에는 붙이지 않아도 된다.
아이템41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
마커 인터페이스란 아무 메서드를 담고 있지 않고 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스이다.
Serializable이 좋은 예이고 이 인터페이스를 구현한 클래스는 ObjectOutputStream을 통해 직렬화되어 쓸 수 있다는 것을 알려준다.
마커 어노테이션은 적용 대상을 더 정밀하게 지정하지 못하고 타입 오류를 컴파일타임에 찾지 못한다.
하지만 마커 어노테이션은 거대한 어노테이션 시스템의 지원을 받으며 클래스와 인터페이스 외의 요소에 마킹해야 할 때 필요하다.
마킹된 객체를 매개변수로 받는 메서드를 만들 일이 있다면 마커 인터페이스를 사용하고 어노테이션을 적극적으로 사용하는 프레임워크라면 마크 어노테이션을 사용하는 것이 좋다.

6장. 람다와 스트림

아이템42. 익명 클래스보다는 람다를 사용하라
기존 자바에선 익명 클래스를 이용해 함수 객체를 만들고 있었다
하지만 익명 클래스 방식은 코드가 너무 길어져 자바와 맞지 않았다.
자바 8부터 익명 클래스를 대체할 함수 객체인 람다가 추가됐다.
람다는 타입을 명시해야할 때를 제외하곤 타입을 생략해도 컴파일러가 직접 추론해준다.
하지만 람다는 이름도 없고 문서화도 할 수 없다 그래서 코드 자체로 이해가 되지 않거나 3줄이 넘어간다면 람다를 쓰지 말아야 한다.
또한 람다에서의 this는 바깥 인스턴스를 가리키므로 this로 자기 자신을 가리키고 싶다면 익명 클래스를 사용해야 한다.
익명 클래스나 람다의 직렬화는 jvm별로 다를 수 있기 때문에 직렬화는 최대한 지양해야하고 꼭 해야 한다면 private 정적 중첩 클래스의 인스턴스를 사용하라
아이템43. 람다보다는 메서드 참조를 사용하라
람다보단 메서드 참조를 사용하는게 보통 더 짧고 간결하다
Function::identity 를 x -> x 로 쓰는 것처럼 람다가 더 짧고 명확한 경우도 있다.
메서드 참조가 짧다면 메서드 참조를 사용하고 그렇지 않을 때만 람다를 사용하라
메서드 참조 유형
1.
정적
Integer::parseInt
Java
복사
str -> Integer.parseInt(str)
Java
복사
2.
한정적
Instant.now()::isAfter
Java
복사
Instant then = Instant.now() t -> then.isAfter(t)
Java
복사
3.
비한정적
String::toLowerCase
Java
복사
str -> str.toLowerCase()
Java
복사
4.
클래스 생성자
TreeMap<K, V>::new
Java
복사
() -> new TreeMap<K, V>()
Java
복사
5.
배열 생성자
int[]::new
Java
복사
len -> new int[len]
Java
복사
제네릭은 람다식으로 사용할 수 없다.
아이템44. 표준 함수형 인터페이스를 사용하라
자바가 람다를 지원하면서 템플릿 메서드 패턴 대신 함수형 인터페이스를 사용한다.
필요한 용도에 맞는게 있다면 직접 구현하기 보단 java.util.function에 표준 함수형 인터페이스를 사용하라
기본 함수형 인터페이스 종류
1.
UnaryOprator<T> : 인수가 1개로 인수 타입과 반환값이 같음
2.
BinaryOperator<T> : 인수가 2개로 인수 타입과 반환값이 같음
3.
Predicate<T> : 인수를 하나 받아 boolean을 반환
4.
Function<T> : 인수 타입과 반환값이 다름
5.
Supplier<T> : 인수 없이 값을 반환
6.
Consumer<T> : 인수를 하나 받고 반환값이 없음
기본 인터페이스는 기본 타입 int, long, double용으로 변형이 일어나고 접두어에 SrcToResult 혹은 ToResult가 붙는다
접두어에 Bi가 붙으면 인수를 2개씩 받는 기본 인터페이스의 변형이 된다.
Consumer 인터페이스에는 접두어에 ObjInt, ObjLong, ObjDouble를 붙이면 객체참조와 기본 타입의 2가지 인수를 받는 변형이 된다
표준 함수형 인터페이스는 대부분 기본 타입만 지원하므로 박싱된 타입을 사용하지 말자
기본 표준 함수형 인터페이스에 없어서 직접 작성해야 할 때는 아래의 조건을 확인하라
1.
자주 쓰이며 이름 자체가 용도를 명확히 설명해준다
2.
반드시 따라야하는 규약이 있다
3.
유용한 디폴트 메서드를 제공할 수 있다
직접 함수형 인터페이스를 작성한다면 @FunctionalInterface 어노테이션을 꼭 붙이고 그 기능은 아래와 같다
1.
해당 인터페이스가 람다용으로 설계된 것을 알려준다
2.
해당 인터페이스가 추상 메서드를 하나만 갖고 있어야 컴파일되게 해준다
3.
2번으로 인해 유지보수 과정에서 메서드를 추가하지 못하게 막아준다
서로 다른 함수형 인터페이스를 같은 위치에 인수로 받는 메서드들을 다중 정의해서는 안 된다.
아이템45. 스트림은 주의해서 사용하라
스트림은 자바8에 추가된 다량 데이터 처리 작업을 돕는 API이다.
스트림은 데이터 윈소의 유한 혹은 무한 시퀀스를 말하며 스트림 파이프라인은 이 원소들로 표현하는 연산 단계를 표현한다.
스트림의 원소는 어디로부터든 올 수 있고 객체 참조나 int, long, double 같이 기본 타입을 지원한다. (char를 사용할 땐 스트림 지양)
파이프라인은 소스 스트림으로 시작해서 중간 연산과 종단 연산으로 끝난다
스트림 파이프라인은 지연 평가가 이뤄지며 종단 연산이 호출될 때 이뤄진다
parallel 메서드를 이용하면 병렬로 처리할 수 있지만 특정 상황이 아니라면 큰 변화가 없다
코드 블록의 장점
1.
범위 안의 지역변수를 읽고 수정할 수 있지만 람다는 final이나 사실상 final인 변수만 읽을 수 있다.
2.
return, continue, break 를 사용할 수 있고 예외를 던질 수 있다. 람다는 불가능하다.
스트림을 사용해야 하는 곳
1.
원소들이 시퀀스를 일관되게 반환한다
2.
원소들이 시퀀스를 필터링한다
3.
원소들의 시퀀스를 하나의 연산을 사용해 결합한다
4.
원소들의 시퀀스를 컬렉션으로 모은다
5.
원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다
스트림은 과용하면 프로그램이 읽거나 유지보수 하기가 어려워진다
스트림과 반복을 둘 다 해보고 더 나은 쪽을 택하라
아이템46. 스트림에는 부작용 없는 함수를 사용하라
스트림은 계산을 일련의 변환으로 재구성한다.
각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
forEach는 스트림 계산 결과를 보고할 때만 사용하고 계산하는데는 쓰지말자
스트림의 Collector에는 toList, toSet, toMap, toCollection(collectionFactory)가 있다
또 다른 Collector의 메서드로는 groupingBy, partitioningBy, summing, averaging, summarizing, filtering, mapping, flatMapping, collectingAndThen, joining 등이 있다
스트림을 사용할 때는 모든 함수 객체가 부작용이 없어야 한다.
아이템47. 반환 타입으로는 스트림보다 컬렉션이 낫다
원소 시퀀스를 반환하는 타입엔 Collection, List, Set, Itreable 등이 있다.
스트림이 추가되고 나서는 Stream이 Iterable을 확장하지 않아서 윈소 시퀀스를 반환할 타입을 고르는게 복잡해졌다.
공개 API에서 원소 시퀀스를 공개하는 경우에는 Collection이나 그 하위 타입으로 반환하는게 최선이다.
원소 시퀀스가 작다면 ArrayList와 같은 표준 컬렉션에 담아서 반환하고 덩치 큰 원소 시퀀스는 단지 컬렉션을 반환한다는 이유로 메모리에 올리지 말자
반환할 원소 시퀀스가 크지만 간단하게 표현할 수 있다면 AbstractList을 구현해 반환하자
AbstractList를 구현하기 위해선 Iterable용 메서드 외에 contain과 size만 구현하면 된다.
Stream이 Iterable을 지원할 때까진 스트림으로 사용하길 원하는 사용자와 반복으로 처리하길 원하는 사용자를 둘 다 고려해 스트림과 Iterable 중 자연스러운걸로 반환해야한다.
아이템48. 스트림 병렬화는 주의해서 적용하라
초기 자바는 스레드, 동기화, wait/notify를 지원하고 자바 5부터는 concurrent, executor, 자바 7부터는 고성능 병렬 분해 프레임위크 fork-join 그리고 자바 8부터는 스트림의 parallel을 지원했다.
동시성 프로그래밍을 할 때는 안전성(safety)과 응답 가능(liveness) 상태를 유지해야한다.
데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 사용하면 병렬화로 성능 개선을 기대할 수 없다
대체로 데이터 소스가 ArrayList, HashSet, HashMap, ConcurrentHashMap, 배열, int 범위, long 범위일 때 병렬화 효과가 가장 좋은데 그 이유는 데이터를 주 메모리에서 캐시 메모리로 가져오는 시간도 사라질뿐더러 실제 메모리가 떨어져 있지 않고 붙어있어 참조 지역성이 뛰어나기 때문이다.
데이터 소스를 다수 스레드에 분배하기 위해 나누는 작업은 Spliterator가 하는데 Stream이나 Iterable의 spliterator 메서드로 얻어올 수 있다
스트림에서 병렬화에 가장 적합한 종단 연산은 sum, max, min, anyMatch, allMatch 같은 축소 연산이고 가장 적합하지 않은 연산은 collect 같은 가변 축소 연산이다.
실제 작업이 병렬화에 드는 비용을 상쇄하지 못한다면 성능 향상은 미미할 수 있다.
스트림 안의 윈소 수 x 윈소당 수행되는 코드의 줄 수가 최소 수십만은 되어야 성능 향상을 기대할 수 있다
병렬 스트림용 데이터 소스를 사용할 땐 ThreadLocalRandom이나 Random보다는 SplittableRandom을 사용하라
성능이 개선될거라는 확신 없이는 병렬화를 사용하지마라

7장. 메서드

아이템49. 매개변수가 유효한지 검사하라
메서드와 생성자 대부분은 입력 매개변수 값이 특정 조건을 만족하길 바란다
매개변수의 특정 조건은 메서드 몸체가 시작되기 전에 검사해야 한다
오류를 발생한 곳에서 즉시 잡지 못하면 감지하기 어려워지고 발생 지점을 찾기 어려워진다.
1.
메서드 실행 중간에 모호한 예외를 던지며 실패할 수도 있다
2.
메서드가 잘 실행되지만 잘못된 결과를 반환할 수 있다
3.
메서드가 잘 실행됐지만 객체를 잘못된 상태로 만들어놓고 미래의 알 수 없는 시점에 오류를 발생시킬 수 있다
매개변수의 제약을 문서화할 때 발생할 수 있는 예외도 함께 문서화해야한다 이 때 문서화한 예외와 다른 예외가 발생할 땐 예외 번역 관용구를 사용하여 문서화한 예외와 같은 예외를 발생시켜야 한다.
Objects.requireNonNull 을 사용하면 null 검사를 수동으로 하지 않아도 된다
assert를 통해 메서드 맨 위에서 매개변수 유효성을 검증할 수 있다 실패하먼 AssertionError 예외를 던진다
아이템50. 적시에 방어적 복사본을 만들라
자바는 메모리 전체를 하나의 배열로 사용하는 C나 C++보다 안전한 언어이다.
그러나 클라이언트가 불변식을 깨뜨리러고 한다는걸 가정하고 방어적으로 프로그래밍해야한다.
Date를 가지는 Period 객체에 setter가 없다고해도 불변이 아니다. 예를 들어 Period가 아닌 Date의 setYear메서드를 통해 직접 Period의 내부를 수정한다하면 불변이 깨지게 된다.
자바 8부터는 Instant나 LocalDateTime 또는 ZoneDateTime을 사용하면 된다. Date는 낡은 API이므로 사용하지말자
외부 공격으로부터 Period를 보호하려면 생성자에서 받은 가변 매개변수를 각각 방어적 복사해야한다.
매개변수 유효성 검사를 하기 전에 방어적 복사를 한 후 복사본으로 유효성 검사를 하자
유효성 검사를 한 후 복사를 할 때 공격하는 방법을 검사시점/사용시점 공격이라고 한다
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사를 할 때 clone을 사용해서는 안 된다
그러나 생성자가 아닌 접근자 메서드에서는 Period가 가지고 있는 Date객체가 java.util.Date임이 확실하기 때문에 방어적 복사에 clone을 사용해도 된다.
항상 가변 매개변수를 방어적 복사해야하는건 아니다. 매개변수를 넘기는 행위가 객체의 통제권을 넘기는 행위이기 때문에 클라이언트와 상호 신뢰할 수 있을 때나 불변식이 깨지더라도 영향이 오직 클라이언트에게 있을 때는 방어적 복사를 하지 않아도 된다.
아이템51. 메서드 시그니처를 신중히 설계하라
API 설계 요령
1.
메서드 이름을 신중히 짓자 : 이해할 수 있고 같은 패키지에 속한 다른 이름들과 일관되게 짓는게 최우선이다.
2.
편의 메서드를 너무 많이 만들지 말자 : 모든 메서드는 각각 자신의 소임을 다해야 하므로 확신이 서지 않으면 만들지 말자
3.
매개변수 목록은 짧게 유지하자 : 4개 이하가 좋고 같은 타입의 매개변수 여러 개가 연달아 나오는 경우는 특히 해롭다.
매개변수 목록을 짧게 줄이는 방법
1.
여러 매서드로 쪼갠다
2.
매개변수 여러 개를 묶어주는 도우미 클래스를 만든다
3.
빌더 패턴을 매서드 호출에 응용한다
매개변수의 타입은 클래스보단 인터페이스가 더 낫다
boolean보다는 enum 타입이 더 낫다
아이템52. 다중정의는 신중히 사용하라
다중정의 메서드는 어떤 메서드를 호출할지 컴파일타임에 정해진다
재정의한 메서드는 동적으로 선택되고 다중정의한 메서드는 정적으로 선택된다
다중정의가 혼동을 일으키는 상황을 피해야한다
1.
매개변수 수가 같은 다중정의는 만들지 말자
2.
가변인수를 사용하는 메서드는 다중정의를 아예 하지말자
3.
다중정의를 하는 대신 메서드 이름을 다르게 짓자
매개변수 중 하나 이상이 어떠한 방향으로도 형변환되지 않는 것처럼 근본적으로 다르다면 혼란을 주는 원인이 사라진다
함수형 인터페이스는 서로 다르더라도 다중정의할 땐 같은 위치의 인수로 받아선 안된다
명령줄에 -Xlint:overloads 를 사용하면 다중정의를 잡아준다
다중정의의 혼란을 피할 수 없다면 메서드들이 모두 동일하게 동작하게 하라
아이템53. 가변인수는 신중히 사용하라
int... args와 같이 타입 뒤에 ...을 붙이면 0개 이상의 인수를 받을 수 있다
가변인수를 받게 되면 인수 길이와 같은 배열을 만들어 해당 배열에 저장하여 건네준다
인수가 1개 이상이어야 할 때는 int...가 아닌 int의 인수를 1개 받도록 하는게 낫다
성능에서 가변인수가 문제가 될 경우 가변인수를 사용하기 보단 인수의 갯수마다 다중정의를 사용하라
아이템54. Null이 아닌 빈 컬렉션이나 배얼을 반환하라
null을 반환하면 클라이언트는 null을 처리하는 코드를 추가로 작성해야한다
빈 컨테이너를 할당하는데도 비용이 드니 null을 반환하는게 성능상 낫다는 주장도 있다
빈 컨테이너를 할당하는 것은 성능 상 큰 차이가 없고 필요할 때 최적화하면 된다
성능이 걱정된다면 Collections의 emptyList, emptySet, emptyMap 같은 메서드를 이용해 불변 컬렉션을 반환하면 된다
배열의 경우엔 길이가 0인 불변 배열을 미리 선언해두고 반환하면 된다
아이템55. 옵셔널 반환은 신중히 하라
자바8 이전에는 반환할 값이 없는 경우 예외를 던지거나 null을 반환해야했다
자바8에서부터 Optional<T>이 생기며 null이 아닌 T타입 참조를 하나 담거나 아무것도 담지 않을 수 있게 됐다.
Optional은 불변 컬렉션이지만 컬렉션은 아니다
empty(), of(), ofNullable() 메서드 등을 통해 빈 값, 실제 값, null일 수 있는 값을 반환할 수 있다
Optional을 반환하는 메서드에서는 절대 null을 반환하지말자
orElse(), orElseThrow()를 통해 기본값을 반환하거나 지정된 예외를 발생시킬 수 있다
isPresent()를 사용하면 비었는지 true/false로 알 수 있으며 stream을 함께 활용하면 filter(Optional::isPresent)를 통해 값이 있는 Optional만 map할 수 있다
컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너는 옵셔널로 감싸면 안된다. 옵셔널 또한 엄연한 객체이기 때문에 두 번 감싸면 성능에 문제가 있을 수 있다
박싱된 기본 타입을 옵셔널로 감쌀 일이 있다면 OptionalInt 같은 기본 타입을 위한 옵셔널을 사용하라 위와 같은 이유이다.
컬렉션의 키, 값, 원소나 배열의 윈소로 사용하는 것 또한 쓸데없이 복잡해지기 때문에 피하라
값을 반환하지 못할 가능성이 있고 호출할 때마다 반환값이 없을 가능성이 있다면 옵셔널을 고려하라
하지만 옵셔널은 성능 저하가 뒤따르므로 null을 반환하거나 예외를 던지는 편이 나을 수도 있다.
아이템56. 공개된 API 요소에는 항상 문서화 주석을 작성하라
API를 올바르게 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야한다
메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다
상속용으로 설계된 클래스의 메서드가 아니라면 무엇을 하는지 how가 아니라 what을 기술해야 한다
메서드를 호출하기 위한 전제조건과 수행된 후에 만족해야될 사후조건도 모두 나열해야 한다
백그라운드 스레드를 시작하는 등 부작용을 일으키는 메서드 또한 문서화해야 한다
문서화 어노테이션
@param : 모든 매개변수
@return : 반환타입이 void가 아닌 모든 것
@throw : 발생할 수 있는 모든 예외
{@code} : 태그로 감싼 내용을 코드옹 폰트로 렌더링, HTML 태그나 자바독 요소를 무시, 여러 줄이면 <pre> 태그로 감싸기
@implSpec : 해당 메서드와 하위 클래스 사이의 계약을 설명
{@literal} : HTML 태그나 자바독 요소를 무시
문서화 주석은 HTML로 변환된다.
문서화 주석에서 this는 해당 메서드를 소유한 객체이다
문서화 주석의 첫번째 문장은 summary discription이고 한 클래스 혹은 인터페이스 안에서 요약 설명이 똑같은 멤버 혹은 생성자가 둘 이상이면 안 된다
요약 설명의 마침표는 . 또는 공백(스페이스, 탭, 줄바꿈) 또는 다음 문장 시작(소문자 -> 대문자)으로 본다 (@summary 어노테이션을 통해 편하게 작성가능)
자바9부터 검색 기능이 생겼고 {@index}를 통해 색인화가 가능하다
제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다
열거 타입을 문서화할 때는 상수들에도 주석을 달아야 한다
어노테이션 타입을 문서화할 때는 맴버들에도 모두 주석을 달아야 한다
패키지를 설명하는 문서화 주석은 package-info.java 파일에 작성하고 모듈을 설명하는 문서화 주석은 module-info.java 파일에 작성한다
클래스 혹은 정적 메서드가 스레드 안전하든 아니든 스레드 안전 수준을 반드시 API 설명에 포함해야 한다 또한 직렬화 할 수 있다면 직렬화 형태도 포함해야 한다
{@inheritDoc} 을 통해 문서화 주석을 상속할 수 있다
명령줄 인수
Xdoclint : 자바독 문서 린트
html5 : HTML 4.01에서 5를 사용하게 해준다
자바독 유틸리티가 생성한 웹페이지를 참고하면 Best Practice를 확인할 수 있다

8장. 일반적인 프로그래밍 원칙

아이템57. 지역변수의 범위를 최소화하라
지역변수의 유효범위를 최소화하면 코드 가독성과 유지 보수성이 높아지고 오류 발생 가능성은 낮아진다
지역변수의 범위를 줄이는 가장 강력한 방법은 가장 처음 쓰일 때 선언하기이다
거의 모든 지역변수는 선언과 동시에 초기화해야 한다
변수를 초기화하는 동안 예외가 발생할 수 있다면 try 블록 안에서 초기화해야 한다
반복문 안에서 사용한 지역변수를 밖에서도 사용해야 하는게 아니라면 while보단 for을 사용하라
while문은 변수 유효범위가 while문의 범위에 한정되지 않지만 for문은 for문에 한정된다
즉 서로 다른 for문이어도 변수 이름을 같게 사용하는게 문제되지 않는다
지역변수의 범위를 최소화하는 또 다른 방법은 메서드를 작게 유지하고 하나의 기능에 집중하는 것이다
아이템58. 전통적인 for문보다는 for-each문을 사용하라
for문의 반복자와 인덱스 변수는 코드를 지저분하게 할 뿐만 아니라 오류가 생길 가능성이 높아진다
for-each문을 사용할 수 없는 상황
파괴적인 필터링 : 컬렉션을 순회하며 선택된 원소를 제거해야 할 경우
변형 : 리스트나 배열을 순회하며 그 원소의 값 일부 혹은 전체를 교체해야 할 경우
병렬 반복 : 여러 컬렉션을 병렬로 순회해야 할 경우
위 3가지 상황일 때는 일반적인 for문을 사용하되 대부분의 경우에선 for-each문을 사용하라
아이템59. 라이브러리를 익히고 사용하라
무작위 정수를 뽑는 메서드의 문제점 : Math.abs(new Random().nextImt()) % n;
n이 그리 크지않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복된다
n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환된다
지정한 범위 바깥 수가 종종 튀어나올 수 있다
자바 7부터는 Random이 아닌 ThreadLocalRandom을 사용하라
포크-조인 풀이나 병렬 스트림에선 SplittableRandom을 사용하라
표준 라이브러리의 장점
그 코드를 작성한 전문가의 지식과 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다
핵심적인 일과 크게 관련 없는 일을 해결하느라 시간을 허비하지 않아도 된다
따로 노력하지 않아도 성능이 지속해서 개선된다
기능이 점점 많아진다
작성한 코드가 다른 사람들에게 낯익은 코드가 된다
자바는 메이저 릴리즈마다 수많은 기능이 라이브러리에 추가된다
java.lang, java.util, java.io 와 그 하위 패키지랑 친해져라
컬렉션 프레임위크와 스트림 라이브러리, java.util.concurrent 들도 좋다
아이템60. 정확한 답이 필요하다면 float와 double은 피하라
float와 double은 과학과 공학 계산용으로 설계되었으며 이진 부동소수점 연산에 쓰이므로 넓은 범위의 수를 빠르게 정밀한 근사치로 계산하도록 설계되었다
금융 관련 계산엔 BigDecimal, int, long이 더욱 어울린다
하지만 BigDecimal은 기본 타입보다 쓰기 불편하고 훨씬 느리다
int나 long을 사용한다면 값의 크기가 제한되고 소수점을 직접 관리해야한다
아이템61. 박싱된 기본 타입보단 기본 타입을 사용하라
박싱된 기본 타입과 기본 타입의 차이
기본 타입은 값만 가지고 있으나 박싱된 기본 타입은 값과 식별성을 갖는다
기본 타입의 값은 언제나 유효하나 박싱된 기본 타입은 null을 가질 수 있다
기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다
박싱된 기본 타입은 == 연산자를 사용하면 오류가 난다
박싱된 기본 타입과 기본 타입을 혼용한 연산이서는 거의 박싱된 기본 타입이 자동으로 언박싱된다
박싱된 기본 타입을 쓰는 곳
컬렉션의 원소, 키 값으로 쓴다
매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로 쓴다
리플렉션을 통해 메서드를 호출할 때 쓴다
아이템62. 다른 타입이 적절하다면 문자열 사용을 피하라
문자열은 다른 값 타입을 대신하기에 적합하지 않으므로 진짜 문자열일 때만 사용하는 것이 좋다
또한 문자열은 열거 타입이나 혼합 타입을 대신하기에 적합하지 않다
자바2땐 스레드 지역변수를 구현하기 위해 각 스레드의 문자열 키를 사용했지만 문자열은 권한에도 적합하지 않기 때문에 현재는 ThreadLocal이라는 객체가 대신하고 있다
문자열을 잘못 사용하면 번거롭고 덜 유연하고 느리고 오류 가능성도 크다
아이템63. 문자열 연결은 느리니 주의하라
문자열 연결 연산자 +로 문자열 n개를 잇는 시간은 n^2에 비례한다
성능을 포기하고싶지 않다면 String대신 StringBuilder의 append()를 사용하자
아이템64. 객체는 인터페이스를 사용해 참조하라
적합한 인터페이스가 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라
Set 인터페이스를 LinkedHashSet 클래스로 생성한 예시와 같다
인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다
적합한 인터페이스가 없다면 당연히 클래스로 참조해야한다
1.
String이나 BigInteger 같은 값클래스
2.
java.io의 OutputStream과 같이 클래스 기반으로 작성된 프레임워크가 제공하는 객체들
3.
PriorityQueue의 comparator와 같이 인터페이스에는 없는 특별한 메서드를 제공하는 클래스들
적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 클래스 타입을 사용하자
아이템65. 리플렉션보다는 인터페이스를 사용하라
java.lang.reflect 를 사용하면 임의의 클래스에 접근할 수 있다.
Class 객체가 주어지면 Constructor, Method, Field 인스턴스를 가져올 수 있고 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다
또한 이 인스턴스들로 각각 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다
Method.invoke()는 어떤 클래스의 어떤 객체가 가진 어떤 메서드라도 호출할 수 있게 해준다
리플렉션의 단점
1.
컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다
2.
코드가 지저분해지고 장황해진다
3.
성능이 떨어진다
리플렉션은 아주 제한된 형태로만 사용해야 단점을 피하고 이점만 취할 수 있다
리플렉션은 인스턴스 생성에만 쓰고 해당 인스턴스는 인터페이스나 상위 클래스로 참조해서 사용하자
리플렉션으로 Set<String> 생성
1.
Class<? extends Set<String>> clazz = (Class<? extends Set<String>>) Class.forName(클래스이름)
2.
Constructor<? extends Set<String>> cons = clazz.getDeclaredConstructor()
3.
Set<String> set = cons.newInstance()
리플렉션은 런타임에 존재하지 않을 수 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다
아이템66. 네이티브 메서드는 신중히 사용하라
자바 네이티브 메서드(JNI)란 C나 C++ 같이 네이티브 언어로 작성된 메서드이다.
네이티브 메서드의 주요 쓰임
1.
레지스트리 같은 플랫폼 특화 기능을 사용한다
2.
네이티브 코드로 작성된 기존 라이브러리를 사용한다
3.
성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다
자바가 성숙해져가면서 자바9부터는 process API를 통해 OS 프로세스에 접근할 수 있게 됐다
성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 권장되지 않는다.
JVM이 발전해오면서 네이티브 메서드보다 성능이 좋아졌지만 최근 또 다시 역전된 케이스가 많기 때문이다.
네이티브 언어는 안전하지 않으므로 메모리 훼손 오류로부터 안전하지 않다
네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성도 낮다
디버깅도 어렵다
가비지 컬렉터에 의해 자동 회수되지 않고 추적조차되지 않는다
네이티브 코드와 자바 코드를 왔다갔다할 때 비용이 발생한다
네이티브 메서드와 자바 코드 사이 접착 코드를 작성하는 일이 귀찮고 가독성이 떨어진다
아이템67. 최적화는 신중히 하라
최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고 섣불리 진행하면 빠르지도 않고 제대로 동작하지도 않으면서 수정하기는 어려운 소프트웨어를 탄생시킨다
빠른 프로그램보다는 좋은 프로그램을 작성하라
성능을 제한하는 설계를 피하라
완성 후 변경하기 어려운 설계 요소는 컴포넌트끼리 혹은 외부 시스템과의 소통방식이다. 이런 설계 요소들은 완성 후 변경이 어렵거나 불가능하고 성능을 심각하게 제한할 수 있다
API를 설계할 때 성능에 주는 영향을 고려하라
상속 방식으로 설계하거나 인터페이스가 있음에도 특정 구현 클래스에 종속되게 설계한다면 성능 제약까지도 물려받게 될뿐만 아니라 더 빠른 구현체가 나오더라도 이용하지 못하게 된다
최소화를 하기 위해선 각각의 최적화 시도 전후로 성능을 측정하라
시간을 많이 잡아먹는 부분을 정확하게 측정하기 위해선 프로파일링 도구를 사용하라
아이템68. 일반적으로 통용되는 명명규칙을 따르라
자바의 명명규칙은 크게 철자 규칙과 문법 규칙으로 나뉜다
철자 규칙
1.
패키지와 모듈의 이름은 인터넷 도메인 이름을 역순으로 뒤집어 각 요소를 점으로 구분하여 계층적으로 짓고 요소들의 이름은 모두 소문자 알파벳 혹은 숫자로 한다
2.
패키지 이름의 나머지는 8자 이하의 한 단어 혹은 약어로 한다
3.
클래스와 인터페이스의 이름은 하나 이상의 단어로 각 단어는 대문자로 시작하며 널리 통용되는 줄임말이 아닌 경우 줄임말을 쓰지 않도록 한다
4.
메서드와 필드 이름은 첫글자를 소문자로 쓴다
5.
상수 필드는 모두 대문자로 각 단어는 밑줄로 구분한다
6.
지역변수엔 약어를 써도 좋지만 입력 매개변수는 문서화되는만큼 신경써야한드
7.
타입 매개변수는 주로 임의의 타입엔 T, 컬렉션 윈소의 타입엔 E, 맵의 키와 값엔 K와 V, 예외엔 X, 반환 타입엔 R, 그 외 임의의 시퀀스 타입엔 T, U, V 혹은 T1, T2, T3를 사용한다
문법 규칙
1.
객체를 생성할 수 있는 클래스의 이름은 보통 단수 명사나 명사구를 사용한다
2.
객체를 생성할 수 없는 클래스의 이름은 보통 복수형 명사를 사용한다
3.
인터페이스의 이름은 보통 클래스와 똑같이 짓거나 able 혹은 ible로 끝나는 형용사를 사용한다
4.
어노테이션은 활용에 따라 자유롭게 사용한다
5.
어떤 동작을 수행하는 메서드의 이름은 목적어를 포함한 동사나 동사구를 사용한다
6.
boolean을 반환하는 메서드는 is나 has로 시작하고 명사나 명사구 혹은 형용사로 기능하는 아무 단어를 사용한다
7.
해당 인스턴스의 속성을 반환하는 메서드는 get으로 시작하는 동사구를 사용한다
8.
객체의 타입을 바꿔서 다른 타입의 객체를 반환하는 메서드는 toType 형태를 사용한다
9.
객체의 내용을 다른 뷰로 보여주는 메서드는 asType 형태를 사용한다
10.
객체의 값을 기본 타입 값으로 반환하는 메서드는 typeValue 형태를 사용한다
11.
정적 팩터리의 이름은 from, of, valueOf, instance, getInstance, newInstance, getType, newType을 흔히 사용한다
오랫동안 따라온 규칙과 충돌한다면 규칙을 맹종하지말고 상식이 이끄는대로 하라

9장. 예외

아이템69. 예외는 진짜 예외 상황에만 사용하라
예외는 예외 상황에 쓸 용도로 설계되었으므로 JVM 개발자는 예외를 명확한 검사보다 최적화 할 의무가 약하다
코드를 try-catch 블록에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다
배열을 순회하는 표준 관용구는 중복 검사를 시행하지 않고 JVM이 최적화해 없애준다
예외는 예외 상황에서만 사용해야 하고 일반적인 제어 흐름용으로 쓰여선 안 된다
잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다
상태 검사 방법엔 상태 검사 메서드, 빈 옵셔널, null 같은 특수한 값이 있다
외부 동기화 없이 여러 스레드가 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다
성능이 중요한 상황에서 상태 검사 메서드가 중복 작업을 수행한다면 옵셔널이나 특정 값을 사용한다
그 외 대부분의 경우엔 상태 검사 메서드가 낫다
아이템70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
예외에는 검사 예외와 비검사 예외로 나뉘고 비검사 예외에선 런타임 예외와 에러로 나뉜다
호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용하라
검사 예외란 메서드 선언부에 붙어있는 throw를 말한다
복구될 수 있는 프로그래밍 오류를 나타낼 땐 런타임 예외를 사용하라
에러는 보통 JVM의 자원 부족, 불변식 깨짐 등 더 이상 수행을 계속할 수 없는 상황일 때 사용한다
Error 클래스를 상속하여 하위 클래스를 만드는 일은 자제하라
비검사 throwable은 모두 RuntimeException의 하위 클래스여야한다
오류 메시지를 파싱하는 행위는 환경에 따라 동작하지 않을 수 있으므로 호출자가 예외 상황을 벗어나는데 필요한 정보를 알려주는 메서드를 함께 제공하라
아이템71. 필요없는 검사 예외 사용은 피하라
검사 예외는 발생한 문제를 프로그래머가 직접 처리하여 안전성을 높이지만 과하게 사용하면 쓰기 불편한 API가 된다
API를 제대로 사용해도 발생할 수 있는 예외거나 프로그래머가 의미있는 조치를 취할 수 있는 경우가 아니라면 비검사 예외를 사용하라
검사 예외를 회피하기 위해선 적절한 결과 타입을 담은 빈 옵셔널을 반환하라
또 다른 방법으론 메서드를 예외가 던질지 결정하는 boolean 메서드와 원래 메서드로 쪼개서 비검사 예외로 바꿀 수 있다
아이템72. 표준 예외를 사용하라
예외도 재사용하는 것이 좋다
API를 사용하는 프로그램에서 낯선 예외를 사용하지 않아 읽기 쉬워지고 메모리 사용량과 클래스를 적재하는 시간도 줄어든다
Exception, RuntimeException, Throwable, Error는 추상 클래스라고 생각하고 직접 재사용하지 말아야 한다
상황에 부합한다면 항상 표준 예외를 사용하라
아이템73. 추상화 수준에 맞는 예외를 던져라
메서드가 저수준 예외를 처리하지 않고 바깥으로 전파하면 내부 구현 방식을 드러내어 윗 레벨 API를 오염시킨다
상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 하고 이를 예외 번역이라 한다
저수준 예외가 디버깅에 도움이 된다면 예외 연쇄를 사용해 저수준 예외를 cause에 실어 고수준 예외로 보내라
무턱대고 예외를 전파하는 것보다 예외 번역이 우수한 방법이지만 남용해선 안된다
아래 계층에서 예외가 발생하지 않는게 베스트고 차선책은 상위 계층에서 처리하여 API 호출자에게까지 전달되지 않는 것이다
아이템74. 메서드가 던지는 모든 예외를 문서화하라
검사 예외는 항상 따로따로 선언하고 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 문서화하라
비검사 예외도 문서화하는 것이 오류가 나지 않도록 코딩하는데에 도움된다. 단 비검사 예외는 throws 목록에 넣지 않는다
예외를 문서화하는 것은 인터페이스에서 특히 중요하다
한 클래스에 정의된 많은 메서드가 같은 이유로 예외를 던진다면 그 예외를 각 메서드가 아닌 클래스 설명에 추가하는 방법도 있다
아이템75. 예외의 상세 메시지에 실패 관련 정보를 담으라
사후 분석을 위해 실패한 순간에 상황을 정확히 포착해 예외의 상세 메시지에 담아야 한다
포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다
예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동해서는 안 된다
최종 사용자에게는 친절한 메시지를 보여주고 예외 메시지에는 가독성보다는 스텍트레이스와 같이 담긴 내용이 중요하다
상세 정보를 알려주는 접근자 메서드를 제공하는 것이 좋다
아이템76. 가능한 실패 원자적으로 만들라
호출된 메서드가 실패하더라도 해당 객체는 호출 전 상태를 유지해야 하고 이러한 특성을 실패 원자적이라 한다
메서드를 실패 원자적으로 만들기 위해선 불변 객체로 설계하는 것이다
가변 객체에선 작업 수행에 앞서 매개변수의 유효성을 검사하는 것이다
추가로 객체의 복사본에서 작업을 수행하는 방법도 있다
또한 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법도 있다
아이템77. 예외를 무시하지 말라
예외를 무시하는건 아주 위험한 행동이지만 무시해야 한다면 catch 블록 안에 무시한 이유에 대한 주석을 남기고 예외 변수의 이름도 ignored로 바꿔놓아야 한다

10장. 동시성

아이템78. 공유 중인 가변 데이터는 동기화해 사용하라
synchronized 한정자는 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장한다
동기화는 객체를 일관성있게 수정하는 것도 중요하지만 한 스레드가 수정한 값이 다른 스레드에도 보이는가를 보장하는 것도 중요하다
동기화는 배타적 실행뿐만 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다
Thread.stop()은 사용하지마라
쓰기와 읽기가 모두 동기화되지 않으면 동작을 보장하지 않는다
volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 보장한다
java.util.concurrent.atomic 패키지에는 AtomicLong과 같이 스레드 안전한 클래스들을 지원한다
가변 데이터는 단일 스레드에서만 사용하고 가변 데이터를 여러 스레드가 공유한다면 읽고 쓰는 동작은 반드시 동기화돼야한다
아이템79. 과도한 동기화는 피하라
과도한 동기화는 성능을 떨어뜨리고 교착상태에 빠뜨리고 심지어 예측할 수 없는 동작을 낳는다
응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블럭 안에서는 제어를 절대로 클라이언트에 양도하면 안된다
동기화 블록 안에 있는 메서드는 동시 수정이 일어나지 않도록 보장하지만 정작 자신이 콜백을 거쳐 되돌아와 수정하는 것까진 막지 못한다
자바의 락은 재진입을 허용하는데 재진입 가능 락은 객체 지향 멀티스레드 프로그램을 쉽게 구현할 수 있도록 해주지만 교착상태가 될 상황을 데이터 훼손으로 변모시킬 수 있다
외계인 메서드를 동기화 블록 밖으로 옮면 예외 발생과 교착상태 증상이 사라진다
외계인 메서드를 동기화 블록 밖으로 옮기는 방법으론 동시성 컬렉션 라이브러리의 CopyOnWriteArrayList로 항상 깨끗한 복사본을 만들어 수행하는 방법이 있다
동기화 블록의 기본 규칙은 블록 안에서는 가능한 일을 적게 하는 것이다
동기화 비용은 락을 얻는데 드는 CPU 시간이 아닌 병렬로 실행 기회를 잃고 모든 코어가 메모리를 일관되게 보기 위한 지연시간과 가상머신의 코드 최적화를 제한하는 것이다
가변 클래스를 작성할 때
1.
동기화를 전혀 하지말고 동시에 사용하는 클래스가 외부에서 동기화하도록 하자
2.
동기화를 내부에서 수행하여 스레드 안전한 클래스로 만들자
동기화 여부에 대한 내용을 문서화하라
아이템80. 스레드보다는 실행자, 태스크, 스트림을 애용하라
실행자 사용 방법 ExecutorService exec = Executors.newSingleThreadExecutor();
실행자에 태스크를 넘기는 방법 exec.execute(runnable);
실행자를 종료하는 방법(종료에 실패해도 VM이 종료되지 않음) exec.shutdown();
실행자의 주요 기능
1.
특정 태스크가 완료되기를 기다린다(get)
2.
태스크 모음 중 아무것 하나(invokeAny) 혹은 모든 태스크(invokeAll)가 완료되기를 기다린다
3.
실행자 서비스가 종료되기를 기다린다(awaitTermination)
4.
완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService)
5.
태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(ScheduledThreadPoolExecutor)
실행자 서비스(스레드 풀)들은 java.util.concurrent.Executors 의 정적 팩터리들을 이용해 만들 수 있다
1.
작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool이 좋다 (태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행, 가용할 스레드가 없다면 새로 생성)
2.
무거운 프로덕션 서버라면 스레드 개수를 고정한 Executors.newFixedThreadPool이나 완전히 통제할 수 있는 ThreadPoolExecutors가 좋다
작업 큐를 손수 만드는 일이나 스레드를 직접 다루는 일을 삼가야한다
스레드를 직접 다루면 작업 단위와 수행 매커니즘 역할을 모두 수행하는 반면 실행자 프레임워크에서는 분리된다
태스크는 Runnable과 Callable(Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다) 두 가지가 있다
실행자 프레임워크는 작업 수행을 담당한다
자바7부터 실행자 프레임워크는 포크-조인 태스크를 지원한다
ForkJoinTask는 작은 하위의 태스크로 나뉠 수 있고 ForkJoinPool을 구성하는 스레드들이 태스크를 처리하며 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 처리한다
포크-조인 풀을 사용하려면 적합한 형태의 작업이어야 하고 병렬 스트림을 사용한다
아이템81. wait과 notify보다는 동시성 유틸리티를 애용하라
wait과 notify는 올바르게 사용하기가 까다로우니 고수준의 동시성 유틸리티를 사용하자
java.util.concurrent의 고수준 유틸리티는 크게 실행자 프레임워크, 동시성 컬렉션(concurrent collection), 동기화 장치(synchronized) 3가지이다.
동시성 컬렉션은 List, Queue, Map 등에 동시성을 가미해 구현한 고성능 컬렉션으로 동기화를 내부에서 수행하여 동시성을 무력화하는 것은 불가능하며 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다
동시성 컬렉션에서 동시성을 무력화하지 못하므로 여러 메서드를 원자적으로 묶어 호출하는 일도 불가능하기 때문에 Map의 putIfAbsent 같은 상태 의존적 수정 메서드를 사용한다
이제는 Collections.synchronizedMap보다는 ConcurrentHashMap을 사용하는 것이 훨씬 빠르다
대부분의 실행자 서비스 구현체는 BlockingQueue를 사용하는데 이 큐는 생산자 스레드가 작업을 큐에 추가하고 소비자 스레드가 큐에 있는 작업을 처리하여 작업 큐에 적합하기 때문이다.
컬렉션 인커페이스 중 일부는 작업이 성공적으로 완료될 때까지 기다리도록 확장되어 BlockingQueue에 적합하다
동기화 장치는 스레드가 서로 다른 스레드를 기다릴 수 있게 하여 서로 작업을 조율하게 해준다.
동기화 장치의 종류로는 CountDownLatch와 Semaphore가 있고 그 외 CyclicBarrier와 Exchanger는 가장 덜 쓰이며 Phaser가 가장 강력하다
CountDownLatch는 일회성 장벽으로 하나 이상의 스레드가 다른 하나 이상의 스레드가 작업이 끝날 때까지 기다리게 한다
CountDownLatch의 생성자는 int를 받으며 이 값이 countDown() 메서드를 몇 번 호출해야 대기 중인 스레드들을 깨우는지 결정한다
동기화 장치에 넘겨진 실행자는 concurrency 매개변수로 지정된 동시성 수준만큼의 스레드를 생성할 수 있어야하고 그렇지 않으면 스레드 기아 교착상태에 빠진다
시간을 잴 때는 System.currentTimeMillis가 아닌 System.nanoTime을 사용하고 정밀한 시간측정은 JMH같은 라이브러리를 사용하자
wait은 스레드가 어떤 조건이 충족되기를 기다리게 할 때 사용한다
락 객체의 wait 메서드는 반드시 그 객체의 동기화 블록에서 호출되어야 한다
wait 메서드를 사용할 때는 반드시 wait loop 반복문을 사용하고 반복문 밖에서는 절대로 호출하지 말자 이 반복문은 wait 호출 전, 후에 조건이 만족하는지 검사해 응답 불가 상태, 안전 실패를 막는다
조건이 만족되지 않아도 스레드가 깨어날 수 있는 상황이 몇가지 있다
1.
스레드가 notify를 호출한 다음 대기 중이던 스레드가 깨어나는 사이에 다른 스레드가 락을 얻어 그 락이 보호하는 상태를 변경한다
2.
조건이 만족되지 않았음에도 다른 스레드가 실수로 혹은 악의적으로 notify를 호출한다. 외부에 노출된 객체는 모두 이 문제에 영향을 받는다
3.
깨우는 스레드는 지나치게 관대해서 대기 중인 스레드 중 일부만 조건이 충족되어도 notifyAll을 통해 모두 깨울 수 있다
4.
대기 중인 스레드가 드물게 notify 없이도 깨어날 수 있고 이는 허위 각성이라는 현상이다
notify보단 notifyAll을 사용하면 관련 없는 스레드가 실수 혹은 악의적으로 wait을 호출하는 공격으로부터 보호할 수 있고 조건이 만족되지 않은 스레드는 wait loop 의 조건문에 의해 다시 잠들게 된다.
아이템82. 스레드 안전성 수준을 문서화하라
메서드 선언에 synchronized 한정자를 선언할지는 구현 이슈일뿐 API에 속하지 않는다
스레드 안전성에도 수준이 나뉜다. 멀티 스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지원하는 스레드 안전성의 수준을 정확히 명시해야 한다
1.
불변 : String, Long, BigInteger가 대표적으로 이 클래스의 인스턴스는 상수와 같아 외부 동기화도 필요 없다
2.
무조건적 스레드 안전 : AtomicLong, ConcurrentHashMap이 대표적으로 이 클래스의 인스턴스는 수정될 수 있으나 내부에서 충실히 동기화되어 별도의 외부 동기화가 필요 없다
3.
조건부 스레드 안전 : Collections.synchronized 래퍼 메서드가 대표적으로 무조건적 스레드 안전과 같으나 일부 메서드는 외부 동기화가 필요하다
4.
스레드 안전하지 않음 : ArrayList, HashMap 같은 기본 컬렉션이 대표적으로 이 클래스의 인스턴스는 수정될 수 있고 동시에 사용하려면 외부 동기화 메커니즘으로 감싸야 한다
5.
스레드 적대적 : generateSerialNumber 메서드에서 내부 동기화를 생략한 것이 대표적으로 이 클래스는 모든 메서드 호출을 외부 동기화로 감싸더라도 멀티스레드 환경에서 안전하지 않다. 이 클래스나 메서드는 대부분 deprecated 된다
스레드 안전성 어노테이션
1.
@Immutable (불변)
2.
@ThreadSafe (무조건적 스레드 안전, 조건부 스레드 안전)
3.
@NotThreadSafe (스레드 안전하지 않음, 스레드 적대적)
조건부 스레드 안전 클래스는 어떤 순서로 호출할 때 외부 동기화가 요구되고 그때 어떤 락을 얻어야 하는지도 알려줘야 한다
무조건적 스레드 안전 클래스를 작성할 때는 synchronized 메서드가 아닌 비공개 락 객체를 사용하자
락 필드는 변경 가능성을 최소화하기 위해 항상 final로 선언하라
아이템83. 지연 초기화는 신중히 사용하라
지연 초기화는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다
지연 초기화는 성능 최적화 용도로 쓰이지만 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다
성능을 느려지게 할 수도 있기 때문에 지연 초기화는 필요할 때까지 하지 말아야 한다
멀티 스레드 환경에서는 어떤 형태로든 반드시 동기화해야 한다
대부분 상황에서는 일반적인 초기화가 지연 초기화보다 낫다
인스턴스 필드를 초기화할 때 지연 초기화가 초기화 순환성을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하라
성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중 검사 관용구를 사용하라(인스턴스를 volatile로 선언한 후 getter 안에 null체크 관용구 한 번과 sychronized 접근자를 단 초기화 관용구를 통해 초기화된 필드에 접근할 때의 비용을 없애준다)
정적 필드에도 static을 붙인채 synchronized를 사용하는 것이 똑같이 적용된다
성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하라(getter를 통해 홀더 클래스가 처음 읽힐 때 초기화하고 동기화를 전혀 하지 않아 성능이 느려지지 않는다)
반복해서 초기화해도 상관 없는 인스턴스 필드를 지연 초기화 할 때는 이중 검사에서 두번째 관용구를 생략한 단일 검사 관용구를 사용할 수 있다
아이템84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라
운영체제의 스레드 스케줄러는 어떤 스레드가 얼마나 오래 실행할지를 정한다
잘 작성된 프로그램이라면 운영체제마다 다른 이 정책에 좌지우지돼선 안된다
정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다
견고하고 빠릿하고 이식성 좋은 프로그램을 작성하는 가장 좋은 방법은 실행 가능한 스레드의 평균적인 수를 프로세스 수보다 지나치게 많아지지 않도록 하는 것이다
실행 가능한 스레드의 적게 유지하는 주요 기법은 각 스레드가 무언가 유용한 작업을 완료한 후엔 다음 일거리가 생길 때까지 대기하도록 하는 것이다
스레드는 당장 처리해야 할 작업이 없다면 실행돼서는 안된다
스레드 풀 크기를 적절하게 설정하고 작업을 짧게 유지하면 된다. 단 너무 짧으면 작업을 분배하는 부담으로 인해 성능이 더 나빠질 수 있다
스레드는 절대 바쁜 대기 상태가 되면 안 된다
이걸 해결하기 위해 스레드 우선순위나 Thread.yield를 사용하지 말자
Thread.yield는 테스트 할 수단도 없을뿐더러 진짜 원인이 수정되기 전까지는 같은 문제가 반복될 것이다

11장. 직렬화

아이템85. 자바 직렬화의 대안을 찾으라
직렬화는 자바가 객체를 바이트 스트림으로 인코딩하는 것을 말하고 역직렬화는 바이트 스트림으로부터 다시 객체를 재구성하는 것을 말한다
직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장한 후 나중에 역직렬화 할 수 있다
ObjectInputStream의 readObject 메서드는 Serializable 인터페이스를 구현했다면 클래스패스 안의 거의 모든 타입의 객체를 만들어 낼 수 있고 바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다
자바 표준 라이브러리나 서드파티 라이브러리는 물론 애플리케이션 자신의 클래스들도 모두 포함된다.
역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 가젯이라고 부르고 여러 가젯을 함께 구성하여 가젯 체인을 구성할 수도 있다.
역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것으로도 서비스 거부 공격에 쉽게 노출될 수 있고 이런 스트림을 역직렬화 폭탄이라고 한다
직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다
자바 직렬화보단 JSON 이나 프로토콜 버퍼 같은 크로스-플랫폼 구조화된 데이터 표현을 사용하라
JSON은 자바스크립트용으로 브라우저와 서버가 통신하기 위해 설계되었고 텍스트 기반으로 데이터를 표현하기 좋다
프로토콜 버퍼는 C++용으로 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계되었고 문서를 위한 스키마(타입)을 제공하고 올바로 쓰도록 강요하며 이진 표현과 텍스트 표현으로 효율이 좋다
레거시 시스템에서 자바 직렬화를 완전히 배제할 수 없다면 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화 하지 않는 것이다
역직렬화 필터링(java.io.ObjectInputFilter)를 사용하면 특정 클래스를 받아들이거나 거부할 수 있다.
기본 수용 모드에서는 블랙리스트에 기록된 클래스를 거부하고 기본 거부 모드에서는 화이트리스트에 기록된 안전한 클래스들만 수용한다
SWAT 도구를 활용하여 화이트리스트를 자동으로 생성해주는 도구가 있으며 블랙리스트 방식보다는 화이트리스트 방식을 추천한다
아이템86. Serializable을 구현할지는 신중히 결정하라
Serializable을 구현하면 릴리즈한 뒤에는 수정하기 어렵고 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 되므로 영원히 지원해야 한다
직렬화 형태에서는 private과 package-private 인스턴스 필드들마저 공개되므로 캡슐화가 깨지고 필드로의 접근을 최대한 막아 정보를 은닉하지도 못한다
ObjectOutputStream.putFields와 ObjectInputStream.readFields를 사용하면 원래의 직렬화 형태를 유지하면서 내부 표현을 바꿀 수 있지만 어렵고 코드가 지저분해진다.
직렬화된 클래스는 statis final long serialVersionUID 라는 고유 식별 번호를 부여받는데 이 번호를 명시하지 않으면 런타임에 SHA-1로 클래스 이름, 구현한 인터페이스, 클래스 멤버들 등을 고려한 값을 자동으로 부여하는데 이들 중 하나라도 수정된다면 호환성이 깨지게 된다
역직렬화는 객체는 생성자를 사용해 만든다는 기본 메커니즘을 우회하는 객체 생성 기법으로 불변식 깨짐과 허가되지 않은 접근에 노출되기 쉬워진다
직렬화 가능 클래스가 되면 신버전 인스턴스를 직렬화 후 구버전 인스턴스로 역직렬화가 가능한지 그 반대도 가능한지를 테스트해야하기 때문에 테스트의 양이 늘어난다.
역사적으로 BigInteger와 Instant 같은 ‘값' 클래스와 컬렉션 클래스들은 Serializable을 구현하고 스레드 풀처럼 ‘동작’하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않았다
상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며 인터페이스도 대부분 Serializable을 확장해서는 안 된다
클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능하다면 인스턴스 필드 값 중 불변식으로 보장해야하는 값들은 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야 한다. 즉, finalize 메서드를 자신이 재정의하면서 final로 선언하면 된다
인스턴스 필드 중 기본값으로 초기화되면 위배되는 불변식이 있다면 클래스에 다음의 readObjectNoData 메서드를 반드시 추가해야 한다
상속용 클래스인데 직렬화를 지원하지 않으면 하위 클래스에서 직렬화하려 할 때 상위 클래스는 매개변수가 없는 생성자를 제공해야 하는데 제공하지 않는다면 하위 클래스는 어쩔 수 없이 직렬화 프록시 패턴을 사용해야 한다
내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가되므로 내부 클래스는 직렬화를 구현하지 말아야 한다 (단, 정적 멤버 클래스는 Serializable을 구현해도 된다)
아이템87. 커스텀 직렬화 형태를 고려해보라
클래스가 Serializable을 구현하고 기본 직렬화 형태를 사용한다면 현재의 구현에 영원히 발이 묶이게 된다
먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라
기본 직렬화 형태는 그 객체를 루트로 객체가 포함한 데이터들과 그 객체로부터 접근할 수 있는 모든 객체를 담아내며 심지어 이 객체들이 연결된 위상까지 기술한다
이상적인 직렬화 형태라면 물리적인 모습과 독립된 논리적인 모습만을 표현해야하며 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다
기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다
자바독에 직렬화를 문서화하는 어노테이션은 @serial이다
객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 크게 네가지 면에서 문제가 생긴다
1.
공개 API가 현재의 내부 표현 방식에 영원히 묶인다
2.
너무 많은 공간을 차지해 디스크에 저장하거나 전송하는 속도가 느려질 수 있다
3.
직렬화 로직은 그래프를 직접 순회하여 시간이 너무 많이 걸릴 수 있다
4.
기본 직렬화 과정은 객체 그래프를 재귀 순회하기 때문에 스택오버플로를 일으킬 수 있다
StringList의 경우 내부 필드들과 같은 물리적인 표현을 직렬화하는 것이 아닌 총 개수 size와 list에 담긴 문자들을 나열하는 논리적 표현을 직렬화하는 것이 좋다
이 때 기본 직렬화 형태에 포함되지 않는 필드들엔 transient 한정자를 붙인다
직렬화 형태는 writeObject와 readObject가 처리하며 이 메서드들은 defaultWriteObject와 defaultReadObject를 무조건 호출해야 한다
직렬화 형태를 자바독에 문서화하기 위한 어노테이션은 @serialData이다
해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략해야 한다
기본 직렬화 형태를 사용하면 transient 한정자가 붙은 필드들은 기본값으로 초기화되고 기본값을 사용하지 않으려면 readObject에서 defaultReadObject를 호출한 후 해당 필드를 원하는 값으로 복원하거나 처음 사용할 때 초기화하라
객체의 전체 상태를 읽는 메서드에 적용 해야 하는 동기화 메커니즘을 직렬화에도 적용해야 한다. 예를 들어 synchronized 메서드가 있는 객체를 기본 직렬화하려면 writeObject도 synchronized로 선언해야 한다
어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 serialVersionUID를 명시적으로 부여하라
구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하곤 serialVersionUID를 절대 수정하지마라
아이템88. readObject 메서드는 방어적으로 작성하라
물리적 표현과 논리적 표현이 부합해 기본 직렬화 형태를 사용하려하더라도 readObject는 실질적으로 또 다른 public 생성자이기 때문에 주의를 기울여 작성하지 않으면 공격자가 손쉽게 해당 클래스의 불변식을 깨뜨릴 수 있다
그러기 위해선 readObject가 defaultReadObject를 호출한 다음 역직렬화된 객체가 유효한지 검사하고 유효성 검사에 실패한다면 Exception을 통해 잘못된 역직렬화를 막을 수 있다
위 방법으로 불변 인스턴스를 생성하는 것은 막을 수 있지만 악의적으로 객체 참조를 읽어 불변 인스턴스를 수정할 수 있다
이 문제는 불변 인스턴스의 readObject 메서드가 방어적 복사를 충분히 하지 않았기 때문이다. 객체를 역직렬화할 때는 클라이언트가 소유해서는 안 되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다
즉 불변 클래스 안의 모든 private 가변 요소를 모두 방어적으로 복사해야 한다
방어적 복사를 하려면 final 한정자를 제거해야 하고 유효성 검사보다 앞서 진행해야 한다
transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가해도 괜찮지 않다면 커스텀 readObject 메서드를 만들어 모든 유효성 검사와 방어적 복사를 해야 한다 혹은 직렬화 프록시 패턴을 사용하라
readObject 메서드는 재정의 가능 메서드를 직접적이든 간접적이든 호출해서는 안 된다. 이 규칙을 어기고 해당 메서드가 재정의된다면 하위 클래스의 상태가 완전히 역직렬화되기 전에 하위 클래스에서 재정의된 메서드가 실행되므로 오작동 할 수 있다
역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하라
아이템89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
싱글턴 클래스를 직렬화한다면 더 이상 싱글턴 클래스가 아니게 된다
readResolve 메서드를 적절히 재정의하여 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다.
대부분의 경우 이때 새로 생성된 객체의 참조는 유지하지 않으므로 바로 가비지 컬렉션의 대상이 된다
readResolve 메서드를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야 한다. 그렇지 않으면 readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 여지가 남는다
이 공격은 싱글턴이 역직렬화될 때 도둑의 readResolve 메서드를 먼저 호출하고 도둑의 인스턴스 필드에는 readResolve가 실행되기 전인 싱글턴의 참조를 담아 정적 필드에 복사하여 싱글턴이 readResolve가 끝난 후에도 참조할 수 있게 한 후 도둑이 훔친게 아닌 원래 타입의 값을 반환하며 도둑의 readResolve 메서드 실행을 끝낸다
이 문제를 고치기 위해선 싱글턴 클래스를 원소 하나의 열거 타입으로 바꾸는 것이 낫다
직렬화 가능한 인스턴스 통제 클래스를 열거 타입으로 구현하면 선언한 상수 외에 다른 객체는 존재하지 않음을 자바가 보장한다
컴파일 타임에는 어떤 인스턴스들이 있는지 알 수 없을 땐 열거 타입으로 표현하는 것이 불가능하기 때문에 readResolve가 완전히 불필요한 것은 아니다
readResolve 메서드의 접근성은 매우 중요하다.
1.
final 클래스에서라면 readResolve 메서드는 private로 선언한다
2.
final 클래스가 아니고 private으로 선언하면 하위 클래스에서 사용할 수 없다
3.
final 클래스가 아니고 package-private으로 선언하면 같은 패키지에 속한 하위 클래스에서만 사용할 수 있다
4.
final 클래스가 아니고 protected나 public이면서 하위 클래스에서 재정의하지 않았다면 하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException을 일으킬 수 있다
아이템90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
Serializable을 구현하여 생기는 위험을 직렬화 프록시 패턴을 통해 크게 줄일 수 있다
바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private statis으로 선언하면 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시이다
중첩 클래스의 생성자는 단 하나여야 하며 바깥 클래스를 매개변수로 받아야 한다. 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다. 그리고 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다고 선언해야 한다
바깥 클래스엔 writeReplace 메서드를 추가해 바깥 클래스의 인스턴스 대신 직렬화 프록시의 인스턴스를 반환하게 한다
바깥 클래스에 readObject 메서드를 추가해 writeReplace를 통해 직렬화 프록시 인스턴스를 생성하지 않으면 Exception을 뱉도록하면 공격을 막아낼 수 있다
마지막으로 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 직렬화 프록시 인스턴스에 추가하여 역직렬화 시 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해준다
직렬화 프록시는 필드를 final로 선언해도 되므로 진정한 불변으로 만들 수도 있다
또한 직렬화 프록시 패턴은 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다
직렬화 프록시 패턴의 한계
1. 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다
2. 객체 그래프의 순환이 있는 클래스에도 적용할 수 없다
3. 강력한 안전성에 비해 방어적 복사보다 느리다.
제3자가 확장할 수 없는 클래스라면 가능한 한 직렬화 프록시 패턴을 사용하라. 중요한 불변식을 안정적으로 직렬화해주는 가장 쉬운 방법일 것이다.