본문 바로가기

프로그래밍 언어/JAVA

JAVA 입문 - 예외 처리하기

지난 글: [프로그래밍 언어/JAVA] - JAVA 입문 - 예외 클래스

 

try-catch문

예외 상황을 어떻게 처리해야 하는지 알아보자. 예외를 처리하는 가장 기본 문법인 try-catch문의 형식은 아래와 같다.

try {
	예외가 발생할 수 있는 코드 부분
} catch(처리할 예외 타입 e) {
	try 블록 안에서 예외가 발생했을 때 예외를 처리하는 부분
}

try 블록에는 예외가 발생할 가능성이 있는 코드를 작성한다. 만약 try 블록 안에서 예외가 발생하면 바로 catch 블록이 수행된다. catch문의 괄호 ( ) 안에 쓰는 예외 타입은 예외 상황에 따라 달라진다.

 

try-catch문 사용하기

간단한 배열로 예외가 발생하는 상황을 만들고 그에 따른 예외 처리를 해보자. 아래는 요소가 5개인 정수형 배열을 만들고 요소에 0부터 4를 대입하는 코드이다. 배열 크기가 5이므로 정수 값을 5개 저장할 수 있다.

int[] arr = new int[5];

for(int i = 0; i < 5; i++) {
	arr[i] = i;
    System.out.println(arr[i]);
}

여기서 i < 5를 i <= 5로 바꿔보자.

int[] arr = new int[5];

for(int i = 0; i <= 5; i++) {
	arr[i] = i;
    System.out.println(arr[i]);
}

변경한 코드는 0부터 5까지 총 6개의 숫자를 배열에 넣기에 예외 상황이 발생한다.

 

배열에 저장하려는 값의 개수가 배열 범위를 벗어났기 때문에 예외가 발생한 것이다. 참고로 이 예외는 RuntimeException의 하위 클래스인  ArrayIndexOutOfBoundsException으로 처리하는데, 이 클래스는 예외 처리를 하지 않아도 컴파일 오류가 나지 않는다. 따라서 프로그래머가 직접 예외 처리를 하지 않으면 예외가 잡히지 않아서 예외가 발생하는 순간(i가 5가 되는 순간)에 프로그램이 갑자기 멈춘다. 그러므로 예외가 발생한 순간 프로그램이 비정상 종료되지 않도록 예외 처리를 해줘야 한다.

 

예외가 발생한다는 가정하에 아래와 같이 예외 처리를 해보자.

출력 결과

ArrayExceptionHandLing 코드를 보면, 배열 범위가 유효한 값 4까지는 배열에 저장되어 출력되고 그 다음 값을 배열에 넣으려 할 때 예외가 발생한다. 발생한 예외는 catch 블록에서 처리하므로 System.out.println("프로그램 종료") 문장까지 수행하고 프로그램이 정상 종료된다. 만약 예외가 발생하여 프로그램이 바로 비정상 종료되었다면 System.out.println("프로그램 종류") 문장을 수행할 수 없다. 이처럼 예외 처리는 비정상 종료되는 것을 방지할 수 있기에 매우 중요하다.

 

컴파일러에 의해 예외가 체크되는 경우

앞에서 본 코드는 예외 처리를 하지 않아도 컴파일 오류가 나지 않지만, 자바에서 제공하는 많은 예외 클래스들은 컴파일러에 의해 처리된다. 이런 경우 자바에서는 예외 처리를 하지 않으면 컴파일 오류가 계속 남는다. 그럼 예외를 처리해야 하는 파일 입출력과 관련한 코드를 살펴보자. 파일 입출력은 아직 배우지 않았지만 예외 처리 방법을 익힐 수 있는 정도의 수준으로 구현하여 코딩한다.

 

파일 입출력에서 발생하는 예외 처리하기

자바에서 파일을 읽고 쓰는 데 스트림(stream) 객체를 사용한다. 스트림 종류는 여러 가지가 있지만, 이 글에서는 파일에서 데이터를 바이트 단위로 읽어 들이는 FileInputStream을 사용한다. ExceptionHandLing 파일을 작성해 main( ) 함수 안에 FileInputStream 선언 코드를 아래처럼 작성한다.

FileInputStream fis = new FileInputStream("a.txt");

지금까지 클래스를 생성할 때 사용하던 코드와 다르지 않다. 위 코드는 a.txt 파일에서 데이터를 읽어 들이기 위해 스트림 객체를 생성한다는 의미다.이렇게 코드를 작성하면 new FileInputStream("a.txt"); 부분에 아래처럼 오류가 난다.

