1. 객체를 생성하고 파괴하기

(1) 생성자 대신 스태틱 팩토리 메서드를 고려하라.

☞  public 스태틱 팩토리 메서드는 단순히 자신이 정의된 클래스의 인스턴스를 리턴하는 메서드로

public static으로 정의한다. 스태틱 팩토리 메서드는 생성자와 달리 이름을 줄 수 있다.

생성자에 전달되는 인자들만으로 그 생성자가 리턴하는 객체의 특징을 알기 어렵다.

하지만.. 스태틱 팩토리 메서드는 이름을 정할 수 있기에 리턴하는 객체의 특징을 설명할 수

있다는 것이다. 그리고 스태틱 팩토리 메서드는 생성자와 달리 호출될 때마다 새로운 객체를

생성하지 않아도 된다. 불변 클래스(immutable class) 같은 경우에 스태틱 팩토리 메서드를

쓰면, 미리 만들어 놓은 인스턴스를 계속 제공할 수도 있고, 동일한 객체가 쓸데없이 생성되는

것을 막기 위해 생성한 인스턴스를 캐시에 저장해 놓고, 필요할 때마다 캐시에 저장된 인스턴스

를 꺼내서 제공할 수 있다. 마지막으로 생성자는 자신이 정의된 클래스의 인스턴스만 리턴할 수

있지만, 스태틱 팩토리 메서드는 자신이 선언되는 것과 같은 타입의 인스턴스는 모두 리턴할 수

있다. 스태틱 팩토리 메서드를 쓰면 리턴할 객체의 타입을 유연하게 선택할 수 있다.

(2) private 생성자를 써서 싱글톤을 유지하라.

☞ 싱글톤이란 정확히 하나의 인스턴스만 만들어지는 클래스로 원래부터 유일할 수 밖에 없는

비디오 출력이나 파일 시스템과 같은 시스템 구성요소들을 주로 표현한다.

싱글톤은 두가지 방법으로 구현할 수 있는데 우선 public static final 멤버 필드를 쓰는 방법을

살펴보자.

public class elvis

{

public static final elvis INSTANCE = new elvis();

private Elvis()

{

..............

}

..............

}

다음은 public static final 멤버 필드 대신에 public 스태틱 팩토리 메서드를 쓰는 방법이다.

public class elvis

{

public static final elvis INSTANCE = new elvis();

private Elvis()

{

..............

}

public static Elvis getInstance()

{

return INSTANCE;

}

..............

}

public static final 멤버 필드를 쓰면 클래스를 구성하는 멤버 선언만으로

그 클래스가 싱글톤이라는 것이 명확하게 드러나며, 이 멤버 필드는 final이므로

항상 동일한 객체를 참조한다. 또 스태틱 팩토리 메서드를 쓰는 것보다 성능이 약간 낫기는 하지만,

웬만한 JVM이라면 스태틱 메서드를 인라인 호출(inline call)로 처리하므로 성능차이를 크게

느낄 수 없을 것이다. public 스태틱 팩토리 메서드를 쓰면 기존 클래스의 API는 그대로 유지하면서

싱글톤의 구현을 마음대로 바꿀 수 있다. 모든 조건을 고려해 볼 때, 영원히 싱글톤으로 남는

클래스라면 스태틱 필드, 그렇지 않으면 스태틱 팩토리 메서드를 쓰는 것이 좋다.

싱글톤 클래스가 직렬화를 제공한다면, 단순히 이 클래스의 선언부에 implements Serializable이란

구문을 더하는 것만으로는 부족하다. 싱글톤을 보장하려면, 반드시 readResolve 메서드를

제공해야 한다. 그렇지 않으면 인스턴스들을 역직렬화할 때마다 새로운 인스턴스가 생겨나서

가짜 클래스가 날뛰는 꼴이 된다. 이것을 막으려면 클래스에 readResolve 메서드를 추가해줘야 한다.

private Object readResolve() throws ObjectStreamException

{

/*

진짜 elvis만 리턴한다.

역직렬화로 태어난 가짜 elvis들은 태어나자마자

Garbage Collector에게 던져 버린다.

*/

return INSTANCE;

}

(3) private 생성자로 인스턴스를 만들지 못하게 하라.

☞ 유틸리티 클래스들은 처음부터 인스턴스를 생성할 수 없게 설계되었을 것이다.

사실, 이런 클래스의 인스턴스는 아무런 의미가 없다. 하지만 생성자를 만들어 놓지 않으면

컴파일러가 자동으로 기본 생성자인 인자 없는 public 생성자를 만들어 버린다는 것을 기억해야

한다. 이렇게 자동으로 만들어진 생성자도 사용자 입장에서 보면 다른 생성자와 다를 것이 없다.

API 제공자의 의도와 달리 인스턴스를 만들 수 있는 것처럼 명세문서가 발표된 클래스들을 쉽게

찾을 수 있다. 그렇다면 어떤 클래스의 인스턴스를 아예 만들지 못하게 하는 방법은 무엇일까?

방법은 의외로 간단한다. private 생성자 하나만 만들어주면 된다.

public class UtilityClass

{

private UtilityClass()

{} // 이 생성자는 호출되지 않는다..

.......................

}

Utility 클래스의 생성자는 이 클래스 밖에서 접근할 수 없다. 따라서 Utility 클래스 내부에서

생성자를 호출하지 않는다면, 절대 인스턴스가 생성되는 일은 없다.

(단점: 상속이 불가능하다...접근할 수 있는 생성자가 없기에...)

(4) 쓸데없는 객체를 중복 생성하지 마라.

☞ 아주 특별한 경우가 아닌 이상, 동등한 기능을 하는 객체를 필요할 때마다 생성하는 것보다는

한 객체를 재사용하는 것이 더 낫다. 재사용이 속도도 더 빠르고 보기에도 편하기 때문이다.

(하지만 요즘 Garbage Collector의 성능이 좋아져서 가볍고 간단한 예제들은 객체풀을

쓰지 않는 편이 더 나은 성능을 낸다. 방어복사가 필요한데도 이미 있는 객체를 그냥 재사용해서

생기는 문제는 쓸데없이 중복된 객체를 생성해서 생기는 문제보다 훨씬 더 심각한 것이라는 것을

명심하도록 하자..)

(5) 쓸모없는 객체 참조는 제거하라.

☞ 보통 자신만의 메모리 영역을 가지는 클래스를 쓸 때, 프로그래머는 메모리 누수에 대해

항상 생각해야 한다. 구성요소를 비울 때마다 null을 대입하여 보관하던 객체 참조를 제거해야 한다.

메모리 누구는 눈에 드러나는 오류가 아니기 때문에 잠복해 있을 가능성이 크다.

메모리 누수는 아주 조심스럽게 코드를 검토하거나 힙 성능검사기로 알려진 도구들을 사용해야만

발견할 수 있다. 따라서 사전에 예방하는 것이 좋다.

(6) 종료자를 쓰지마라.

☞ 종료자들의 행동은 예측이 불가능하고 위험하다. 사실 종료자를 쓸 필요는 없다.

종료자들을 쓰면 프로그램이 이상하게 행동할 수도 있고, 성능이 떨어질 수 있으며,

이식성에도 문제가 발생할 수 있다. 반드시 써야하는 몇 안 되는 경우를 제외하고는

종료자를 안 쓰는 것이 철칙이다.

2. 모든 객체의 공통 메서드 구현하기

(7) equals 메서드를 재정의할 때 표준 구현계약을 지켜라.

☞ 좋은 equals 메서드를 만드는 비법을 알아보자.

ⓐ == 연산자를 써서 인자가 this 객체를 참조하는지 검사한다.

만약 그렇다면 true를 리턴한다. 비교작업이 복잡하다면 이 검사를 하는 것이 좋다.

ⓑ instanceod 연산자를 써서 인자의 타입이 올바른지 검사한다.

만약 타입이 틀리다면 false를 리턴한다.

ⓒ 인자를 정확한 타입으로 변환한다.

ⓓ 주요 필드에 대해 인자의 필드와 this 객체의 해당 필드 값이 동등한지 검사한다.

모두 동등하다면 true를 리턴하고, 하나라도 동등하지 않다면 false를 리턴한다.

ⓔ equals 메서드를 만들었다면, 대칭성, 매개성, 일관성을 지키는지 확인한다.

