Item76. 가능한 한 실패 원자적으로 만들라

실패 원자성 (failure atomic)

  • 호출된 method가 실패하더라도 해당 객체는 method 호출 전 상태를 유지하면 호출자가 오류 상태를 복구할 수 있다. 이런 특성을 실패 원자적(failure-atomic)이라고한다.

method를 실패 원자적으로 만드는 방법

  1. 불변 객체로 설계 : 불변 객체는 생성 시점에 상태가 고정된다

  2. 가변 객체의 경우 method 실행전에 매개변수의 유효성을 검사한다. 객체 내부 상태를 변경하기전에 잠재적 예외 가능성을 대부분 걸러낼 수 있는 방법이다.

1
2
3
4
5
6
7
8
9
10

public Object pop(){
// 유효성검사
if(size == 0 ){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size]=null; // 다 쓴 참조 해제
return result;
}

이와 비슷하게 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법도 있다.

예를 들면 TreeMap class 는 원소들을 특정 기준으로 정렬할 수 있으며 기본적으로 원소 객체들이 정렬 가능한 타입이여야 한다. 따라서 TreeMap class를 변경하기 전에 해당 원소가 들어갈 위치를 찾는과정에서 ClassCastException이 터진다.

  1. 객체의 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교체하는 방법

예를 들면 정렬 method에서 정렬을 수행하기 전에 입력 리스트를 임시배열로 옮겨 담는다. 혹시나 정렬이 실패하더라도 입력 리스트 자체는 변경되지 않는다.

  1. 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성해 작업 전 상태로 되돌리는 방법
Read more

Item75. 예외의 상세 메시지에 실패 관련 정보를 담으라.

  • 예외를 잡지 못해 프로그램이 실패하면 예외의 stack trace 정보를 자동으로 출력한다.
  • stack trace 정보는 예외 객체의 toString method를 호출해 얻는 문자열로 보통 예외 클래스 이름 뒤에 상세 메시지가 붙는 형태이다. 따라서 최대한 예외의 toString method에 실패 원인에 관한 정보를 가능한 많이 담아 반환하는게 좋다.

  • 즉 실패 순간의 상황에 관련된 객체의 상태, 매개변수 등을 담아서 출력해주는게 실패 원인을 분석할떄 좋다.

  • 당연한 말이지만 최종 사용자(고객)에게 보여줄 오류 메시지는 친절한 안내 메시지로 가독성이 중요한 반면 , 예외에는 가독성이 다소 떨어지더라도 실패 원인을 분석할때 유용한 정보들을 포함시켜 보여주는게 중요하다.

  • 실패를 적절히 포착하려면 필요한 정보를 예외 생성자에서 모두 받아서 상세 메시지까지 미리 생성해두는 방법도 괜찮다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public IndexOutOfBoundsException(int lowerBound,int upperBound,int index){

// 실패를 포착하는 상세 메시지를 생성

super(String.format(
"최솟값 : %d , 최댓값: %d , 인덱스 : %d" ,
lowerBound , upperBound , index ));

// 프로그램에서 이용할 수 있도록 실패 정보를 저장해둔다.
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.index = index;
}

이렇게 실패 상황의 정보를 예외 클래스에 담아두면 예외 클래스 사용자가 메시지를 만드는 작업을 중복하지 않아도 된다는 장점을 가지고 있다.

또한 예외는 실패와 관련된 정보를 얻을 수 있는 접근자 method를 적절히 제공하는게 좋다.

1
2
3
getLowerBound();
getUpperBound();
// 실패 상황시 필드정보 접근자 method
Read more

Item74. method가 던지는 모든 예외를 문서화하라.

  • method가 던지는 검사 예외는 그 method를 올바로 사용하는데 중요한 정보이므로, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.

  • 공통 상위 클래스(ex)Exception,Throwable) 하나로 문서화하는 경우, method 사용자에게 예외에 대처할 수 있는 힌트를 주지 못하며 다른 예외들까지 삼켜버릴 수 있어 API 사용성을 크게 떨어트린다.

  • 비검사 예외도 검사예외처럼 문서화하면 개발자가 해당 오류가 나지 않도록 코딩하게 유도하므로 좋은 습관이다. 다만 비검사 예외는 method 선언의 @throws 목록에 넣지 말아야 한다.

  • 인터페이스 method의 경우 특히 발생 가능한 비검사 예외를 문서화해두는 것이 하위 구현체가 일관되게 동작하도록 하는데 중요하다

  • 한 class에 정의된 많은 method가 같은 이유로 같은 예외를 던진다면 class 레벨에 문서화해두는 방법도 있다.

