Exception-처리

Java의 Exception(예외) 처리

자바를 사용사면서 개념정리가 잘 되지 않았던 부분들을 정리하는 목적에서 글을 씁니다. 따라서 글에는 오타 및 오류가 있을 수 있으며, 혹시 이 글을 읽는 다른 사람들 그리고 저를 위해 틀린 부분이나 추가되어야할 부분이 있다면 주저하지 말고 알려주시기 바랍니다 ^^


예외 처리의 이유

일반적으로 프로그래밍, 즉 코딩을 할때는 정상적이고 이상적인 시나리오를 예상하며 진행을 합니다. 모든게 이상직이면 얼마나 좋을까요ㅠ
바로 여기서 부터 문제는 시작됩니다.
실제 상황에선 수 많은 예외상황이 발생하기 때문입니다.
개발을 하다보면 도대체 어떻게 이런식으로 접근하는거지? 라는 의문을 가지면서 특이한 케이스들을 맞이하게 됩니다.
이러한 상황을 만나다보면 프로그램이 오류를 발생시켜 프로그램이 종료되는 불상사가 생길 수 있습니다.
따라서 원활한 서비스를 운영하기 위해서는 예외처리(Exception Handling)을 통해 프로그램이 갑자기 원하지 않는 순간에 종료하지 않도록 처리해주어야 합니다.


예외의 종류

자바에서는 예외를 클래스로 관리합니다.
JVM은 프로그램을 실행하는 도중에 예외가 발생하면 해당 예외 클래스로 객체를 생성하고 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해줍니다.

이 예외 클래스들은 크게 두가지로 나눌 수 있습니다.

바로 “컴파일 타임 예외”“런타임(Runtime) 예외” 입니다.

이름에서 알 수 있는 느낌대로 첫째는 소스를 작성하고 컴파일을 할 때 예외 처리코드가 필요한지 JVM에서 검사해주는 예외이고, 둘째는 컴파일 시에 따로 예외처리 코드 검사를 하지 않는 예외 입니다.


공통점

두 종류의 예외는 공통점이 있습니다.
모두 예외처리를 하지 않는다면 프로그램이 에러와 함께 종료된다는 점 입니다.

한마디로 모든 예외를 대할때 예외처리는 필수 라고 기억해야 합니다.


차이점

위에서 이름에서 오는 느낌의 차이를 언급 했지만 코드를 작성할때 알아둬야하는 차이점도 있습니다.

먼저 컴파일 타임 예외와 런타임 예외는 자바에서 클래스로 관리한다는 개념을 다시 짚고 넘어가야 합니다.

각 에러 클래스들은 상속을 통해 구현하는데, 상속의 최종 보스(부모)는 바로 Exception이라는 클래스(java.lang.Exception) 입니다.
이 공톰점이 바로 예외처리를 하지 않으면 프로그램이 예상치 못하게 종료되는 원인이라고 할 수 있습니다.

중요한 차이점은 그림의 오른쪽 아래쪽에 Exception에서 나눠지는 두 줄기로 확인 할 수 있습니다. Exception클래스를 직접적으로 상속하느냐(그림에서 Other Exceptions라고 생각하면됩니다.), 아니면 RuntimeException이라는 클래스를 통해 상속을 구현하느냐에 있습니다(그림에 없는 개념으로 RuntimeException아래에 가지를 뻗어내린다고 생각하면 됩니다.).

이 글에선 Exception클래스를 벗어난(Errors, Throwable 등) 이야기는 다루지 않겠습니다. 사실 제가 아직 개념이 덜 정리되었습니다. (추후에 이 글을 통해 업데이트를 하거나 다른 글을 통해 추가하도록 하겠습니다.)

덧붙여 말하면 RuntimeException 역시 Exception을 상속받지만, JVM이 각 예외클래스를 바라볼 때 RuntimeException을 상속받았는지 여부를 보고 판단하여 구분하는 것 입니다.


RuntimeException

런타임 예외는 자바 컴파일러가 체크를 하지 않기 때문에 개발자의 경험과 지식에 따라 처리하는 커버리지에 차이가 생깁니다.
따라서 자주 발생되는 런타임 예외는 어떤 오류 메시지가 출력되는지 잘 알아둘 필요가 있습니다.


  • NullPointerException
    가장 빈번하게 발생하는 런타임 예외입니다.
    객체 참조가 없는 상태, 즉 null값을 갖는 참조 변수로 객체 접근 연산자인 dot(.)을 사용했을 때 발생합니다.


  • ArrayIndexOutOfBoundsException
    배열에서 인덱스 범위를 초과하여 사용할 경우 발생합니다.
    위 예외를 발생시키지 않으려면 배열을 다룰때 배열의 길이와 인덱스를 항상 고려하여 코딩하는 습관을 들여야 합니다.


  • NumberFormatException
    문자열로 되어 있는 데이터를 숫자로 변경할때 발생합니다.
    보통 많이 쓰이는
    1
    2
    Integer.parseInt(String s) // 문자열을 정수로 변환해서 리턴
    Double.parseDouble(String s) // 문자열을 실수로 변환해서 리턴