[주의사항: equals 메서드를 재정의하면 hashCode 메서드도 반드시 재정의 해야 한다.

불안정한 자원에 의존하는 equals 메서드를 작성하지 마라.

equals 메서드의 인자를 Object 차입이 아닌 다른 타입으로 선언하지 마라.]

(8) equals 메서드를 재정의하면 hashCode 메서드도 반드시 재정의하라.

☞ hashCode 메서드를 제대로 재정의하지 않으면 많은 버그가 생길 수 있다.

equals 메서드를 재정의하고도 hashCode 메서드를 재정의하지 않으면

Object.hashCode의 표준 구현계약을 어기는 것이다.

hashCode 메서드의 표준 구현계약을 어긴 클래스를 해시 알고리즘을 기반으로 동작하는

HashMap, HashSet,Hashtable과 같은 컬렉션과 같이 쓰면 문제가 생긴다.

hashCode 메서드를 재정의하지 않으면 두번째 항목을 위반하기 쉽다.

논리적으로 동등한 객체는 반드시 해시코드가 같아야 한다.

equals 메서드를 재정의하면, 물리적으로 다른 두 객체를 논리적으로는 동등한 것으로

판단할 수 있다. 하지만, 만약 hashCode 메서드를 재정의하지 않으면 호출되는

Object.hashCode 메서드는 두 객체의 논리적 동등성을 전혀 알 수 없기 때문에 공통점이

전혀 없는 객체로 판단할 것이다. 따라서 Object.hashCode 메서드가 서로 다른 두 개의 정수를

리턴할 수 있기 때문에, API문서에 정의된 "equals(Object)메서드의 리턴값이 true인 두 객체의

hashCode 메서드는 같은 정수값을 리턴해야 된다."는 조항이 깨지게 된다.

(9) toString 메서드는 항상 재정의하는 것이 좋다.

☞ 정형화된 문자열 형식을 명세하는 것과 상관없이 toString 메서드가 리턴하는 문자열에 포함된

모든 정보에 접근할 수 있는 방법을 제공하는 것이 좋다.

(10) clone 메서드는 신중하게 재정의하라.

☞ Cloneable 인터페이스와 관련된 모든 문제점을 고려해 볼 때,  다른 인터페이스들은

이 인터페이스를 extends 하지 말아야 하고 상속을 위해 설계한 클래스도 역시 이 인터페이스를

implements 하지 말아야 한다. 실제로 더 나은 방법이 많이 있기 때문에 고급 프로그래머들은

값싸게 배열을 복사하는 것과 몇 가지 특이한 경우가 아니라면 절대 clone 메서드를 재정의하거나

쓰지 않는다. 상속을 위해 설계한 클래스에 제대로 동작하는 protected clone 메서드가 없다면,

이 클래스의 하위 클래스는 Cloneable 인터페이스를 implements해도 소용없다.

(11) Comparable 인터페이스의 구현을 고려하라.

☞ 어떤 클래스가 Comparable 인터페이스를 구현하여, Comparable 타입이 되는 순간,

이 클래스는 수많은 일반 알고리즘들과 컬렉션 구현체들과 함께 쓸 수 있다.

아주 작은 노력만으로 어마어마한 혜택을 누릴 수 있는 것이다.

실제로 자바 플랫폼 라이브러리의 모든 값클래스는 이 인터페이스를 구현하고 있다.

3. 클래스와 인터페이스

(12) 클래스와 멤버에 대한 접근은 최소화하라.

☞ 접근성은 최대한 줄여야 한다. 아주 조심스럽게 최소의 public API를 설계한 다음,

쓸데없는 클래스, 인터페이스, 멤버가 API의 일부가 되지 않도록 해야 한다.

public static final 필드가 아니라면 클래스에 public 필드가 있어선 안 된다.

또, public static final 필드라 해도 이 필드는 기본타입이거나 불변 객체만 참조해야 한다.

(13) 불변 클래스를 써라.

☞ 불변 클래스를 만들려면 다음과 같은 규칙을 따라야 한다.

ⓐ 객체를 변경하는 메서드(변경자)를 제공하지 않는다.

ⓑ 재정의할 수 있는 메서드를 제공하지 않는다.

ⓒ 모든 필드를 final로 만든다.

ⓓ 모든 필드를 private으로 만든다.

ⓔ 가변 객체를 참조하는 필드는 배타적으로 접근해야 한다.

불변 객체는 단순하고, 원래부터 다중 쓰레드 환경에서 안전하기 때문에 동기화를 할 필요가 없다.

그리고 불변 객체는 자유롭게 공유될 수 있을 뿐만 아니라, 이 객체들의 내부구조까지도 공유할 수

있다. 이 불변 객체는 다른 객체(가변객체든 불변객체든 상관없이)를 만들 때 쓸 수 있는 아주

중요한 구성요소가 될 수 있다. 하지만 이런 불변 클래스에도 단점이 있으니.. 그 단점은 바로

각각의 값에 대해 서로 다른 객체가 필요하다는 것이다..

그러므로 모든 클래스는 특별한 이유가 없다면 불변 클래스로 만드는 것이 좋다.

(14) 상속보다 컴포지션을 써라.

☞ 상속은 코드를 재사용할 수 있는 좋은 방법이긴 하지만, 상속을 잘못 쓰면 프로그램을 강건하지

못하게 만들 수 있다. 같은 패키지 안에서는 한 프로그래머가 모든 클래스의 구현을 책임지기

때문에 상속을 안전하게 쓸 수 있다. 또 특별히 상속을 위해 설계하고 문서화된 클래스도 안전하게

상속 받을 수 있다. 하지만, 다른 패키지에 있는 일반 클래스를 상속 받는다는 것은 위험하다.

(↑구현상속에 관한 글) 메서드의 호출과 달리 상속은 캡슐화 규칙을 어긴다.

하위클래스가 제대로 동작하려면 상위 클래스가 어떻게 구현되는지 자세히 알아야만 한다.

상위클래스의 구현이 배포판마다 바뀔 때 코드를 수정하지 않으면 하위 클래스는 제대로

동작하지 않는다. 따라서 만약 특별히 상속을 위해 설계하고 문서화한 클래스를 상속받지 않는다면

하위 클래스도 상위 클래스와 함께 계속 진화해야만 한다. 상속은 상위 클래스 API의 오류를

전파시키지만, 컴포지션은 이런 오류를 감춘 새로운 API를 제공할 수 있다.

컴포지션과 포워딩을 쓰면 상속의 폐해를 막을 수 있다. 특히, 적절한 인터페이스가 있어서

래퍼 클래스를 구현할 수 있다면 금상첨화이다. 래퍼 클래스는 강건할 뿐만 아니라

기능도 막강하다.

(15) 상속받을 수 있도록 설계하고 문서화하라. 아니면 상속을 금지하라.

☞ 클래스 명세문서에는 재정의 가능한 메서드가 자기사용을 하고 잇는지도 반드시 명기해야 한다.

또, public이나 protected 메서드나 생성자가 다른 재정의 가능한 메서드를 호출한다면,

어떤 순서로 호출하는지, 호출결과가 다음 작업에 어떤 영향을 미치는지 반드시 명기해야 한다.

클래스 명세문서에는 어떤 상황에서 자신의 재정의 가능한 메서드를 호출하는지에 대해 반드시

명기해야 한다. 상속과 관련된 문제를 해결하는 가장 좋은 방법은 안전하게 상속 받을 수 있도록

설계하지도 않고, 문서화하지도 않은 클래스는 아예 상속받지 못하게 만드는 것이다.

( 꼭 상속받을 수 있게 만들고 싶은 클래스가 있다면, 이러한 클래스의 메서드 내부에서

절대 재정의 가능한 다른 메서드를 호출하지 않아야 하고, 이 사실을 문서화해야 한다.

즉, 재정의 가능한 메서드의 자기 사용을 모두 제거해야 한다. 이렇게 해야 안전하게 상속받을 수

있는 클래스를 만들 수 있다. 메서드를 재정의해도 다른 메서드의 행동에 전혀 영향을 주지 않기

때문이다. )

(16) 추상클래스보다는 인터페이스를 써라.

☞ 이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 일은 어려운 일이 아니다.

인터페이스를 쓰면 깔끔하게 믹스인 타입을 정의할 수 있다.

그리고 계층이 없는 타입 프레임워크를 만들 수 있을 뿐만 아니라, 안전하고 강력하게 클래스에