Item73. 추상화 수준에 맞는 예외를 던지라

예외 번역 (Exception Translation)

  • method가 저수준 예외를 처리하지 않고, 바깥으로 예외를 전파시킬 때 내부 구현 방식을 드러내어, 윗 레벨 API를 오염시킨다.
    다음 릴리스에서 구현 방식을 바꾸면 기존 client 프로그램을 깨지게 만들수도 있다.

  • 이 문제를 피하려면 상위계층에서는 저수준 예외를 잡아, 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 하는데 이를 예외 번역이라고 한다.

1
2
3
4
5
try{
}catch(LowerLevelException e){
//추상화 수준에 맞게 예외를 잡아 번역한다.
throw new HigherLevelException(...);
}

예외 번역의 예시로서, AbstractSequentialList 의 get method는 List 인터페이스의 get method 명세에 명시된 예외로 번역한다

1
2
3
4
5
6
7
8
public E get(int idx){
ListIterator<E> i = listIterator(idx);
try{
return i.next();
}catch(NoSuchElementException e){
throw new IndexOutOfBoundsException("인덱스: " + index);
}
}
Read more

Item 72. 표준 예외를 사용하라

표준 예외를 재사용함으로서 얻는 장점

  1. 많은 개발자가 이미 익숙해진 예외를 그대로 사용함으로서, 다른 개발자가 API를 사용하기 편해진다.
  2. 예외 클래스 수가 적을수록 메모리 사용량도 줄어들고, 클래스를 적재하는 시간도 적게 걸린다.

가장 많이 사용되는 예외 예시

  1. IllegalArgumentException : 호출자가 인수로 부적절한 값을 넘길 때, 던지는 예외
  2. IllegalStateException : 대상 객체의 상태가 호출된 메소드를 수행하기에 적합하지 않을 때 주로 던진다. ex) 초기화되지 않은 객체
  3. ConcurrentModificationException : 단일스레드에서 사용하려고 설계한 객체를 여러 스레드가 동시에 수정하려 할 때 던진다.
  4. UnsupportedOperationException : client가 요청한 동작을 대상 객체가 지원하지 않을때 던진다. ex) 삼입만 가능한 List구현체에 remove method호출
  • Exception, RuntimeException , Throwable, Error class의 경우 여러 예외들을 포괄하는 클래스임으로 직접 재사용하는 것을 지양하자.

  • IllegalStateException 과 IllegalArgumentException 중에 어떤 표준 예외를 쓸지 선택하기 어렵다면, 인수값이 무엇이든 실패한 경우는 IllegalStateException , 그렇지 않은 경우는 IllegalArgumentException을 던지자.

Read more

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

  • 검사 예외를 제대로 활용하면 API를 사용하는 client가 예외에서 발생한 문제를 처리하여 프로그램의 안정성을 높일 수 있으나, 불필요한 검사 예외는 프로그램 복잡성만 늘린다.

예를 들면 다음과 같이 검사 예외를 던졌는데 client가 아래와 같이 처리해야 된다면 오히려 코드만 불필요하게 길게 만든다.

1
2
3
catch(TheCheckedException e){
throw new AssertionError();
}
1
2
3
4
catch(TheCheckedException e){
e.printStackTrace();
System.exit(1);
}
  • 그외에 단점으로 검사 예외를 던지는 method는 Stream 안에서 직접 사용할 수 없다.
Read more

Item70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라

-Java는 문제 상황을 알리는 타입 (Throwable)으로 3가지 ( 검사 예외, 런타임 예외 , 에러 ) 를 제공한다.

(ref- https://butter-shower.tistory.com/87 )

  • RuntimeException은 예외처리를 하지 않아도 컴파일 가능한 비검사형 예외이며, 검사형 예외(ex) IllegalAccessException)는 예외 처리를 하지 않으면 컴파일 오류가 터지므로 꼭 처리해주어야 하는 예외이다.

  • Error class도 비검사형 예외에 속하며, Exception class 와 차이점은 시스템레벨에서 비정상적인 상황이 발생한 심각한 수준의 오류라고 한다. 반면 Exception은 개발자가 구현한 로직에서 발생한 오류이다.

Read more

Item69. 예외는 진짜 예외 상황에만 사용하라

다음과 같이 예외를 예외 상황이 아닌 경우에 사용하는 것을 지양하라고 언급하고 있다.

