IT_Programming/Java

Effective JAVA™ Programming Language Guide - 2

JJun ™ 2007. 2. 15. 12:45
LONG

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 메서드를 간단하게 대체 할 수 있다.

ARTICLE  

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) 예외를 잡아서 버리지 마라.

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

     예외가 예측 가능한 상황을 표현하든, 프로그래밍 오류를 표현하든 예외를 잡아서

     처리하지 않고 버리면 프로그램은 오류가 발생해도 아무 일 없듯이 잘 진행되는 것처럼

     보인다. 그러다가, 문제의 근원과 전혀 상관없는 곳에서 오류를 내며 프로그램이

     죽어버릴지도 모른다. 예외를 적절히 처리한다면 프로그램 실패를 완전히 방지 할 수도 있다.

     처리하지 않는 예외를 단지 외부로 전파시키기만 해도 적어도 실패의 원인을 디버깅할 수

     있는 충분한 정보를 제공하면서 프로그램을 재빨리 끝낼 수 있지만, 잡아서 버리면

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