요약

예외 발생 원인과 관련된 정보를 필요한 것만 자세히 담아서 알려주도록 하자.

  • 스택 추적에 꼭 넣어야 하는 정보
    • 예외가 발생한 파일 이름/줄번호
    • 스택에서 호출한 다른 메서드들의 파일 이름/줄번호

 

스택 추적(Stack Trace)

  • 스택 추적은 예외 객체의 toString 메서드를 호출해 얻는 문자열
  • 보통은 예외의 클래스 이름 뒤에 상세 메시지가 붙는 형태
  • 실패 원인을 분석하기 위해 얻을 수 있는 유일한 정보

→ 예외의 toString 메서드에 실패 원인에 관한 정보를 꼭 필요한 것만 자세하게 예외의 상세 메시지에 담는 것이 중요하다.

스택 추적에 넣어야하는 정보

  • 예외가 발생한 파일 이름/줄번호
  • 스택에서 호출한 다른 메서드들의 파일 이름/줄번호

 

예시 - IndexOutOfBoundsException

IndexOutOfBoundsException의 상세 메시지는 범위의 최솟값/최댓값, 그 범위를 벗어났다는 인덱스 값을 담아야 한다.

예외 발생하는 상황

  • 인덱스 < 최솟값
  • 인덱스 ≥ 최댓값
  • 최솟값 > 최댓값 (내부 불변식이 심각히 깨진 경우)

위 현상들의 원인은 모두 다르므로, 최솟값/최댓값/인덱스 값 정보를 제공하면 원인을 분석하는데 도움이 될 것이다.

 

예외 생성자에서 상세 메시지 미리 생성하기

실패를 적절하게 포착하는 방법으로 예외 생성자에서 상세 메시지를 미리 생성하는 방법도 있다.

  • 예시 코드
    • 이 코드는 상세 메시지를 만들어내는 코드를 예외 클래스 안으로 모아주는 효과도 있어 클래스 사용자가 메시지를 만드는 작업을 중복하지 않아도 되는 효과도 있다
    public class IndexOutOfBoundsException extends RuntimeException {
        private int lowerBound;
        private int upperBound;
        private int index;
    
        /**
         * IndexOutOfBoundsException을 생성한다. 
         * 
         * @param lowerBound 인덱스의 최솟값
         * @param upperBound 인덱스의 최댓값 + 1
         * @param index 인덱스의 실제값
         */
        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;
        }
    }
    
    • 길이가 5인 배열, arr[5]; → 예외 발생
    • 예외 메시지 : “최솟값 : 0, 최댓값 : 4, 인덱스 : 5”
  • 참고
    • 자바 9에서 IndexOutOfBoundsException의 생성자는 정수 인덱스를 받는다
    public IndexOutOfBoundsException(String  s){ }
    

요약

되도록이면 표준 예외를 재사용 하자. 이유는

  1. 내가 작성한 API가 다른 사람이 익히고 사용하기 쉬워진다
  2. 코드를 읽기 쉬워진다
  3. 표준 예외를 사용하면 예외 클래스 수가 적어지므로, 메모리 사용량과 클래스 적재 시간이 감소된다

 

표준 예외를 사용하자

 

이점

표준 예외를 사용하면 다음과 같은 이점이 있다.

  1. 내가 작성한 API가 다른 사람이 익히고 사용하기 쉬워진다
  2. 코드를 읽기 쉬워진다
  3. 표준 예외를 사용하면 예외 클래스 수가 적어지므로, 메모리 사용량과 클래스 적재 시간이 감소된다

 

주의할 점

  1. API 문서를 참고해 상황과 맥락에 맞는 예외를 선택해 재사용하자
  2. Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말자
    1. 위 클래스들은 여러 성격의 예외들을 포괄하는 클래스이므로, 안정적으로 테스트할 수 없기 때문이다
  3. 더 많은 정보를 제공하길 원한다면 표준 예외를 확장가능하지만, 예외는 직렬화할 수 있어 많은 부담이 따른다
    1. 따라서, 되도록이면 커스텀 예외는 만들지 않도록 하자

 