새로운 기능을 더할 수 있다.

추상 클래스를 쓰는 것이 인터페이스를 쓰는 것보다 좋은 점이 한 가지 있긴 있다.

인터페이스를 발전시켜 가는 것보다 추상클래스를 발전시켜 가는 것이 훨씬 더 쉽다는 것이다.

다시 정리하면 다양한 구현이 가능한 타입을 정의할 때 인터페이스를 쓰는 것이 좋다.

단, 쉽게 기능을 추가할 수 있는 것이 유연성과 강력함보다 더 중요한 경우에는 추상 클래스를

쓰는 것이 좋다. 하지만 그 한계를 이해하고 수용할 수 있을 때만 추상클래스를 사용해야 한다.

외부에 제공하는 중요한 인터페이스에 대한 기본 뼈대 구현을 제공하는 것이 좋다.

public 인터페이스는 신중하게 설계해야 하고, 다양하게 구현을 통한 철저한 시험을 거친 후

발표해야 한다.

(17) 인터페이스는 타입을 정의할 때만 써라.

☞ 인터페이스는 타입을 정의할 때만 써야 한다. 상수를 제공하기 위해 쓰지 말아야 한다.

상수 인터페이스 패턴은 아주 형편없이 인터페이스를 쓰는 것이다. 클래스 내부에서 쓰는

상수는 자세한 구현 방식에 해당하는 것이다. 상수 인터페이스 패턴을 쓰면 이런 자세한 구현방식을

외부에 제공하는 API에 그대로 드러내게 된다. API 사용자에게는 클래스가 상수 인터페이스를

implements 하든 말든 전혀 상관할 바가 아니다. 상수 인터페이스는 오히려 사용자들을 혼란스럽게

만들 수 있다. 더욱 나쁜 것은 이 상수들이 끝까지 지켜야 하는 약속이 된다는 것이다.

(18) 중첩 클래스는 정적 멤버 클래스로 정의하라.

☞ 자바 프로그래밍 언어에는 4가지 종류의 중첩 클래스가 있고, 각각 용도도 다르다. 한 메서드에서만

쓰는 것이 아니거나, 너무 길어서 한 메서드에 넣기 힘들다면 멤버클래스로 만드는 것이 좋다.

멤버 클래스의 인스턴스에서 자신을 감싼 인스턴스의 참조가 필요한 경우에만 비정적 멤버 클래스로

만들고, 나머지의 경우에는 모두 정적 멤버 클래스로 만든다. 메서드 안에 중첩 클래스를 만들어야

하는 경우에 딱 한 곳에서만 쓰거나 이미 이 클래스를 특징짓는 타입이 이미 존재한다면 익명 클래스로

만들고, 그렇지 않다면 지역 클래스로 만들어야 한다.

4. C 구문 바꾸기

(19) 구조체를 클래스로 바꿔라.

☞ 모든 면에서 자바 프로그래밍 언어의 클래스가 더 낫기 때문에 C의 struct 구문은 제거되었다.

구조체는 단순히 몇 개의 자료 필드를 단순히 하나로 묶는 역할만 하지만, 클래스는 여기에 연산을

결합시켜 자료 필드 자체는 사용자로부터 감춘다. 다시 말하면, 클래스는 자료 자체를 객체 안에

감추고 메서드를 통해서만 접근할 수 있게 하여 구현자가 경우에 따라 자유롭게 자료를 표현할 수

있게 해준다. 자바 프로그래밍 언어를 처음 접하는 C 프로그래머들은 클래스가 너무 무거워

구조체를 대체할 수 없는 상황이 있다고 믿고 있지만 그런 경우는 없다.

(20) union은 클래스 계층구조로 바꿔라.

☞ C의 union 구문은 한 가지 타입 이상의 자료를 저장할 수 있는 구조체를 정의할 때 쓴다.

이런 구조체는 보통 최소한 union과 tag라는 두 개의 필드를 가진다. tag는 유니언이

어떤 타입을 가지고 있는지 알려주는 필드로, 보통 enum타입이다. union과 tag 필드를 가진 유니언을

특별히 식별자가 있는 유니언(discriminates union)이라고도 한다. 하지만 자바 프로그래밍 언어에는

다양한 종류의 객체를 하나로 표현할 수 있는 타입을 정의하는 “서브타이핑”이라는 더 좋은 방법이

있기 때문에 union구문을 제공하지 않는다. 식별자가 있는 유니언이란 결국 클래스 계층 구조를

모방한 것뿐이다. 식별자가 있는 유니언을 클래스 계층구조로 바꾸기 위해서는 tag값에 따라 달라지는

행동들을 추상메서드로 표현한 추상클래스를 만들어야 한다. 그 다음으로 식별자가 있는 유니언이

표현할 수 있는 각 타입을 이 추상클래스를 상속받는 클래스로 각각 정의한다. 마지막으로

각 하위 클래스에서 추상메서드를 알맞게 구현하면 된다. 이런 클래스 계층 구조는 식별자가 있는

유니언보다 장점이 많다. 첫째로 클래스 계층구조는 타입 안정성을 제공한다는 것이다.

둘째, 코드가 단순 명료하다는 것이다. 셋째 전혀 별개로 동작하는 다양한 타입들에 대해서도

쉽게 확장할 수 있다는 것이다. 마지막 네 번째 장점은 타입들 사이의 자연스러운 계층 관계를

반영할 수 있어서 유연성이 커지고 컴파일 시점의 타입 검사를 더 잘 할 수 있다는 것이다.

(21) enum 구문은 클래스로 바꿔라.

☞ 타입안전열거 패턴은 열거타입의 구성요소를 표현하는 클래스를 public 생성자 없이 정의한다.

대신, 열거타입의 각 상수에 해당하는 public static final필드를 하나씩 만든다.

이 클래스의 객체를 생성하거나 상속 받을 수 있는 방법이 전혀 없기 때문에

public static final필드를 통해 외부에 제공되는 객체만 존재할 수 있다. 클래스 자체가 final은

아니지만 이 클래스를 상속 받을 수 있는 방법은 없다. 상속을 받으려면 하위 클래스의 생성자에서

상위 클래스의 생성자를 호출해야 하는데, 이 클래스에는 접근 가능한 생성자를 제공하지 않기

때문이다. 타입안전 열거패턴은 컴파일 시점의 타입 안전성을 제공해준다. 그리고

타입안전 열거 클래스에 새로운 상수를 추가해도 클라이언트 코드를 다시 컴파일 할 필요가 없다.

또한 이 클래스에 무슨 메서드든 추가할 수 있기 때문에 인터페이스를 구현할 수도 있다.

타입 안전 열거 패턴은 int 열거 패턴에 비해 장점이 매우 많다. 몇 가지 단점이 있지만,

이것들은 주로 열거 상수들의 집합을 만들어야 할 때나, 주어진 자원이 매우 한정적일 때만

일어나는 문제이다. 따라서 열거타입이 필요하다면 항상 타입안전 열거 패턴을 써야 한다.

타입 안전 열거 패턴을 쓰는 API가 int 열거 패턴을 쓰는 API보다 프로그래머가 쓰기 쉽다.

자바 플랫폼 API에서 타입 안전 열거 패턴을 쓰지 않았던 것은 단지 API를 작성하던 시점에

이 패턴을 몰랐기 때문이다. 마지막으로, 열거타입의 필요성은 상대적으로 적어야 한다는 것을

지겹더라도 또 이야기하고 싶다. 서브클래싱으로 열거타입의 기능을 거의 다 제공할 수 있기 때문이다.

(22) 함수 포인터를 클래스와 인터페이스로 바꿔라.

☞ C에서는 주로 전략 패턴을 구현하기 위해서 함수 포인터를 쓴다.

자바 프로그래밍 언어에서는 전략 인터페이스를 선언하고 구체적인 전략 클래스가

이 인터페이스를 구현하면 된다. 구체적인 전략 클래스를 단 한번만 쓴다면 익명 클래스로 만들어야

한다. 만약, 구체적인 전략 클래스를 다시 써야 한다면 이 클래스를 private 정적 멤버 클래스로 만들고

전략 인터페이스 타입의 public static final 필드로 만들어야 한다.

5. 메서드

(23) 인자의 유효성을 검사하라.

☞ 유효성 검사를 통해 인자가 유효하지 않을 때 적절한 예외를 발생시키면, 빠르고 깔끔하게 메서드를

