『Effective Java (3rd Edition)』에서 배우는 베스트 프랙티스
Joshua Bloch의 『Effective Java (3rd Edition) 』에서 발췌
1. null이 아닌 빈 컬렉션이나 빈 배열을 반환하라
메서드가 Set을 반환한다면, 결과가 비었을 때 return null; 하지 말고 대신 return Set.of(); 하세요.
2. 생성자 대신 정적 팩터리 메서드를 고려하라
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
항상 더 나은 것은 아니지만, 경우에 따라 여러 장점이 있습니다:
- 정적 팩터리 메서드의 장점 하나는 생성자와 달리 이름을 가질 수 있다는 것입니다.
- 호출될 때마다 새 객체를 만들 필요가 없습니다.
- 반환 타입의 어떤 하위 타입 객체든 반환할 수 있습니다.
- 입력 매개변수에 따라 호출할 때마다 다른 클래스의 객체를 반환할 수 있습니다.
- 메서드를 포함하는 클래스를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됩니다.
단점:
- 정적 팩터리 메서드만 제공할 때의 주된 제약은 public이나 protected 생성자가 없는 클래스는 상속할 수 없다는 것입니다.
- 정적 팩터리 메서드의 두 번째 단점은 프로그래머가 찾기 어렵다는 것입니다.
3. 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩터리와 생성자에는 공통된 제약이 있습니다. 선택적 매개변수가 많아지면 제대로 대응하기 어렵다는 점입니다. 제가 참여했던 몇몇 프로젝트는 이 문제로 아주 크게 고생했습니다.
4. private 생성자나 열거 타입으로 싱글턴임을 보장하라
인스턴스가 여러 개 필요 없는 것은 한 번만 생성해야 합니다. @ApplicationScoped를 쓰든, 다른 싱글턴 패턴을 쓰든 마찬가지입니다.
5. 인스턴스화를 막으려면 private 생성자를 사용하라
정적 메서드와 정적 필드만 모아 놓은 클래스를 만들고 싶을 때가 가끔 있습니다. 객체 지향적으로 생각하지 않으려고 이런 클래스를 남용하는 사람들이 있어서 평판이 좋지 않지만, 분명히 쓸모 있는 경우도 있습니다.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. 불필요한 객체 생성을 피하라
String s = new String("bikini"); // DON'T DO THIS!
또는:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
해설: 이 프로그램은 정답을 내놓지만, 글자 하나를 잘못 쓴 탓에 원래보다 훨씬 느립니다. 변수 sum이 long이 아니라 Long으로 선언되어 있어서, 프로그램이 불필요한 Long 인스턴스를 약 2^31개나 만들어 냅니다(long i를 Long sum에 더할 때마다 대략 하나씩).
7. finalizer와 cleaner 사용을 피하라
finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있으며, 일반적으로 불필요합니다. 사용하면 오동작, 낮은 성능, 이식성 문제가 생길 수 있습니다.
8. try-finally보다는 try-with-resources를 사용하라
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Comparable 구현을 고려하라
Comparable을 구현하면, 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션 구현체와 클래스가 함께 동작할 수 있게 됩니다.
10. 클래스와 멤버의 접근 권한을 최소화하라
잘 설계된 컴포넌트와 그렇지 않은 컴포넌트를 가르는 가장 중요한 요소는, 컴포넌트가 내부 데이터와 그 밖의 구현 세부 사항을 다른 컴포넌트로부터 얼마나 잘 숨기느냐입니다. 잘 설계된 컴포넌트는 모든 구현 세부 사항을 숨기고, API와 구현을 깔끔하게 분리합니다. 그러면 컴포넌트들은 오직 API를 통해서만 소통하고, 서로의 내부 동작에는 전혀 신경 쓰지 않습니다. 정보 은닉 또는 캡슐화라고 불리는 이 개념은 소프트웨어 설계의 근간이 되는 원리입니다.
11. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
가끔은 인스턴스 필드를 모아 놓는 것 말고는 아무 목적도 없는 퇴보한 클래스를 작성하고 싶어질 수도 있습니다:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
이런 클래스는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 누리지 못합니다. API를 바꾸지 않고는 내부 표현을 바꿀 수 없고, 불변식을 보장할 수 없으며, 필드에 접근할 때 부수 작업을 수행할 수도 없습니다. 철저한 객체 지향 프로그래머는 이런 클래스를 몹시 싫어하며, 항상 private 필드와 public 접근자 메서드(게터)를 가진 클래스로, 가변 클래스라면 변경자(세터)까지 갖춘 클래스로 대체해야 한다고 생각합니다:
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
public 클래스라면 확실히 이 강경파의 말이 옳습니다. 패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공해, 클래스의 내부 표현을 바꿀 수 있는 유연성을 지키세요. public 클래스가 데이터 필드를 노출하면, 클라이언트 코드가 어디에 얼마나 퍼져 있을지 모르기 때문에 내부 표현을 바꿀 희망은 완전히 사라집니다.
하지만 package-private 클래스나 private 중첩 클래스라면, 데이터 필드를 노출해도 본질적으로 문제될 것이 없습니다. 단, 그 필드들이 클래스가 표현하려는 추상 개념을 제대로 나타내고 있어야 합니다.
12. 변경 가능성을 최소화하라
불변 클래스란 간단히 말해 인스턴스를 수정할 수 없는 클래스입니다. 각 인스턴스가 가진 정보는 객체가 존재하는 동안 고정되어 있으므로, 어떤 변경도 관찰되지 않습니다. Java 플랫폼 라이브러리에는 String, 박싱된 기본 타입 클래스, BigInteger, BigDecimal 등 다양한 불변 클래스가 있습니다. 여기에는 그럴 만한 이유가 많습니다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 더 안전합니다. 클래스를 불변으로 만들려면 다음 다섯 가지 규칙을 따르세요:
- 객체의 상태를 변경하는 메서드를 제공하지 않는다(이런 메서드를 변경자라고 합니다).
-
클래스를 확장할 수 없도록 한다. 이렇게 하면 부주의하거나 악의적인 하위 클래스가 마치 객체의 상태가 바뀐 것처럼 동작해 클래스의 불변성을 깨뜨리는 일을 막을 수 있습니다. 상속을 막으려면 보통 클래스를
final로 선언하는 등의 방법을 씁니다. - 모든 필드를 final로 선언한다. 시스템이 강제하는 방식으로 의도를 명확히 드러낼 수 있습니다. 또한 메모리 모델에서 설명하듯, 새로 생성된 인스턴스에 대한 참조를 동기화 없이 다른 스레드로 건네도 올바르게 동작하도록 보장하는 데 필요합니다.
- 모든 필드를 private으로 선언한다. 필드가 참조하는 가변 객체에 클라이언트가 접근해 직접 수정하는 일을 막아 줍니다. 기술적으로는 불변 클래스가 기본 타입 값이나 불변 객체를 참조하는 public final 필드를 가져도 되지만, 다음 릴리스에서 내부 표현을 바꿀 수 없게 되므로 권장하지 않습니다.
-
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하는 필드가 있다면, 클라이언트가 그 객체의 참조를 얻을 수 없도록 해야 합니다. 이런 필드를 클라이언트가 제공한 객체 참조로 초기화하거나, 접근자가 그 필드를 그대로 반환하게 해서는 안 됩니다. 생성자, 접근자,
readObject메서드 모두에서 방어적 복사를 수행하세요.
13. 상속보다는 합성을 사용하라
상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선의 도구는 아닙니다. 잘못 사용하면 깨지기 쉬운 소프트웨어가 됩니다. 하위 클래스와 상위 클래스를 같은 프로그래머가 관리하는 패키지 안에서라면 상속도 안전합니다. 확장할 목적으로 설계되고 문서화도 잘 된 클래스를 확장할 때도 안전합니다. 하지만 패키지 경계를 넘어 일반적인 구체 클래스를 상속하는 것은 위험합니다.
메서드 호출과 달리 상속은 캡슐화를 깨뜨립니다. 새 메서드를 추가할 뿐 기존 메서드를 재정의하지 않는다면 클래스를 확장해도 안전하다고 생각할 수도 있습니다. 이런 확장이 훨씬 안전한 것은 맞지만, 위험이 전혀 없는 것은 아닙니다. 다음 릴리스에서 상위 클래스에 새 메서드가 추가됐는데, 운 나쁘게도 하위 클래스에 시그니처는 같고 반환 타입만 다른 메서드가 있다면 하위 클래스는 더 이상 컴파일되지 않습니다. 하위 클래스의 메서드가 새로운 상위 클래스 메서드와 시그니처도 반환 타입도 같다면, 이제 그 메서드를 재정의하는 셈이 되어 앞서 설명한 문제에 그대로 노출됩니다. 게다가 여러분의 메서드가 새 상위 클래스 메서드의 계약을 지킨다고 보장하기도 어렵습니다. 하위 클래스의 메서드를 작성할 때는 그 계약이 아직 쓰이지도 않았으니까요.
다행히 위에서 말한 문제를 모두 피해 가는 방법이 있습니다. 기존 클래스를 확장하는 대신, 새 클래스에 기존 클래스의 인스턴스를 참조하는 private 필드를 두는 것입니다. 기존 클래스가 새 클래스의 구성 요소로 쓰인다는 뜻에서 이 설계를 합성(composition)이라고 합니다. 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환합니다. 이를 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드라고 부릅니다. 그 결과 기존 클래스의 구현 방식에 전혀 의존하지 않는, 아주 견고한 클래스가 만들어집니다. 기존 클래스에 새 메서드가 추가되더라도 새 클래스에는 아무런 영향이 없습니다.
14. 추상 클래스보다는 인터페이스를 우선하라
Java에는 여러 구현을 허용하는 타입을 정의하는 메커니즘이 두 가지 있습니다. 인터페이스와 추상 클래스입니다. Java 8에서 인터페이스에 디폴트 메서드가 도입된 이후로는 두 메커니즘 모두 일부 인스턴스 메서드의 구현을 제공할 수 있습니다.
큰 차이는 추상 클래스가 정의한 타입을 구현하려면 그 추상 클래스의 하위 클래스가 되어야 한다는 점입니다. Java는 단일 상속만 허용하므로, 이 제약 때문에 추상 클래스를 타입 정의로 쓰기가 크게 제한됩니다. 반면 인터페이스는 필요한 메서드를 모두 정의하고 일반 규약을 지키기만 하면, 클래스 계층 구조의 어디에 있는 클래스든 구현할 수 있습니다. 기존 클래스에 새 인터페이스를 구현하도록 손보는 것도 쉽습니다.
- 인터페이스는 믹스인 정의에 안성맞춤입니다.
- 인터페이스로는 계층 구조가 없는 타입 프레임워크를 만들 수 있습니다.
- 인터페이스는 래퍼 클래스 관용구를 통해 기능을 안전하고 강력하게 확장할 수 있게 해 줍니다. 타입을 추상 클래스로 정의하면, 기능을 추가하고 싶은 프로그래머에게는 상속 말고는 선택지가 없습니다.
15. 제네릭: 로 타입은 사용하지 말라
오류는 런타임에야, 그것도 오류가 생기고 한참 지난 뒤에, 오류가 있는 코드와 멀리 떨어진 곳에서 발견될 수도 있습니다. ClassCastException이 발생하면 우표 컬렉션에 동전을 넣은 메서드 호출을 찾기 위해 코드베이스 전체를 뒤져야 합니다. 컴파일러는 도와주지 않습니다.
16. 제네릭: 비검사 경고를 제거하라
제네릭을 사용해 프로그래밍하면 컴파일러 경고를 많이 보게 됩니다:
- 비검사 형변환 경고
- 비검사 메서드 호출 경고
- 비검사 매개변수화 가변인수 타입 경고
- 비검사 변환 경고
제네릭에 익숙해질수록 경고는 줄어들지만, 새로 작성한 코드가 경고 하나 없이 깔끔하게 컴파일되리라고 기대하지는 마세요. 비검사 경고의 상당수는 쉽게 없앨 수 있습니다. 예를 들어 실수로 다음과 같이 선언했다고 해 봅시다:
Set<Lark> exaltation = new HashSet();
이것을 다음과 같이 고칩니다:
Set<Lark> exaltation = new HashSet<>();
어떤 경고는 없애기가 훨씬 어렵습니다. 고민이 필요한 경고를 만나더라도 끈질기게 맞서세요! 할 수 있는 한 모든 비검사 경고를 제거하세요.
경고를 모두 제거하면 코드가 타입 안전하다고 확신할 수 있습니다. 아주 좋은 일입니다. 런타임에 ClassCastException이 발생하지 않는다는 뜻이고, 프로그램이 의도한 대로 동작하리라는 확신도 커집니다. 경고를 없앨 수는 없지만 경고를 일으킨 코드가 타입 안전하다고 증명할 수 있다면, 그때만(오직 그때만) @SuppressWarnings("unchecked") 어노테이션으로 경고를 숨기세요. 코드가 타입 안전함을 먼저 증명하지 않고 경고를 숨기면, 잘못된 안도감을 갖게 될 뿐입니다. 코드는 경고 없이 컴파일되겠지만, 그래도 런타임에 ClassCastException을 던질 수 있습니다.
@SuppressWarnings("unchecked") 어노테이션을 쓸 때마다, 그렇게 해도 안전한 이유를 주석으로 남기세요.
17. 제네릭: 의미가 있을 때 쓰기
아야 – 제네릭이 절실히 필요한 클래스:
이런 건 늦어도 머지 리퀘스트 단계에서는 잡아내야 합니다!
18. 제네릭: 이렇게 하면 안 된다
죄송합니다, 할 말이 없네요. 이런 건 절대로 머지하지 마세요:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
클래스 선언 하나를 이해하는 데 5분이나 걸려서는 안 됩니다.
19. 배열보다는 리스트를 사용하라
배열과 제네릭 타입에는 중요한 차이가 두 가지 있습니다. 첫째, 배열은 공변입니다. 이 무시무시해 보이는 단어의 뜻은 간단합니다. Sub가 Super의 하위 타입이면, 배열 Sub[]도 배열 Super[]의 하위 타입이 된다는 것입니다. 반면 제네릭은 불공변입니다. 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아닙니다.
20. 익명 클래스보다는 람다를 사용하라
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
대신 이렇게 하세요:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. 지역 변수의 범위를 최소화하라
지역 변수의 범위를 줄이는 가장 강력한 기법은 가장 처음 쓰일 때 선언하는 것입니다...
22. 전통적인 for 문보다는 for-each 문을 사용하라
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. 라이브러리를 익히고 사용하라
0부터 어떤 상한값 사이의 무작위 정수를 생성하고 싶다고 해 봅시다. 이렇게 흔한 작업을 앞에 두면 많은 프로그래머가 다음과 같은 작은 메서드를 만들곤 합니다:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
이 메서드는 괜찮아 보이지만 세 가지 문제가 있습니다. 첫째, n이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복됩니다. 둘째, n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환됩니다. n이 크면 이 현상은 더 두드러집니다.
random 메서드의 세 번째 문제는 드물게 지정한 범위 바깥의 수를 반환하며 완전히 실패할 수 있다는 것입니다. 이 메서드가 rnd.nextInt()가 반환한 값을 Math.abs를 이용해 음수가 아닌 int로 매핑하기 때문입니다. nextInt()가 Integer.MIN_VALUE를 반환하면 Math.abs도 Integer.MIN_VALUE를 반환하고, n이 2의 제곱수가 아니라면 나머지 연산자(%)는 음수를 반환합니다. 이는 거의 틀림없이 프로그램의 실패로 이어지며, 그 실패는 재현하기도 어려울 수 있습니다.
24. 정확한 답이 필요하다면 float와 double은 피하라
금액에는 BigDecimal을 사용하세요. float와 double은 반올림 오류를 일으킵니다.
예를 들어 주머니에 1.03달러가 있는데 42센트를 썼다고 해 봅시다. 남은 돈은 얼마일까요? 이 질문에 답하려는 순진한 프로그램 코드는 다음과 같습니다:
System.out.println(1.03 - 0.42);
안타깝게도 이 코드는 0.6100000000000001을 출력합니다. 이건 특별한 경우가 아닙니다. 주머니에 1달러가 있고, 개당 10센트짜리 와셔 9개를 샀다고 해 봅시다. 거스름돈은 얼마일까요?
System.out.println(1.00 - 9 * 0.10);
이 프로그램 코드에 따르면 거스름돈은 $0.09999999999999998입니다.
결과를 출력하기 전에 반올림하면 해결될 거라고 생각할 수도 있지만, 안타깝게도 항상 그렇지는 않습니다. 예를 들어 주머니에 1달러가 있고, 선반에 10센트, 20센트, 30센트, … 이렇게 1달러까지 가격이 매겨진 맛있는 사탕이 줄지어 있다고 해 봅시다. 10센트짜리부터 하나씩 사 나가다가, 선반의 다음 사탕을 살 돈이 없을 때까지 계속합니다.
25. 박싱된 기본 타입보다는 기본 타입을 사용하라
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. 객체는 인터페이스를 사용해 참조하라
좋은 예:
Set<Son> sonSet = new LinkedHashSet<>();
나쁜 예:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. 리플렉션보다는 인터페이스를 사용하라
그렇지 않으면:
- 예외 검사를 포함해 컴파일타임 타입 검사가 주는 이점을 모두 잃습니다. 프로그램이 존재하지 않거나 접근할 수 없는 메서드를 리플렉션으로 호출하려 하면, 미리 대비해 두지 않은 한 런타임에 실패합니다.
- 리플렉션으로 접근하는 코드는 지저분하고 장황합니다. 작성하기도 지루하고 읽기도 어렵습니다.
- 성능이 떨어집니다. 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느립니다.
28. 일반적으로 통용되는 명명 규칙을 따르라
Java 플랫폼에는 잘 정립된 명명 규칙이 있으며, 그 대부분은 자바 언어 명세(The Java Language Specification) [JLS, 6.1]에 담겨 있습니다.
29. 예외는 진짜 예외 상황에만 사용하라
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
이 코드는 무슨 일을 할까요? 들여다봐서는 전혀 알 수 없습니다. 그것만으로도 이 코드를 쓰지 말아야 할 이유는 충분합니다. 알고 보면 이 코드는 배열의 원소를 순회하는, 아주 잘못된 관용구입니다. 무한 루프를 돌다가 배열의 범위를 벗어난 첫 번째 원소에 접근하려는 순간 ArrayIndexOutOfBoundsException을 던지고, 이를 잡아서 무시하는 방식으로 끝납니다. 원래는 배열을 순회하는 표준 관용구와 같은 일을 하려던 것인데, 표준 관용구라면 Java 프로그래머 누구나 한눈에 알아봅니다:
for (Mountain m : range) {
m.climb();
}
예외는 이름 그대로 예외적인 상황에서만 써야 합니다. 일상적인 제어 흐름에 써서는 절대 안 됩니다.
30. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
호출하는 쪽에서 복구할 것이라고 합리적으로 기대할 수 있는 상황이라면 검사 예외를 사용하세요. 검사 예외를 던져 사용자에게 들이밀면, API 설계자는 그 상황에서 복구하라고 요구하는 셈입니다. 사용자는 예외를 잡고 무시하는 방식으로 이 요구를 무시할 수도 있지만, 대개는 좋지 않은 생각입니다.
프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하세요. 런타임 예외의 대부분은 전제 조건을 위배했다는 뜻입니다. 전제 조건 위배란 단순히 API의 클라이언트가 API 명세에 기록된 계약을 지키지 않았다는 것입니다. 예를 들어 배열 접근의 계약에 따르면 배열의 인덱스는 0 이상, 배열 길이 −1 이하여야 합니다. ArrayIndexOutOfBoundsException은 이 전제 조건이 위배되었음을 나타냅니다.
31. 필요 없는 검사 예외 사용은 피하라
정리하자면, 검사 예외는 꼭 필요한 곳에만 쓰면 프로그램의 안정성을 높여 주지만, 남용하면 쓰기 고통스러운 API가 됩니다.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. 표준 예외를 사용하라
숙련된 프로그래머와 그렇지 않은 프로그래머를 구분하는 특징 하나는, 숙련된 프로그래머일수록 높은 수준의 코드 재사용을 추구하고 대개 이를 이뤄 낸다는 점입니다.
코드는 재사용하는 것이 좋다는 규칙에서 예외도 예외는 아닙니다. Java 라이브러리는 대부분의 API에서 쓰기에 충분한 예외들을 제공합니다. 표준 예외를 재사용하면 여러 이점이 있습니다. 그중 최고는 프로그래머에게 이미 익숙한 규약을 그대로 따르기 때문에 API를 배우고 사용하기 쉬워진다는 것입니다. 그다음은 낯선 예외가 뒤섞여 있지 않아 API를 사용하는 프로그램도 읽기 쉬워진다는 것입니다. 마지막으로(그리고 가장 사소하게), 예외 클래스가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸립니다.
가장 많이 재사용되는 예외는 IllegalArgumentException입니다. 이 예외는 일반적으로 호출자가 인수로 부적절한 값을 넘길 때 던집니다. 예를 들어 어떤 동작을 반복할 횟수를 나타내는 매개변수에 호출자가 음수를 건넸을 때 던지면 됩니다. 또 하나 자주 재사용되는 예외는 IllegalStateException입니다. 이 예외는 일반적으로 대상 객체의 상태가 호출된 메서드를 수행하기에 적합하지 않을 때 던집니다. 예를 들어 제대로 초기화되지 않은 객체를 호출자가 사용하려 할 때 던지면 됩니다.
메서드 호출이 잘못됐다면 결국은 모두 인수 아니면 상태가 잘못된 것이라고 볼 수도 있지만, 특정한 종류의 잘못된 인수와 상태에는 다른 예외를 쓰는 것이 표준입니다. null 값을 허용하지 않는 매개변수에 호출자가 null을 건네면, 관례상 IllegalArgumentException이 아니라 NullPointerException을 던집니다. 마찬가지로 시퀀스의 인덱스를 나타내는 매개변수에 범위를 벗어난 값을 건넸다면 IllegalArgumentException이 아니라 IndexOutOfBoundsException을 던져야 합니다.
Exception, RuntimeException, Throwable, Error는 직접 재사용하지 마세요. 이 클래스들은 추상 클래스라고 생각하세요. 다만 검사 예외를 런타임 예외로 바꾸고 싶고, 자신이 무엇을 하는지 정말로 잘 알고 있다면 RuntimeException을 써도 됩니다.
33. 예외를 무시하지 말라
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
비어 있는 catch 블록은 예외의 존재 이유를 없애 버립니다. 예외는 예외적인 상황에 반드시 대처하도록 강제하기 위해 존재합니다. 예외를 무시하는 것은 화재 경보를 무시하는 것과 같습니다. 그것도 경보를 꺼 버려서, 다른 누구도 진짜 불이 났는지 확인할 기회조차 얻지 못하게 하는 것입니다.