표준 예외 재사용 예시

  1. IllegalArgumentException
    1. 허용하지 않는 값이 인수가 건네졌을 때 사용하는 예외
    2. null로 넘어온 것은 NullPointerException으로 따로 처리한다
  2. IllegalStateException
    1. 대상 객체의 상태가 호출된 메서드를 수행하기 적합하지 않을 때 사용하는 예외
    2. 주로 초기화되지 않은 객체를 사용하려 할 때 던질 수 있다
  3. NullPointerException
    1. 메서드 인수로 null 값이 넘어왔을 때 사용
  4. IndexOutOfBoundsException
    1. 어떤 시퀀스의 허용 범위를 넘는 값을 건넬 때 사용하는 예외
  5. ConcurrentModificationException
    1. 단일 스레드에서 사용하려고 설계한 객체를 여러 스레드가 동시에 수정하려 할 때 사용하는 예외
    2. 동시 수정을 확실히 검출할 수 있는 안정된 방법은 없으니, 문제가 생길 가능성을 알려주는 정도의 역할
  6. UnSupportedOperationException
    1. 클라이언트가 요청한 동작을 대상 객체가 지원하지 않을 때 사용하는 예외
    2. 예시) List 구현체에 remove() 호출 할 때 UnSupportedOperationException이 발생

0. 요약

자바에서는 문제 상황을 알리는 타입(throwable)으로 검사 예외, 런타임 예외, 에러 를 제공한다. 각각 어떤 상황에 사용해야하는지는 다음과 같다.

  • 검사 예외 → 복구할 수 있는 상황, 자원 고갈 상황 복구 가능
  • 비검사 예외 → 프로그래밍 오류, 자원 고갈 상황 복구 불가능
  • 만약 복구여부를 확실하게 알 수 없다면 비검사 예외를 던지자

 

1. 검사 예외를 사용해야하는 상황

만약 호출하는 쪽에서 복구할 수 있는 상황이라고 여겨지면 검사 예외를 사용하는 것이 좋다.

  • 검사 예외 메서드를 사용하면 사용자 측에서 예외 처리 방법
    • catch로 잡아서 처리하기
    • 더 바깥으로 전파하도록 강제하기
  • 그 메서드를 호출했을 때 발생할 수 있는 유력한 결과임을 API사용자에게 알려주고, 해결하라고 요구

 

2. 런타임 예외 vs 에러

둘 다 비검사 throwable에 해당하고, 동작방식은 다음과 같다.

  1. 프로그램에서 잡을 필요가 없는 경우
  2. 통상적으로 잡지 말아야하는 경우

프로그램에서 비검사 예외나 에러를 던졌다는 것은 복구가 불가능하거나 더 실행해봐야 득보다는 실이 많다는 뜻이다.

 

3. 비검사 예외(런타임 예외)를 사용해야하는 상황

프로그래밍 오류를 나타낼 때는 비검사 예외를 사용하자. 런타임 예외의 대부분은 전제조건을 위배했을 때 발생하기 때문에 복구할 필요가 없다.

검사 예외 vs 비검사 예외

Q: 만약 복구할 수 있는 상황인지 아닌지 판단하기 애매하다면 어떻게 해야할까?

A : 보통 복구할 수 있는 상황 여부를 판단하기가 어려울 수 있다. 예를 들어 자원 고갈 문제가 매우 큰 배열을 할당해 생긴 프로그래밍 오류일 수도 있고, 진짜로 자원이 부족해서 발생한 문제일 수도 있다. 따라서 API 설계자가 상황을 판단해 복구 가능하다면 검사 예외를, 그렇지 않다면 런타임 예외를 사용하는 것이 좋다. 확신하기 어렵다면 비검사 예외를 선택하는 편이 낫다.

 

4. 에러를 사용해야하는 상황

에러는 보통 JVM이 자원 부족, 불변식 깨짐 등 더이상 수행을 계속할 수 없는 상황을 나타낼 때 사용한다.

  • 되도록이면 하지 말아야 할 것들
    • Error 클래스를 상속한 하위 클래스 생성은 하지 말자
    • Error를 throw 문으로 직접 던지는 것도 하지 말자

 

5. 커스텀 throwable은 정의하지도 말자

Exception, RuntimeException, Error를 상속하지 않는 throwable을 만들 수 있기는 하지만, 만들지 말자.

  • 이유
    • 정상적인 검사 예외보다 나을게 하나도 없다
    • API 사용자만 헷갈리게 할 뿐이다

예외의 메서드

예외의 메서드는 주로 그 예외를 일으킨 상황에 관한 정보를 코드 형태로 전달하는데 쓰인다.

이런 메서드가 없다면 프로그래머들은 오류 메시지를 파싱해 정보를 가져와야 하는데, 이 방식에는 다음과 같은 문제점이 있다.

  • 포맷이 버전별로 다를 수 있기에 데이터가 쉽게 깨지기 쉽다
  • 특정 환경에서의 의존도가 높으므로 다른 환경에서 동작하지 않을 가능성이 높다

요약

리플렉션은 복잡한 특수 시스템을 개발할 때 필요한 강력한 기능이지만, 단점이 많기에 신중하게 사용해야 한다.