마우스를 올려 보면 'FileNotFoundException이 처리되지 않았다'는 메시지가 있고 그 아래쪽에 있는 두 옵션 중 하나를 선택하라고 되어 있다. 이 코드는 a.txt 파일을 열어 읽으려고 FileInputStream 클래스를 생성한 경우인데, 이 경우 a.txt 파일이 존재하지 않는 오류가 발생할 수 있다. 읽으려는 파일이 없는 경우에 자바 가상 머신에서는 FileNotFoundException 예외 클래스가 생성된다. 따라서 나타나는 오류 메시지는 이러한 예외 상황에 대비한 예외 처리를 해야 한다는 뜻이다. try-catch문으로 감싼다는 뜻인 Surround with try/catch를 클릭하면 아래처럼 예외가 발생할 위험이 있는 코드가 try 블록으로 감싸진다.

위에 적은 내용과 마찬가지로 try 블록이 먼저 수행되고, 이 코드에서 예외가 발생하면 catch 블록을 수행한다. try문으로 감싸진 부분에서 발생할 수 있는 예외는 FileNotFoundException이고 변수 이름 e로 선언되었다. 그리고 어디에서 예외가 발생했는지 따라가는 printStackTrace( ) 메서드가 호출된 것이 보인다. 아직 a.txt 파일이 없으므로 이 상황에서는 당연히 아래처럼 예외가 발생한다.

코드를 실행해 보면 결과 화면에 예외 이름과 그 내용이 보인다. 결과 화면에서 ExceptionHandLing.java:9를 클릭해보면 예외가 발생한 코드 위치로 이동한다. 마치 프로그램이 비정상 종료된 것 같지만 그렇지 않다. try-catch문을 사용해 아래처럼 코드를 작성해보자.

출력 결과

예외가 발생했을 때 FileNotFoundException e의 toString( ) 메서드가 호출되도록 코드를 작성했다. 출력 결과를 보면 첫 번째 줄은 e의 출력 내용이다. 하지만 두 번째 줄 '여기도 수행됩니다'가 출력되었으므로 예외 처리 후에도 프로그램이 계속 수행되었음을 알 수 있다.

 

예외 처리를 한다 해서 프로그램의 예외 상황 자체를 막을 수는 없지만 예외 처리를 하면 예외 상황을 알려 주는 메시지를 볼 수 있고, 프로그램이 비정상 종료되지 않고 계속 수행되도록 만들 수 있다.

 

try-catch-finally문

프로그램에서 사용한 리소스는 프로그램이 종료되면 자동으로 해제된다. 예로 네트워크가 연결되었을 경우에 채팅 프로그램이 종료될 때 연결도 닫힌다. 하지만 끝나지 않고 계속 수행되는 서비스 같은 경우에 리소스를 여러 번 반복해서 열기만 하고 닫지 않는다면 문제가 발생한다. 시스템에서 허용하는 자원은 한계가 있기 때문이다. 따라서 사용한 시스템 리소스는 사용 후 반드시 close( ) 메서드로 닫아 줘야 한다. 그럼 위에서 사용한 FileInputStream 클래스를 다시 보자. 열어 놓은 파일 리소스를 닫는 코드를 아래와 같이 추가한다.

지금은 try 블록에서만 파일 리소스를 닫았다. 그런데 프로그램이 정상적으로 종료된 경우에도 열어 놓은 파일 리소스를 닫아야 하고, 비정상 종료된 경우에도 리소스를 닫아햐 한다. 따라서 try 블록뿐 아니라 catch 블록에도 close( ) 메서드를 사용해야 한다.

 

만약 try 블록 안에서 발생할 수 있는 예외 상황이 여러 개라면 catch 블록을 예외 상황 수만큼 구현해야 한다. 그런데 한 번 열어 놓은 리소스를 해제하는 코드를 try-catch-catch... 각 블록에 모두 작성해야 한다면 번거로울 것이다. 이때 사용하는 블록이 finally이다. finally를 사용하는 형식은 아래와 같다.

try { 
	예외가 발생할 수 있는 코드 부분
} catch(처리할 예외 타입 e) {
	예외를 처리하는 부분
} finally {
	항상 수행되는 부분
}

일단 try 블록이 수행되면 finally 블록은 어떤 경우에도 반드시 수행된다. try나 catch문에 return문이 있어도 말이다. 따라서 try-catch-catch... 각 블록마다 리소스를 해제하지 않고 finally 블록에서 한 번만 해제해 주면 된다. 코드로 작성해보자.

출력 결과

우선 입력받은 파일이 없는 경우에 대해 try-catch문을 사용해 FileNotFoundException 예외 처리를 했다. 프로그램을 실행하면 a.txt 파일이 없으므로 예외가 발생하여 catch 블록이 수행된다. 예외를 출력하고 15행 강제로  return 했다. 하지만 출력 결과는 return문과 상관없이 finally 블록이 수행되어 '항상 수행됩니다.' 문장이 출력되었다. fis.close( ) 문장에서도 예외가 발생할 수 있으므로 예외 처리를 해야 한다.

 