종료할 수 있다. 인자값을 검사하지 않는다면, 무슨 일이 발생할지 모른다. 메서드가 이상한 예외를

발생시키면서 수행을 멈출 수도 있고, 오류는 나지 않지만 잘못된 결과를 내는 것과 같은

더 나쁜 상황에 빠질 수도 있다. 하지만 가장 나쁜 상황은 마치 아무 문제없는 것처럼 정상적으로

메서드는 끝났지만, 객체의 상태가 이상해져서 불특정시점에 불특정 위치에서 오류가 나는 것이다.

메서드나 생성자를 만들 때 인자에 어떤 조건이 있는지 반드시 검토해야 한다. 이 제약 조건들을

문서화하고 메서드 본문의 시작부분에서 이 제약조건을 검사해야 한다. 이것을 습관화하라.

조금만 신경 써서 이 사소한 작업을 해놓으면, 인자가 제약조건을 어겼을 때 충분히 보상받을 수 있다.

(하지만 인자들을 함부로 제약하는 것이 좋다는 것은 아니다. 오히려, 가능하다면 최대한으로

관대하게 메서드를 설계해야 한다. 메서드가 제대로 동작한다면 인자에 적용하는 제약은

적으면 적을수록 좋다. 하지만, 어떤 종류의 제약은 구현하는 추상화에 어쩔 수 없이 원래부터

내재된 경우도 있다.)

(24) 필요한 경우 방어 복사하라.

☞ 클래스의 모든 클라이언트는 항상 불변규칙을 깨뜨리기 위해 최선을 다한다고 가정하고,

프로그래밍 할 때 항상 방어하는 자세를 가져야 한다.

생성자에 전달되는 변경가능 인자들을 방어복사 해야 하고, 원본 대신에 복사본으로 인스턴스를

만들어야 한다. 그리고 인자의 유효성을 검사하기 전에 먼저 복사하고 나서 원본이 아닌 복사본의

유효성을 검사한다는 것에 주목해야 한다. 이상해 보이는가? 하지만 이렇게 해야 할 이유가 있다.

이렇게 해야 인스턴스가 생성될 때 인자를 검사하는 시점과 복사하는 시점 사이의

“공격당하기 쉬운 창가”에 머물러 있는 동안 다른 쓰레드가 생성자에 전달한 인자를 바꿔도

불변규칙을 지킬 수 있기 때문이다.

(25) 메서드 시그니처를 신중하게 설계하라.

☞ 메서드 이름을 신중하게 결정하라. 이해하기 쉽고, 같은 패키지에 잇는 다른 클래스나

인터페이스들과 일관성 있는 이름을 짓도록 노력해야 한다.

그리고 편리한 메서드를 제공하기 위해 너무 애쓰지 마라. 모든 메서드는 “자기 나름대로의 역할”을

해야 한다. 메서드가 너무 많으면 클래스를 배우고, 쓰고, 문서화하고, 시험하고, 관리하기 어려워진다.

만약, 인터페이스에 메서드가 너무 많다면 사용자뿐만 아니라 구현자에게도 영향을 주므로 어려움은

두 배로 커진다. 여러분이 만든 타입이 지원해야 하는 행위 하나하나에 완벽하게 기능을 발휘하는

메서드를 제공하라. 정말 자주 쓰이는 경우에만 여러 기능을 한 번에 처리하는 “속기” 메서드를

제공할지 고민하라. 만약 조금이라도 의심이 생기면 “속기”메서드를 그냥 제거하라.

인자를 너무 많이 받지 마라. 인자는 적을수록 좋다. 현실적으로 인자가 세 개를 넘으면 문제가

있다고 본다. 이것이 일반 규칙이다. 대부분의 프로그래머는 인자 개수가 세 개를 넘으면 기억하지

못한다. 만약, API가 인자 개수가 세 개보다 많은 메서드들로 이루어졌다면, 이 API를 명세문서 없이는

거의 쓸 수 없을 것이다. 특히, 동일한 타입의 인자가 죽 이어져 있으면 훨씬 더 위험하다.

인자의 순서를 기억하기 힘들다는 것도 문제이지만 실수로 순서를 바꿔 인자에 값을 넣으면

컴파일도 잘 되고, 실행도 되지만 분명히 이상한 행동을 할 것이다. 인자의 길이를 줄이는 두 가지

방법이 있다. 그 중 하나는 많은 인자를 받는 메서드를 인자의 일부만 받는 여러 개의 메서드로

나누는 것이다. 마지막으로 인자들을 모아서 보관하는 헬퍼 클래스도 인자 수를 줄일 수 있는

좋은 방법이다. (보통 정적 멤버 클래스로 헬퍼 클래스를 만든다.)

(26) 메서드를 중복 정의할 때는 신중하라.

☞ 메서드를 중복 정의 할 수 있다는 것을 반드시 중복정의 하라는 것으로 벌대 받아들이지 말아야 한다.

인자 수가 같은 메서드가 여러 개 생기도록 중복정의 하지 말아야 한다. 하지만, 이 규칙을

적용할 수 없는 경우도 있다. 이런 경우에도, 최소한 같은 수의 인자 집합이 간단한 타입 변환만 하면

다른 중복 정의 메서드에 전달될 수 있는 상황만은 피해야 한다. 기존 클래스가 새로운 인터페이스를

구현하는 것과 같이 이런 상황을 피할 수 없다면, 같은 수의 인자를 받는 중복 정의 메서드들이 똑같이

행동하도록 만들어야 한다. 이 규칙들을 지키지 않고 중복정의 메서드를 만들어 놓으면,

대부분의 프로그래머들은 이 중복정의 메서드들을 제대로 쓰지 못할뿐더러, 왜 제대로 동작하지 않는지

조차 모른 채 고민만 할 것이다.

(27) 널(NULL)이 아닌 길이가 0(zero-length)인 배열을 리턴하라.

☞ 다음과 같은 메서드를 만드는 경우를 많이 볼 수 있다.

private List cheeseInStock = ...;

/*

상점에 남아있는 모든 치즈의 배열을 리턴하거나, 팔 수 있는 치즈가 없으면 null을 리턴한다.

*/

public Cheese[] getCheeses()

{

if(cheeseInStock.size()==0)

return null;

....................

}

팔 수 있는 치즈가 남아 있지 않다고 특별하게 처리할 이유는 없다. 다음과 같이 간단하게 처리할 수

있는 것을 if(Array.asList(shop.getCheeses()).contains(Cheese.STILTON))

{System.out.println("Jolly good");}

다음과 같이 복잡하게 null인지 확인까지 해주어야 한다.

Cheese[] cheeses = shop.getCheeses();

if(cheeses != null && Array.asList(shop.getCheeses()).contains(Cheese.STILTON))

{System.out.println("Jolly good");}

길이가 0인 배열을 리턴할 수 있는 곳에서 null을 리턴하는 메서드를 쓸 때마다,

이렇게 필요 없는 코드가 반복되어야 한다. 게다가 null을 처리하는 코드를 실수로 빼먹기 쉽기 때문에

오류가 일어나기 쉽다. 배열을 리턴하는 메서드에서 null을 리턴할 이유가 전혀 없다.

null을 리턴할 상황이라면 길이가 0인 배열을 리턴하면 된다.

(배열의 길이와 배열 자체가 분리되어 리턴되는 C 프로그래밍 언어의 영향을 받아 null을 리터해온

것 같다. C에서는 길이가 0으로 리턴하더라도 배열을 할당하는 데는 전혀 이득을 주지 못한다.)

(28) 외부에서 제공하는 API의 모든 구성요소에 대해 문서화 주석을 달아라.

☞ 문서화 주석은 API를 문서화 할 수 있는 가장 훌륭하고 효율적인 방법이다. 외부에 제공하는

모든 API에 대해 반드시 문서와 주석을 달아야 한다. 또, 문서화 주석을 작성하는 표준규칙을 지켜

일관성 있는 문서를 만들어야 한다. HTML 문서 조각을 문서화 주석에 포함시킬 수 있고,

HTML 메타문자는 반드시 이스케이프 처리해야 한다는 것도 기억해야 한다.

6. 프로그래밍 일반

(29) 지역변수의 유효범위를 최소화하라.

☞ 지역변수의 유효범위를 최소화하면 코드의 가독성이 좋아지고, 유지보수가 편해지고,