컴파일타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성할 때는 리플렉션 사용을 권장한다.

하지만 되도록이면 객체 생성에만 사용하는 것이 좋고, 인스턴스 생성 후에는 적절한 인터페이스나 컴파일타임에 알 수 있는 상위 클래스로 형변환 후 사용을 권장한다.

 

→ 런타임에 리플렉션을 사용해야하는 경우가 있고, 리플렉션으로 생성한 객체는 인터페이스로 받아서(컴파일타입 체크가능) 사용하자!(리플렉션 장점 + 인터페이스 장점)

 

객체 생성은 리플렉션으로, 객체 사용은 인터페이스로

 

리플렉션

리플렉션 기능(java.lang.reflect)을 이용하면 다음 기능을 사용할 수 있다.

  1. 임의의 클래스에 접근이 가능하다
  2. 클래스의 생성자/메서드/필드의 인스턴스를 가져올 수 있다
  3. 인스턴스를 이용해 각각에 연결된 실제 생성자/메서드/필드 조작이 가능하다
    1. 클래스 인스턴스 생성 or 메서드 호출 or 필드 접근이 가능하다는 것을 의미한다
  4. 주의해야할 점은 리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다
    1. 리플렉션은 인스턴스 생성에만 사용하고,
    2. 생성한 인스턴스는 인터페이스나 상위클래스로 참조해 사용하는 것을 권장한다

 

리플렉션

장점

  1. 복잡한 애플리케이션에서 사용 가능
    1. 코드 분석 도구
    2. 의존관계 주입 프레임워크
  2. 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다
    1. 예시로, 버전이 여러 개인 외부 패키지를 다룰 때 유용
    2. 주로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메서드 등은 리플렉션으로 접근하는 방식
    3. 이러기 위해선 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 반드시 감안해야 한다

단점

리플렉션의 단점은 다음과 같다.

  1. 컴파일 타입 검사가 주는 이점을 하나도 누릴 수 없다
  2. 코드가 지저분하고 장황해진다
  3. 성능이 떨어진다
    1. 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.

예시

아래 코드는 다음과 같은 형식으로 진행된다.

  • 리플렉션으로 Set<String> 인터페이스의 인스턴스를 생성한다
  • 정확한 클래스는 명령줄의 첫 번째 인수로 확정한다
  • 그 다음에는 생성한 집합에 두 번째 이후의 인수들을 추가후 화면에 출력한다
    • 출력은 인수 중복 제거 후 진행된다
    • 첫번째 인수로 지정한 클래스가 무엇이냐에 따라 인수 출력 순서가 달라진다
    • HashSet인 경우 무작위로 출력되고, TreeSet인 경우 알파벳 순서로 출력된다
// 리플렉션으로 인스턴스를 생성하고 인터페이스로 참조해 활용한다.
public static void main(String[] args) {
    
    // 클래스 이름을 Class 객체로 변환
    Class<? extends Set<String>> cl = null;
    try {
        cl = (Class<? extends Set<String>>) Class.forName(args[0]);
    } catch (ClassNotFoundException e) {
        fatalError("클래스를 찾을 수 없습니다.");
    }
    
    // 생성자를 얻는다.
    Constructor<? extends Set<String>> cons = null;
    try {
        cons = cl.getDeclaredConstructor();
    } catch (NoSuchMethodException e) {
        fatalError("매개변수 없는 생성자를 칮을 수 없습니다.");
    }
    
    // 집합의 인스턴스를 만든다.
    Set<String> s = null;
    try {
        s = cons.newInstance();
    } catch (IllegalAccessException e) {
        fatalError("생성자에 접근할 수 없습니다.");
    } catch (InstantiationException e) {
        fatalError("클래스를 인스턴스화할 수 없습니다.");
    } catch (InvocationTargetException e) {
        fatalError("생성자가 예외를 던졌습니다 : " + e.getCause());
    } catch (ClassCastException e) {
        fatalError("Set을 구현하지 않은 클래스입니다.");
    }
    
    // 생성한 집합을 사용한다
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

    private static void fatalError(String msg) {
        System.err.println(msg);
        System.exit(1);
    }

위의 예시에서 리플렉션의 단점이 2가지 있다.

  1. 총 6개의 런타임 예외 발생가능
  2. 쓸데없이 너무 긴 코드
    1. 클래스 이름으로 인스턴스를 생성하기 위해 총 25줄의 코드를 작성해야한다
    2. 리플렉션을 사용하지 않는다면 생성자 호출 1줄 코드로 완료할 수 있다

+ Recent posts