try-with-resources문

위에서 봤듯 시스템 리소스를 사용하고 해제하는 코드는 다소 복잡하다. 자바 7부터 try-with-resources문을 제공하여 close( ) 메서드를 명시적으로 호출하지 않아도 try 블록 내에서 열린 리소스를 자동으로 닫도록 만들 수 있다. try-with-resources 문법을 사용하려면 해당 리소스가 AutoCloseable 인터페이스를 구현해야 한다.

 

AutoCloseable 인터페이스에는 close( ) 메서드가 있고 이를 구현한 클래스는 close( )를 명시적으로 호출하지 않아도 close( ) 메서드 부분이 호출된다. 위에서 사용했던 FileInputStream을 JavaDoc에서 찾아보면 AutoCloseable 인터페이스를 구현하고 있다. FileInputStream 클래스는 Closeable과 AutoCloseable 인터페이스를 구현했다. 따라서 자바 7부터는 try-with-resources 문법을 사용하려면 FileInputStream을 사용할 때 cloas( )를 명시적으로 호출하지 않아도 정상인 경우와 예외가 발생한 경우 모두 close( ) 메서드가 호출된다. FileInputStream 이외에 네트워크(socket)와 데이터베이스(connection) 관련 클래스도 AutoCloseable 인터페이스를 구현하고 있다.

 

AutoCloseable 인터페이스

AutoCloseable 인터페이스를 직접 구현한 클래스를 만들고 프로그램이 정상적으로 수행됐을 때와 예외가 발생했을 때 각각 close( ) 메서드 부분이 잘 호출되는지 살펴보자. 먼저 프로그램이 정상적으로 수행되는 경우다.

AutoCloseable 인터페이스는 반드시 close( ) 메서드를 구현해야 한다. 시스템 리소스인 경우에는 파일 스트림을 닫거나 네트워크 연결을 해제하는 코드를 작성해야 한다고 한다. 하지만 여기서는 close( ) 메서드가 제대로 호출되는지 알아보기 위해 출력문만 남긴다.

출력 결과

try-with-resources문을 사용할 때 try문의 괄호 ( ) 안에 리소스를 선언한다. 이 코드는 예외가 발생하지 않고 정상 종료되는데 출력 결과를 보면 close( ) 메서드가 호출되어 '리소스가 close( ) 되었습니다.' 문장이 출력되었다. 리소스를 여러 개 생성해야 한다면 세미 콜른(;)으로 구분한다.

 

이번에는 예외가 발생하여 종료되는 경우에도 close( ) 메서드가 잘 호출되는지 살펴보자. throw new Exception( ) 문장을 사용하면 프로그램에서 강제로 예외를 발생시켜 catch 블록이 수행되도록 구현할 수 있다.

출력 결과

6행에서 강제로 예외를 발생시키면 catch 블록이 수행된다. 출력 결과를 보면 리소스의 close( ) 메서드가 먼저 호출되고 예외 블록 부분이 수행되는 것을 알 수 있다. 이처럼 try-with-resources문을 사용하면 close( ) 메서드를 명시적으로 호출하지 않아도 정상 종료된 경우와 예외가 발생한 경우 모두 리소스가 잘 해제된다.

 

향상된 try-with-resources문(자바9에서 추가된 문법)

자바 7에서 제공하기 시작한 try-with-resources문의 예외 처리 방법은 자바 9로 업그레이드되며 조금 더 향상되었다. 자바 7에서는 AutoCloseable 인터페이스를 구현한 리소스의 변수 선언을 try문 괄호 안에서 해야 했다. 따라서 리소스가 외부에 선언되고 생성된 경우에도 다른 참조 변수로 괄호 안에 다시 선언해야 했다.

하지만 자바 9부터는 아래처럼 try문의 괄호 안에서 외부에서 선언한 변수를 쓸 수 있다. 이렇게 사용하면 가독성도 좋고 반복해 선언하는 일도 줄어든다.

AutoCloseObj obj = new AutoCloseObj();
try(obj) {
	throw new Exception();
} catch(Exception e) {
	System.out.println("예외 부분입니다.");
}

이 문법은 자바 9에서부터 추가된 내용으로 자바 8 이하에서는 오류가 난다.

 

참고 서적 : 자바 프로그래밍 입문 - 박은종

'프로그래밍 언어 > JAVA' 카테고리의 다른 글

JAVA 입문 - 사용자 정의 예외  (0) 2022.06.12
JAVA 입문 - 예외 처리 미루기  (0) 2022.06.11
JAVA 입문 - 예외 클래스  (0) 2022.06.11
JAVA 입문 - 스트림  (0) 2022.06.10
JAVA 입문 - 람다식  (0) 2022.06.09