오류의 발생 가능성도 줄어든다. 지역 변수의 유효범위를 최소화하는 가장 좋은 방법은 쓰기

바로 직전에 선언하는 것이다. 지역변수를 너무 일찍 선언하면 이 변수가 너무 일찍부터 영향력을

미치는 것뿐만 아니라 너무 늦게까지 영향력을 미치는 것도 문제다. 지역변수는 선언한 시점부터

이것을 포함하는 블록의 끝까지 영향력을 미친다. 만약, 어떤 변수를 실제로 이 변수를 쓰는 블록

바깥에 선언해 놓았다면, 이 블록이 끝난 다음에도 계속 이 변수는 영향력을 미친다.

만약, 이 변수를 다른 곳에서 다른 의도로 쓴다면 심각한 문제가 생길 수 있다.

대부분의 지역 변수는 선언과 함께 초기화해야 한다. 만약, 변수를 선언할 때 의미 있는 값으로

이 변수를 초기화하는데 필요한 정보가 충분하지 않다면, 정보가 충분해질 때까지 변수 선언을 미루어야

한다. (한 가지 예외라면 try~catch문을 사용하는 경우) 그리고 반복문에는 변수의 유효범위를 최소화

할 수 있는 특별한 방법이 있다. for 반복문 안에서 반복 변수를 선언할 수 있어서,

이 변수의 유효범위를 정확하게 필요한 부분으로 제한할 수 있다. 만약 반복변수를 반복문 안에서만

쓴다면, while문 대신 for문을 쓰는 것이 좋다. 그리고 마지막으로 지역변수의 유효범위를 최소화 하려면

, 한 메서드는 한 가지 작업만 처리하도록 가능하면 작게 만들어야 한다. 두가지 이상의 작업을 하나의

메서드에서 처리한다면, 하나의 작업에 썼던 지역변수가 다른 작업을 처리할 때까지 유효할 수 있기

때문에 문제가 발생할 수 있다. 따라서 하나의 메서드가 하나의 작업만 처리할 수 있도록 메서드를

적절히 나누어 이런 문제를 미리미리 막아야 한다.

(30) 라이브러리를 배우고 익혀서 써라.

☞ 모든 것을 다시 만들려고 하지 마라. 공통 기능인 것처럼 보이는 것은 이미 표준 라이브러리에 있을

가능성이 크다. 표준 라이브러리에 있는 것을 써라. 표준 라이브러리에 잇는지 없는지 모르겠다면

다시 한 번 확인하라. 표준 라이브러리가 제공하는 것이 여러분이 직접 만든 것보다 십중팔구

더 나을 것이다. 라이브러리의 코드는 오랜 시간에 걸쳐 개선되었기 때문이다. 여러분의 능력을

폄하하려는 것이 아니라 단지, 표준 라이브러리의 코드들은 일반 프로그래머가 같은 기능을 구현하기

위해 애쓴 것보다 훨씬 많은 투자를 통해 태어난 코드이기 때문이다.

(31) 정확한 계산에 float이나 double 타입을 쓰지 마라.

☞ float과 double 타입은 과학과 공학 계산용으로 만든 것이다. 이 타입들은 매우 넓은 범위의 수에 대한

매우 정확한 근사값을 빨리 계산할 수 있는 이진 부동소수점 연산을 수행한다.

하지만, 이 타입들의 값은 정확하지 않기 때문에, 정확한 계산이 필요한 상황에서는 절대로

쓰지 말아야 한다. 특히, 금전 계산할 때 float이나 double 타입을 절대로 쓰지 말아야 한다.

약간 불편하더라도 BigDecimal을 써서 시스템이 소수점을 관리하게 하라.

BigDecimal은 쓰면 모두 8가지의 우수리 처리방식을 제공하므로 아주 편리하다.

만약 성능이 매우 중요하다면, int나 long을 쓰면서 소수점을 직접 관리하라.

다루어야 하는 숫자의 자릿수가 모두 합쳐서 9자리를 넘지 않으면 int를 쓰고, 18자리를 넘지 않으면

long을 써라. 이것보다 더 커지면 BigDecimal을 써라.

(32) 적절한 타입 대신 문자열을 쓰지 마라.

☞ 알맞은 데이터 타입이 있거나, 새로운 데이터 타입을 만들 수 있다면, 문자열로 객체를 표현하지

말아야 한다. 적절한 데이터 타입이 있거나, 새로운 데이터 타입을 만들 수 있다면, 문자열로 객체를

표현하지 말아야 한다. 적절한 데이터 타입 대신 문자열을 쓰면 오류가 발생하기 쉽고, 느리고,

유연성이 떨어질 수 있다. 기본타입, 열거타입, 집합타입을 써야할 곳에 문자열을 쓰면 안 된다.

(33) 성능을 떨어뜨리는 문자열 연결을 조심하라.

☞ 문자열 연결 연산을 쓰면 성능이 나빠질 수 있다.

대신 StringBuffer 클래스의 append 메서드를 써라.

아니면 문자타입 배열(char[])을 쓰거나, 문자열을 연결하지 말고 한 번에 한 문자열만 처리하라.

(34) 인터페이스 타입으로 객체를 참조하라.

☞ 인자타입으로 클래스보다는 인터페이스를 쓰는 것이 좋다는 것은 이미 (25)에서 살펴보았다.

이 지침은 “객체를 참조할 때는 클래스 타입보다는 인터페이스 타입을 쓰는 것이 더 좋다.”라는 것으로

일반화할 수 있다. 적절한 인터페이스 타입이 있다면 인자, 리턴값, 변수, 필드는 반드시 인터페이스

타입으로 선언해야 한다. 클래스는 객체를 생성할 때만 필요하다.

객체 참조의 타입을 인터페이스 타입으로 선언하면 유연한 프로그램을 만들 수 있다.

하지만 적절한 인터페이스가 없다면, 인터페이스 타입이 아닌 클래스 타입으로 객체를 참조해도 괜찮다.

예를 들어, String이나 BigInteger와 같은 값클래스가 다양한 구현체를 가진다는 것은 상상하기 어렵다.

이 클래스들은 대부분 final이고, 해당하는 인터페이스를 가진 경우는 거의 없다. 따라서 값클래스는

인자, 변수, 리턴타입으로 쓸 수 있다. 어떤 클래스에 대응되는 적당한 인터페이스가 없다면,

클래스 타입으로 객체를 참조할 수밖에 없다. Random클래스가 이런 경우에 속한다.

즉, 적절한 인터페이스 타입이 있다면, 객체를 반드시 인터페이스 타입으로 참조해야 프로그램이

유연해진다. 만약, 인터페이스 타입이 없다면, 필요한 기능을 제공해 주는 최상위 클래스 타입을

사용하는 것이 좋다.

(35) 리플렉션보다 인터페이스를 써라.

☞ 리플렉션은 굉장히 복잡한 시스템을 만들 때 꼭 필요한 강력한 기능이지만 많은 단점이 있다.

만약, 컴파일 시점에 없는 클래스를 써야 하는 프로그램을 만들 때, 가능하면 인스턴스 생성할 때만

리플렉션을 쓰고, 인스턴스에 대한 접근은 컴파일 시점에 이미 알려진 인터페이스나 상위 추상클래스를

쓰는 것이 좋다.

(36) 네이티브 메서드는 신중하게 써라.

☞ Java Native Interface(JNI)는 자바 프로그램에서 C나 C++과 같은 네이티브 프로그래밍언어로 작성한

특수한 메서드인 네이티브 메서드를 호출할 수 있게 해준다. 네이티브 메서드는 네이티브 언어를 써서

다양한 계산을 수행한 후, 그 결과를 자바 프로그래밍 언어로 작성한 프로그램에 리턴한다.

네이티브 메서드는 크게 세 가지 용도가 있다. 첫째, 레지스트리나 파일락과 같이 플랫폼에 종속적인

기능에 접근할 때 쓴다. 둘째, 레거시(legacy)에 존재하는 데이터에 접근하기 위해 레거시 언어로 작성한

라이브러리에 접근할 때 쓴다. 셋째, 성능에 민감한 부분을 네이티브 언어로 작성하여 성능을 높일 때

쓴다. (하지만 1.3 배포판부터 JVM의 성능이 좋아지면서 네이티브 메서드를 써서 성능을 높이는 것을

권장하지 않는다.) 네이티브 메서드는 여러 가지 심각한 단점이 있다. 네이티브 프로그래밍 언어는