위의 메소드를 호출할때 숫자로 변환 될 수 없는 문자가 포함되어 있을 때 예외가 발생합니다.


  • ClassCastException
    타입변환(Casting)은 상위 클래스와 하위 클래스 간에 발생하고 구현 클래스와 인터페이스 간에도 발생합니다. 이러한 관계가 아닌데 억지로 타입 변환을 시도할 경우 예외가 발생합니다.

예외 처리

예외 처리를 하려면 try-catch-finally 구문을 알아야 합니다.
구문을 코드로 표현해보겠습니다.

1
2
3
4
5
6
7
8
9
try {
// do something
// 예외가 발생할만한 로직 수행
// 추가 수행 코드
} catch (예외클래스 e) {
// 예외 처리
} finally {
// 무조건 실행되는 코드
}

지금부터 설명이 길어질지도 모르니 심호흡한번 하세요 ㅎㅎ
일단 finally 문은 생략이 가능합니다.
try-catch-finally에서 try-catch는 필수 구문이고, finally는 옵션입니다.
finally는 주로 파일, 세션, DB연결등을 닫을때(close() 메서드활용) 유용하게 사용됩니다. 예외처리를 해도 무조건 실행되기때문에 정상적일때 처리해야되는 로직들을 수행할 수 있도록 해주는 옵션 구문입니다.

다시 중요한 try-catch로 돌아오면,
try문에서 do something을 실행하다가 예외가 발생할만한 로직을 수행을 하면서 두가지 갈래길에 서게 됩니다.
첫번째 상황은 정상적으로 로직이 수행 됐을 경우입니다.
그렇다면 우리가 원하는대로 로직이 흘러가게되고, 그 경우 try의 모든 로직들이 수행된 후 catch구문을 뛰어넘고(무시하고) finally를 찾거나 없다면 그 밑의 로직으로 넘어갑니다.
두번째 상황은 예외사 발생한 경우입니다.
예외가 발생한 순간 예외 처리 규칙에 따라 catch가 해당 예외를 잡아(catch) 예외처리를 하게 됩니다. 이 때 try구문안에서 예외가 발생한 순간 아래 부분인 추가 수행 코드는 수행되지 않고(무시하고) 넘어가게 됩니다. 예외를 처리한후엔 finally구문을 찾아서 수행하고, 없다면 바로 해당 메소드를 종료하고(stack에서 제거) 호출한 부분으로 돌아가게 됩니다.

여기서 알아둬야할 부분은 catch문은 여러번 올 수 있다는 것입니다.
다중 catch문을 통해 여러 예외를 핸들링 할 수 있습니다.

1
2
3
4
5
6
7
try {

} catch (예외클래스1 e) {

} catch (예외클래스2 e) {

}

주의해야할 것은 아까 말했듯이 catch가 수행된 후에는 해당 메서드가 stack에서 사라지고 호출한 부분으로 돌아가기 때문에 여러개의 catch문을 작성하더도 한개의 catch문만 실행되며 위부터 순차적으로 검사를 하여 catch 합니다.

이쯤에서 예상하시는 분도 있겠지만, 이 이유로 예외 클래스의 범위가 아래에 있는 예외 클래스의 범위보다 넓다면(상속구조에서 부모쪽에 있는 클래스가 먼저 catch를 한다면) 뒤에 오는 범위가 작은 클래스에게는 기회가 오지 않고, 개발자나 사용자들이 중요하고 자세한 정보를 얻기가 힘들어 집니다.
따라서 상속구조상 하위에 속하는(범위가 좁은) 클래스를 catch문에 먼저(위쪽에) 선언하는 것이 중요합니다.


Java 7에서 추가된 예외 처리

  • 멀티 catch
    하나의 catch블록에서 여러 개의 예외를 처리할 수 있는 기능이며 한개의 catch괄호()안에 동일하게 처리하고 싶은 예외를 파이프(|)로 연결하면 됩니다.
    1
    2
    3
    4
    5
    6
    7
    try {
    /* 예외1
    또는
    예외2 발생 */
    } catch (예외클래스1 | 예외클래스2 e) {
    // 예외처리
    }


  • 자동 리소스 닫기
    try-with-resources를 사용하면 예외 발생 여부와 상관없이 사용했던 리소스 객체(각종 입출력 스트림, 서버 소켓, 소켓, 각종 채널)의 close()메소드를 호출해서 안전하게 리소스를 닫아 줍니다.
    먼저 자바6 이전까지 사용했던 코드를 이용하여 FileInputStream을 예로 들어보겠습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    FileInputStream fis = null;
    try{
    fis = new FileInputStream("file.txt");
    // 로직 처리
    } catch(IOException e) {
    // 에러처리
    } finally {
    if(fis != null) {
    try {
    fis.close(); // 중요부분
    } catch (IOException e) {}
    }
    }