1
2
3
4
5
6
7
8
9
10
// 굳이 예외를 써서 루프를 종료 
try{
int i = 0;
while(true){
range[i++].climb();
}
}
catch(ArrayIndexOutOfBoundException e){
//
}

위와 같이 예외가 아닌 상황에 사용하게 되면 가독성이 떨어지는 것은 물론 다음과 같은 단점들도 있다고 한다.

  1. 예외는 예외 상황에 쓰도록 설계되었으므로, JVM 구현체에서 명확한 검사만큼 최적화되지 않았을 가능성이 크다.
  2. 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
  3. 향상된 for문으로 사용하면 중복검사를 알아서 JVM이 최적화함으로 굳이 위처럼 예외처리 할필요가 없다.
1
2
3
4
// 표준 배열 순회 관용구 
for(Mountain m : range ){
m.climb();
}
  1. 디버깅을 어렵게 만든다. 다른 반복문이 ArrayIndexOutOfBoundException 을 터트린다고 가정하였을때, 첫번쨰 잘못된 예시는 이를 반복문 종료 상황으로 판단한다.
Read more

Item68. 일반적으로 통용되는 명명 규칙을 따르라.

Java 명명규칙은 철자와 문법 범주로 나뉜다.

철자 규칙

  • 패키지,클래스,인터페이스,메소드,필드,타입 변수의 이름을 다룬다.
  • 프로젝트 생성시 패키지명은 조직의 인터넷 도메인 이름을 역순으로 사용하는게 관례이다.

ex) Google : com.google , Naver : com.naver

  • 나머지 패키지 이름은 해당 패키지를 설명하는 하나이상의 요소로 이루어진다.이떄 각 요소는 8자 이하의 짧은 단어로 구성한다.

ex) com.naver.cafe

  • 많은 기능을 가진 경우 붙여할 요소가 많다면 계층적으로 이름을 짜는것도 좋다

ex) java.util.concurrent.atomic

  • class와 인터페이스의 이름의 경우 단어의 첫글자는 대문자로 시작한다. ex) List

  • method와 필드 변수명은 첫글자만 소문자로 하고, 나머지 단어의 첫글자는 대문자로 쓴다. ex) phoneNumber

  • 값이 불변인 static final

  • 상수필드는 예외적으로 모두 대문자를 사용하고, 단어사이는 밑줄로 구분한다. ex) NEGATIVE_INFINITY

  • 타입 매개변수는 보통 한 문자로 표현한다.

  • 대표적으로 관례상 많이 사용하는 타입매개변수들은 다음과 같다.

    T : 임의의 타입
    E : Collection 원소의 타입
    K , V : Map의 키와 값
    x : 예외
    R : 메소드 반환 타입

문법 규칙

  • 객체를 생성할 수 있는 클래스는 보통 단수 명사 혹은 명사구를 사용 ex) Thread
  • 객체를 생성할 수 없는 클래스는 보통 복수형 명사 사용 ex) Collectors, Collections
  • 인터페이스의 이름은 클래스와 똑같이 짓거나, able로 끝나는 형용사를 사용 ex) Runnable
  • Annotation의 경우 명확한 규칙은 없다.
  • 동작을 수행하는 method의 이름은 동사로, boolean값을 반환하는 method의 이름은 is 또는 has로 시작한다. ex) isPrime,hasSiblings
  • 해당 인스턴스의 속성을 반환하는 method는 get으로 시작하는 동사구로 짓거나, 명사로 짓는다. ex) getSize , size

그 외에 타입 변환과 관련된 특별한 method 이름 규칙은 다음과 같다.

  • toType 형태 (toArray,toList) : 객체의 타입을 다른 타입의 객체로 반환하는 method 명
  • asType 형태 (asList) : 객체의 내용을 다른 뷰로 보여주는 method명
  • typeValue 형태 (intValue) : 객체의 값을 기본 타입값으로 반환하는 method명

Item67. 최적화는 신중히 하라

  • 성능떄문에 아키텍쳐 구조를 희생하면 안된다.좋은 아키텍쳐를 가지고 있는 경우 각 모듈이 정보 은닉 원칙을 따르므로, 개별 모듈을 독립적으로 재설계함으로, 성능을 향상시킬 수 있다.

  • 성능 결함은 모듈의 구현 로직을 바꿔서 나중에 최적화할 수 있지만, 전체 아키텍쳐 구조가 성능을 제한하는 상황은 시스템 전체를 다시 작성해야 하는 단점이 있다.

Read more