안전하지 않기 때문에 네이티브 메서드를 쓴다면 메모리에 신경 써야 한다. 네이티브 프로그래밍 언어는

플랫폼에 종속되었기 때문에, 네이티브 메서드를 쓰는 프로그램은 더 이상 플랫폼에 독립적이지 않다.

네이티브 프로그래밍 언어로 만든 부분은 플랫폼이 바뀌면 다시 컴파일 해야하고 십중팔구 소스까지

바꿔야 할 것이다. 또, 네이티브 메서드에 진입하고 빠져 나오는데 드는 고정비용이 있기 때문에

아주 작은 부분만 네이티브 메서드로 만들면 오히려 성능을 떨어뜨릴 수도 있다. 네이티브 메서드는

작성하기 어렵고 작성한 코드는 가독성도 떨어진다.

네이티브 메서드를 꼭 써야 하는지 다시 한 번 생각해보라. 네이티브 메서드를 써서 성능을 높이려

하지 마라. 시스템의 저수준 자원이나 기존 라이브러리에 접근하기 위해 네이티브 메서드를 써야 한다면

네이티브 코드는 최소로 줄여야 하고, 철저한 시험 과정을 거쳐야 한다. 네이티브 코드에 있는 아주

작은 버그가 전체 프로그램을 죽일 수 있다.

(37) 신중하게 최적화하라.

☞ 1) 빠른 프로그램보다는 좋은 프로그램을 만들기 위해 노력하라. 좋은 프로그램이 빠르지 않다면,

아키텍처 차원에서 최적화할 것이다. 좋은 프로그램은 정보은폐 원칙을 지켜야 한다.

가능하면 개별 모듈 안에 설계의 결정사항을 숨겨야 나중에 설계 결정사항을 수정한다고 해도

시스템의 나머지 부분은 영향을 받지 않을 것이다. 그렇다고 프로그램이 완성될 때까지 성능 문제를

무시하라는 이야기는 아니다. 구현 때문에 발생한 성능문제는 나중에라도 최적화를 통해 바로

잡을 수 있지만, 만연한 아키텍처의 결함 때문에 생긴 성능 문제는 시스템을 새롭게 만들지 않는다면

거의 바로 잡을 수 없다. 모든 문제가 발생한 다음에 기본 설계를 고친다면, 유지하기도 어렵고

발전시키고도 어려운 이상한 구조의 시스템이 태어날지도 모른다. 따라서 성능 문제는 설계단계에서

미리 생각해야 한다.

2) 성능을 제한할 수 있는 설계는 하지 마라. 나중에 가장 바꾸기 어려운 설계요소는 모듈과

외부 환경 사이의 인터페이스이다. API, 통신 프로토콜, 영구히 저장할 데이터 형식과 같은 것들이

바꾸기 어려운 설계요소들 중에서도 가장 중요한 것들이다. 이런 설계 요소는 수정이 어렵거나

불가능할 뿐만 아니라, 시스템이 달성해야 할 성능 목표에도 중대한 영향을 미칠 수 있다.

3) API에 대한 설계 결정이 성능에 미치는 영향을 고려해야 한다.

가변타입을 public으로 선언하면 쓸데없는 방어복사가 필요할지도 모른다.

비슷하게, 컴포지션이 바람직한 상황에서 public 클래스들끼리 상속관계를 맺으면,

하위클래스는 영원히 상위클래스에 묶여 성능에 제약을 받을 수도 있다.

API에 인터페이스가 아닌 구체적인 클래스를 쓰면, API가 특정 구현체에 종속되어 나중에 성능이

더 나은 구현체로 바꿀 수 없다. API 설계가 성능에 미치는 영향은 매우 현실적인 문제이다.

(38) 일반 작명규칙을 지켜라.

☞ 표준 작명규칙을 완전히 익혀 마치 습관처럼 쓸 수 있어야 한다. 글자규칙은 단순하고 명백하지만

문법규칙은 좀 더 복잡하고 느슨하다. “이미 오랜 관행에 따라 다른 식으로 이름을 지어야 한다면,

이 작명규칙을 모조건 따를 이유는 없다.” 상식에 맞춰 이름을 지어라.

7. 예외처리

(39) 예외는 예외상황에서만 써야 한다.

☞ 예외는 말 그대로 예외상황에서만 써야한다. 프로그램 흐름을 예외로 제어하려 하면 안 된다.

좋은 API는 클라이언트가 프로그램 흐름을 제어할 때 예외를 쓸 수밖에 없도록 만들지 않는다.

(40) 처리해야 하는 예외와 런타임 예외를 구분해서 던져라.

☞ 처리해야 하는 예외(checked exception)와 처리하지 않는 예외(unchecked exception) 중 어떤 것을

던져야 하는지 결정하는 규칙은 다음과 같다. 처리해야 하는 예외는 호출자가 예외상황을 복구할 수

있다고 기대할 수 있을 때 던진다. 처리해야 하는 예외를 던진다는 것은, 호출자에게 이 예외를

catch 구문으로 잡아서 처리하거나 다시 던져 외부로 전파시킬 것을 강요하는 것이다.

메서드 시그니처에 나오는 예외는 메서드를 호출했을 때 발생할 수 있는 상황을 API 사용자에게

강력하게 경고하는 것이다. API 사용자는 처리해야 할 예외를 만나면, API 설계자가 예외가 발생한

상황을 반드시 복구하도록 요구하는 것이라고 생각해야 한다. 물론, 예외를 잡아서 무시하는 방법으로

이 요구를 묵살할 수도 있지만 이것은 아주 잘못된 생각이다. 처리하지 않는 예외에는 런타임 예외와

런타임 에러가 있다. 이 둘의 행동 방식은 똑같다. 이것들을 잡을 필요도 없고, 특별한 경우가 아니라면

잡아서도 안 된다. 런타임 예외나 에러가 발생하면, 복구할 수 없는 상황이기 때문에 더 이상 프로그램

을 실행하는 것은 위험할 수 있다. 이런 예외를 잡지 않으면 문제가 발생한 쓰레드는 적절한

오류 메시지를 내고 멈추기 때문에 위험을 피할 수 있다. 위험을 해결할 수 없다면 피해야 한다.

런타임 예외는 프로그래밍 오류가 발생했을 때만 써야 한다. 대부분 런타임 예외는 사전조건을

어겼을 때 발생한다. (API 클라이언트가 API 명세에 기술된 계약을 지키지 않았기 때문에 생기는

실패가 바로 사전조건 위반이다.)

(41) 처리해야 하는 예외는 꼭 필요할 때만 던져라.

☞ 예외처리 메커니즘은 자바 프로그래밍 언어의 훌륭한 특징이다. 리턴코드와 달리, 처리해야 하는 예외가

발생하면 프로그래머는 반드시 예외상황을 처리할 수밖에 없기 때문에 프로그램의 신뢰도가 크게

나아진다. 하지만, 처리해야 하는 예외를 던지는 메서드를 호출하는 메서드는 catch 블록을 써서

이 예외를 잡거나, 같은 예외를 던진다고 선언하여 이 예외를 외부로 전파해야만 한다.

사실 이런 작업은 프로그래머에게는 만만치 않은 부담을 준다. API를 정확히 쓰더라도 이런 예외가

발생할 수 있고, 프로그래머가 이 예외를 적절하게 처리할 수 있을 때만 처리할 수 있는 예외를

던져야 한다. 이 두 가지 조건을 모두 만족시키지 못한다면 처리하지 않는 예외를 던지는 것이 좋다.

(42) 표준 예외를 써라.

☞ 고급 프로그래머와 초급 프로그래머를 구분하는 방법은 무엇일까? 고급 프로그래머는 코드를

재사용하기 위해 최대한 노력한다. 예외도 마찬가지다. 자바 플랫폼 라이브러리는 대부분 API에서

쓸 수 있도록 처리하지 않는 예외의 기본형을 거의 다 제공한다. 기존 예외를 재사용하면 몇 가지

장점이 있다. 최고의 장점은 많은 프로그래머들이 이미 이 예외에 익숙하기 때문에, API를 쉽게

배울 수 있고 쓸 수 있다는 것이다. 두 번째 장점도 비슷한데, 이 예외들을 쓴 API로 작성된 프로그램은

잘 모르는 예외들을 썼을 때보다 가독성이 좋아진다. 마지막 장점은 예외 클래스가 적을수록