자바 7에서 추가된 try-with-resources사용

1
2
3
4
5
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 로직 처리
} catch(IOException e) {
// 에러 처리
}

차이는 close()를 명시적으로 호출하지 않고 자동으로 처리 된다는 것입니다.

하지만 사용을 위해서는 한가지 조건이 필요합니다.
리소스 객체가 java.lang.AutoCloseable 인터페이스를 구현하고 있어야 한다는 것입니다.


예외 떠넘기기 : throws

보통 메소드 내부에서 예외가 발생할 수 있는 코드를 작성할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만, 경우에 따라서는 메소드를 호출한 곳으로 예외를 떠넘길 수도 있습니다.
이때 사용하는 키워드가 throws이며 아래와 같이 사용합니다.

1
2
3
리턴타입 메소드명(매개변수,...) throws 예외클래스1, 예외클래스2, ... {
// 내부 로직
}

위의 예외클래스들은 하나로 묶어서 아래와 같이 간단히 처리할 수도 있습니다.

1
2
3
리턴타입 메소드명(매개변수,...) throws Exception {
// 내부 로직
}


사용자 정의 예외 처리

프로그램을 개발하다 보면 자바 표준 API에서 제공하는 예외 클래스만으로는 다양한 종류의 예외를 표현할 수가 없습니다. 그때는 개발자가 직접 예외클래스를 정의해서 원하는 방식으로 예외를 처리할 수 있습니다.

실제 운영을 하다보면 사용자 정의 예외클래스와 자바 표준 예외 클래스를 적절하게 조합하여 사용하여야 빠르고 정확한 예외 대응을 할 수 있기에 매우 중요한 스킬이라고 할 수 있습니다.

사용자 정의 클래스는 통상 Exception으로 끝나는 클래스명을 이용하며, 원하는 방식에 맞게 Exception 혹은 RuntimeException을 상속하면 됩니다.
예외 클래스도 다른 클래스와 마찬가지로 내부엔 필드 생성자, 메소드 선언을 모두 포함할 수 있지만 대부분 생성자만을 포함합니다.
클래스에 선언한 생성자를 통해 예외를 발생시키며, 보통 매개 변수가 없는 기본 생성자와 예외 발생원인(예외 메시지)을 전달하기 위해 String 타입의 매개 변수를 갖는 생성자 두개를 선언하는 것이 일반적 입니다.
예)

1
2
3
4
public class XXXException extends [ Exception | RuntimeException ] {
public XXXException() { }
public XXXException(String message) { super(message); }
}


Tip

  • 사용자 정의 예외 클래스와 표준 예외 클래스 혼용하기
    1
    2
    3
    4
    5
    6
    7
    public void testException() throws MyException {
    try {
    // do something
    } catch (NumberFormatException e) {
    throw new MyException("예외를 설명하는 메시지", e);
    }
    }

장점은 개발자나 사용자가 예외를 확인할 수 있는 메시지를 담고 있고.
던져진(throw) 예외를 호출한 부분에서 처리하면서 e.printStackTrace()나 e.getMessage()등을 활용해 예외 상황을 파악하는 중요한 정보들을 파악할 수 있습니다.

  • 비지니스 위험도가 있는 중요한 예외는 컴파일 타임 예외로 처리하기
    잔액부족, 아이디/비밀번호 오류 등 미리 인지가 가능하고 에러 발생시 고객에 피해가 가거나 비지니스에 타격이 생길만한 로직은 RuntimeException을 피하고 컴파일타임 예외로 처리하여 경각심을 갖고 대비할 수 있도록 준비한다면 원활한 서비스를 운영하는데 도움이 될 것입니다.

마무리

긴 글 읽어 주셔서 고맙습니다. 혹시 오타나 오류가 있다면 저를 포함한 글을 읽는 모든 사람을 위해 언급해 주세요 ~

글 중간에도 언급을 했지만 예외처리는 많은 경험과 지식이 필요합니다.
귀찮거나 복잡해진다는 이유로 피하지 말고 다양한 상황에 대한 유연한 생각을 가지고 최대한 꼼꼼하게 예외처리를 한다면 개발 역량도 올리고 고객을 만족시키는 서비스 제공에 한발 더 나아갈 수 있다고 생각합니다.

모두 해피코딩 하세요 ~ ^^


본 글을 쓰는데 도움받은 레퍼런스

  1. 자바지기(박재성-pobi) 마스터의 코드스쿼드 수업
  2. (책) 이것이 자바다 (한빛미디어)
  3. 9 Best Practices to Handle Exceptions in Java - DZone Java
Share 0 Comments