메모리 사용과 초기 클래스 로딩시간이 줄어든다는 것이다.

(43) 예외를 적절하게 추상화하라.

☞ 어떤 메서드가 자신이 수행하는 작업과 전혀 관련이 없어 보이는 예외를 던진다면 사용자는 무척

혼란스러울 것이다. 이런 일은 추상화 수준이 낮은 계층에서 발생한 예외를 그대로 전파하는

메서드에서 주로 발생한다. 이런 혼란을 참아낼 수 있다 해도 낮은 계층의 세부 구현사항이 외부에

드러나게 되어 추상화 수준이 높은 계층의 API를 오염시킬 수 있다. 만약, 다음 배포판에서 추상화

수준이 높은 계층의 구현이 변하여 던지는 예외가 달라진다면, 기존 클라이언트 프로그램은 아마도

못쓰게 될 것이다. 이런 문제를 해결하려면, 높은 계층에서 낮은 계층의 예외를 잡아서 높은 계층의

추상화 수준에 맞게 변환해서 던져야 한다. 이런 구현패턴을 예외변환(exception translation)이라고

한다. 예외변환의 특별한 형태로 예외 연쇄(exception chaining)라는 것이 있다. 이 방식은 낮은 계층의

예외가 디버깅에 도움이 될 때 적합하다. 낮은 계층에서 발생한 예외를 저장하고, 이 저장한 예외에

접근할 수 있는 접근자를 제공하는 높은 계층의 예외를 정의하면 예외연쇄를 쓸 수 있다.

아무 생각 없이 낮은 계층의 예외를 높은 계층으로 전파하는 것보다 예외변환을 쓰는 것이 더 좋지만,

남용해서는 안 된다. 낮은 계층의 예외를 다루는 가장 좋은 방법은 낮은 계층의 메서드를 호출하기 전에

이 메서드의 사전조건을 보장하여 예외발생을 아예 원천봉쇄하는 것이다. 높은 수준의 메서드가

인자들을 낮은 계층으로 전달하기 전에 미리 유효성을 명시적으로 검사하여 최대한 예외발생을

피해야 한다. 낮은 계층에서 발생하는 예외를 막을 수 없다면, 높은 계층에서 이 예외들을 조용히

처리하고, 사용자들을 낮은 계층에서 발생한 문제부터 완전히 격리 시키는 것이 좋다.

이런 환경에서는 적절한 로깅 기능을 써서 발생한 예외를 기록해 둘 필요가 있다.

낮은 계층에서 예외가 발생하는 것을 막지 못 하거나 높은 계층을 낮은 계층의 예외로부터 격리하지

못한다면, 예외변환을 쓰는 것이 좋다. 낮은 계층의 메서드가 던지는 예외가 우연히도 높은 계층의

메서드가 던질 수 있는 예외와 같을 때만 예외를 단순히 전파할 수 있다.

(44) 메서드가 던지는 모든 예외를 명세문서에 기술하라.

☞ a) 처리해야 하는 예외는 메서드 선언부에 하나씩 선언하고, @throws 태그를 써서 모든 예외가

발생하는 상황을 정확하게 문서화하라.

b) 처리하지 않는 예외는 @throw 태그를 써서 명세문서에 기술하지만, 메서드 선언의 throws 절에는

나타나지 말아야 한다.

c) 대부분 메서드가 같은 예외를 던지는 클래스라면, 이런 예외는 각 메서드의 문서화 주석에

기술하는 것보다 클래스의 문서화 주석에 기술하는 것이 좋다.

(45) 실패에 대한 자세한 정보를 상세 메시지 문자열에 담아라.

☞ 예외의 실패 원인을 알 수 있는 정보에 접근할 수 있는 접근자 메서드를 제공하는 것도 좋다.

처리하지 않는 예외는 이런 접근자 메서드를 제공하지 않아도 되지만, 처리해야 하는 예외는

반드시 제공하는 것이 좋다. 실패 원인에 대한 정보가 있으면, 실패를 복구하기 더 쉽게 때문이다.

처리하지 않는 예외라 할지라도 (9)의 일반 원칙에 따라 접근자 메서드를 제공하는 것이 좋다.

(46) 실패 원자성을 얻기 위해 노력하라.

☞ 예외를 던지고 난 객체는 어떤 상태여야 할까? 예외를 던졌어도 객체 상태는 분명하고 다시 쓸 수 있는

것이 바람직하다. 특히, 호출자가 예외상황을 복구해야 한다면, 객체상태가 안정되어 있어야 하는 것은

무엇보다도 중요하다. 메서드 호출이 실패하더라도 객체 상태는 메서드 호출 전과 같아야 한다.

이런 특징을 가진 메서드를 “실패 원자성이 있는(failure atomic) 메서드”라고 부른다.

언제나 실패 원자성을 얻기 위해 노력해야 하지만 항상 가능한 것은 아니다.

예를 들어, 두 개의 쓰레드가 동기화하지 않은 채 같은 객체를 동시에 수정한다면, 이 객체의 상태는

이상해질 수 있다. ConcurrentModificationException을 잡은 후에도 이 객체를 계속 쓸 수 있다고

생각하면 오산이다. 또, 오류는 예외와 달리 보통 복구할 수 없기 때문에 오류가 발생했을 때

실패 원자성을 달성하기 위해 애쓸 필요가 없다. 실패 원자성을 제공할 수 있다고 항상 그렇게

해야 하는 것은 아니다. 실패 원자성을 제공하는 것은 매우 복잡하고 비용이 많이 드는 작업일 수 있다.

그러나, 잘 알고 있기만 하면 실패 원자성을 달성하는 것은 쉬운 일이다. 보통 메서드 명세문서의

한 부분을 차지하는 예외가 발생했을 때, 메서드를 호출하기 전이나 호출한 다음에 객체의 상태는

같아야 한다. 이 규칙을 어긴다면 API 명세문서에 객체상태가 어떻게 바뀌는지 정확하게 밝혀야 한다.

하지만, 불행하게도 현재 많은 API 문서들에 이런 설명이 없는 것이 현실이다.

(47) 예외를 잡아서 버리지 마라.

☞ 처리해야 하는 예외와 처리하지 않는 예외에 똑같이 적용이 된다.

예외가 예측 가능한 상황을 표현하든, 프로그래밍 오류를 표현하든 예외를 잡아서 처리하지 않고 버리면

프로그램은 오류가 발생해도 아무 일 없듯이 잘 진행되는 것처럼 보인다. 그러다가, 문제의 근원과

전혀 상관없는 곳에서 오류를 내며 프로그램이 죽어버릴지도 모른다. 예외를 적절히 처리한다면

프로그램 실패를 완전히 방지 할 수도 있다. 처리하지 않는 예외를 단지 외부로 전파시키기만 해도

적어도 실패의 원인을 디버깅할 수 있는 충분한 정보를 제공하면서 프로그램을 재빨리 끝낼 수 있지만,

잡아서 버리면 아무것도 할 수 없게 된다. 절대 예외를 잡아서 버리지 마라.

8. 쓰레드

(48) 변경 가능한 공유 데이터에 접근할 때 동기화하라.

☞ 여러 쓰레드가 변경 가능한 데이터를 공유하려면 읽고 쓸 때 반드시 잠금장치를 얻어야 한다.

읽고 쓰는 작업이 아무리 원자성 작업이더라도 단지 “성능”을 높인다는 이유로 동기화를 빼먹지 마라

동기화 하지 않으면 한 쓰레드가 변경한 공유 데이터를 다른 쓰레드가 알지 못할 수도 있다.

동기화 하지 않은 채 공유 데이터에 접근하면 문제가 생길 수 있고, 이 문제는 재현하기도 아주 어렵다.

이런 문제는 경우에 따라 일어날 수도 있고, 이 문제는 재현하기도 아주 어렵다. 이런 문제는 경우에

따라 일어날 수도 있고 일어나지 않을 수도 있으며, JVM의 구현 방식과 하드웨어의 특성에 따라

달라질 수도 있다.

(49) 지나친 동기화는 피하라.

☞ 교착상태나 데이터 손상을 막으려면, 동기화 영역에서 외계인 메서드를 절대로 호출하면 안 된다.

동기화 영역에서는 가능하면 가장 작은 작업만 처리하는 것이 원칙이다. 가변 클래스를 설계할 때는

반드시 클래스 내부를 동기화할 것인지 고민해야 한다. 자바 플랫폼 초창기처럼 동기화를 생략했다고

성능이 크게 나아지지 않을 수도 있지만, 여전히 무시할 수 없는 부분이다.

클래스 내부를 동기화할지 말지 결정하는 것은 설계하는 클래스가 어떤 환경에서 쓰이는지에 따라

많이 달라진다. 어떻게 결정하든 반드시 결정사항을 명확하게 문서화해야 한다.

(50) wait 메서드는 반복문 안에서만 호출하라.

☞ 모든 wait 메서드는 항상 while 반복문 안에서만 호출해야 한다. 다르게 쓸 이유가 전혀 없다.

notify 메서드보다 notifyAll메서드를 쓰는 것이 좋지만, 성능이 심각하게 떨어지는 경우도 있다.

이 문제 때문에 notify 메서드를 쓸 때 쓰레드의 건전성을 보장하려면 엄청나게 주의해야 한다.

(51) 쓰레드 스케줄러에 의존하지 마라.

☞ 프로그램의 정확성을 쓰레드 스케줄러로 제어하려 하지 마라.

프로그램의 이식성과 강건성이 떨어진다. 쓰레드 스케줄러를 보조하는 Thread.yield 메서드나 스레드

우선순위도 믿지 마라. 이것들은 이미 제대로 동자가고 있는 기능의 질을 향상 시킬 때만 아주 드물게

써야 하는 것이지 제대로 동작하지 않는 프로그램을 “바로잡기” 위해 쓰는 것이 아니다.

(52) 쓰레드 안전성을 문서화하라.

☞ 모든 클래스는 자신의 쓰레드 안전성을 명확하게 문서화해야 한다.

안전성을 문서화하는 단 하나의 방법은 산문체로 조심스럽게 작성하는 것뿐이다.

synchronized 수정자는 어떤 클래스의 쓰레드 안전성을 문서화 하는데 아무런 도움도 안 된다.

그러나 상황에 따라 쓰레드-안전인 클래스의 명세문서에 일련의 메서드 호출을 하나의 처리 단위로

수행하려면 어떤 객체의 잠금장치를 얻어 동기화해야 하는지 반드시 밝혀야 한다. 쓰레드 안전성은

보통 클래스 문서화 주석에 기술하지만, 특별한 쓰레드 안전성 속성을 가진 메서드가 있다면

이 메서드의 문서화 주석에 이 속성을 기술해야 한다.

(53) 쓰레드 그룹을 쓰지 마라.

☞ 쓰레드 그룹은 원래 보안을 위해 격리된 애플릿에서 쓰려고 만든 것이다. 하지만, 스레드 그룹은

이 목적을 달성하지 못했고, 심지어는 자바2 플랫폼 보안 모델의 장래 계획에서도 쓰레드 그룹이

빠졌을 정도로 이제는 보안과 거리가 멀다. 쓰레드 그룹은 쓸만한 기능을 거의 제공하지 않고

그나마 하는 기능에도 심각한 결함이 있다. 쓰레드 그룹은 실패한 실험이다.

여러분은 쓰레드 그룹의 존재 자체를 잊어버리는 것이 좋다.

만약, 쓰레드를 묶어서 다루는 클래스를 작성해야 한다면, Thread 객체의 참조를 배열이나 컬렉션에

넣어 하나의 논리 그룹으로 처리하면 된다.

(ThreadGroup API 중에서 오직 하나 쓸만한 아주 사소한 기능이 있기는 하다.

쓰레드 그룹 안에 있는 쓰레드가 처리하지 않는 예외를 던지면, ThreadGroup.uncaughtException

메서드가 자동으로 호출된다. “실행환경”에서 처리하지 않는 예외에 적절하게 대응하기 위해

이 메서드를 쓴다. 현재, 표준구현은 스택의 내용을 표준 오류 스트림으로 출력하고 있는데,

만약 스택의 내용을 바로 프로그램의 로그 파일로 출력하고 싶다면 이 메서드를 재정의 하면 된다.)

9. 직렬화

(54) Serializable 인터페이스는 신중하게 implements 하라.

☞ 클래스 선언부에 implements Serializable을 붙이는 것은 굉장히 쉬워 보이지만 실제로 굉장히

많은 것을 고려해야 한다. 클래스를 한 번 쓰고 버릴 것이 아니라면, Serializable 인터페이스를

implements 할 것인지 매우 신중하게 결정해야 한다. 특히, 상속을 위해 설계한 클래스라면

좀 더 신중해야 한다. 이 클래스 자체는 Serializable 인터페이스를 implements 하지 않지만,

하위 클래스는 직렬화를 지원할 수 있게 하려면 접근할 수 있는 기본 생성자를 제공하면 된다.

이렇게 하면, 하위클래스는 아무 문제없이 Serializable 인터페이스를 implements 할 수 있다.

(55) 맞춤 직렬화 형태를 사용하라.

☞ (객체를 바이트 스트림으로 바꾸는 작업을 “직렬화(serialization)”라고 하고,

그 반대 작업을 “역직렬화(deserialization)”라고 한다.)

어떤 클래스가 직렬화를 제공하기로 했다면 어떤 직렬화 형태를 제공할 것인지 신중하게 생각해

보아야 한다. 기본 직렬화 형태는 클래스의 물리적인 필드와 논리적 구조가 같을 때만 써야 한다.

이런 경우가 아니라면, 해당 클래스를 가장 잘 설명할 수 있는 맞춤 직렬화 형태를 설계해야 한다.

외부에 제공하는 메서드를 설계하듯이 직렬화 형태도 많은 시간을 들여 신중하게 설계해야 한다.

일단 메서드를 외부에 제공하는 API에 공개하고 나면 함부로 뺄 수 없는 것처럼, 한번 직렬화 형태에

포함된 필드들도 함부로 뺄 수 없다. 직렬화 형태의 호환성을 지키려면 영원히 이 필드들을 지원해야

한다. 직렬화 형태를 한번 잘못 설계하면, 프로그램의 복잡성이 증가하고, 성능이 떨어지는 것과 같은

문제가 영원히 여러분을 괴롭힐 것이다.

(56) readObject 메서드는 모든 공격을 방어 할 수 있도록 작성하라.

☞ readObject 메서드는 바이트 스트림을 인자로 받는 public 생성자라고 생각해야 한다.

인자로 어떤 바이트 스트림이 넘어오더라도 반드시 유효한 인스턴스를 생성해야 한다.

인자로 어떤 바이트 스트림이 넘어오더라도 반드시 유효한 인스턴스를 생성해 내야 한다.

맞춤 직렬화 형태를 쓰는 모든 클래스의 readObject 메서드를 구현할 때, 인자로 넘어오는

바이트 스트림이 무조건 실제 인스턴스로부터 만들어진 직렬화 형태라고 믿지 마라.

readObject 메서드를 제대로 구현하는 방법은 다음과 같다.

￭ 객체 참조 필드가 외부에 공개되지 말아야 하는 클래스는 이 객체들을 방어복사 해야 한다.

불변 클래스를 구성하는 가변 객체 참조 필드들이 여기에 속한다.

￭ 불변규칙을 지켜야 하는 클래스라면, 반드시 불규칙을 검사해야 한다.

불변규칙을 어긴다면 InvalidObjectException을 던져야 한다.

이 검사는 반드시 방어복사가 끝난 다음에 해야 한다.

￭ 역직렬화의 결과로 얻은 객체 그래프의 유효성을 검사해야 한다면,

ObjectInput-Validation 인터페이스를 써야 한다.

(이 인터페이스에 대한 설명은 The JAVA Class Libraries, second edition, Volumne 1)

￭ 재정의 가능한 메소드를 호출하지 않는다.

(57) 필요하다면 readResolve 메서드를 제공하라.

☞ 싱글톤처럼 인스턴스의 개수를 제한해야 한다는 불변규칙이 있는 클래스는 readResolve 메서드를

제공해야 한다. readResolve 메서드 구현 패턴의 핵심사상은 사실상 public 생성자로 동작하던

readObject 메서드를 사실상 public 스태틱 팩토리 메서드로 동작하는 readResolve 메서드로

바꾼다는 것이다. 또, readResolve 메서드는 다른 패키지에서는 상속받을 수 없는 클래스의

readObject 메서드를 간단하게 대체 할 수 있다.