ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 예외 처리하기
    IT/개발(자바, 서블릿, 스프링 등) 2020. 1. 24. 16:26

    오류가 발생하면 프로그램이 강제로 중단되거나 원하는 결괏값을 도출할 수 없다. 개발 단계에서 오류를 예측하고 적절하게 대처해야 한다. 자바에서 발생하는 오류는 예외(Exception)와 에러(Error) 두 가지 종류가 있다. 이 중 '예외'는 프로그램을 잘못 구현해서 발생하고 프로그램 내에서 처리할 수 있는 오류다. 따라서 우리가 처리할 수 있는 예외에 대한 내용만 살펴본다.

     

    1. 자바 예외 API

    자바는 객체지향 언어이므로 모든 기능은 객체에 기반을 두고 처리한다. 오류 처리 또한 객체로 처리한다. 관련 API를 Java SE에서 제공한다. java.lang 패키지의 Exception 항목과 Error 항목으로 구분되어 있다.

     

    예외 처리 객체 구조

    오류에 관한 최상위 객체는 java.lang의 Throwable 클래스이다. 자바 프로그램에서 발생하는 모든 오류는 Throwable 클래스의 하위 객체로 정의되어있다. 

     

    그림 Throwable 클래스의 상속 구조

     

     

     

     

    Throwable을 상속하는 하위 객체는 Exception과 Error 계열로 분류된다. 두 계열의 차이는 Exception은 프로그램 내에서 발생하는 예외로서 프로그램 내에서 처리가 가능하고, Error는 JVM 내에서 발생하는 에러로서 프로그램 내에서 처리가 불가능하다. 

     

    Exception 계열 예외는 다시 '확인된 예외(checked exception)'와 '미확인 예외(unchecked exception)'로 분류할 수 있다. 미확인 예외는 RuntimeExcepion과 직간접적으로 관련된 객체들이다. 잘못 구현된 프로그램 코드 때문에 발생한다. 예를 들어, 배열에서 존재하지 않는 인덱스에 접근하거나 어떤 수를 0으로 나눌 때 발생하는 오류가 미확인 예외다. 확인된 예외는 Exception을 상속하는 객체 중 RuntimeException과 관련없는 객체들로서 프로그램 내에서 통제할 수 없는 조건 때문에 발생한다. 예를들어 존재하지 않는 외부 파일을 사용할 때 발생한다. 따라서 확인된 예외가 발생할 수 있는 명령문을 사용할 땐 오류가 발생할 것을 대비해 예외 처리도 함께 해주어야 한다. 확인된 예외에 대한 예외 처리를 구현하지 않으면 컴파일 시 오류가 발생한다. 확인된 예외는 소스 파일이 컴파일될 때 컴파일러가 예외 처리를 했는지 검사하기 때문이다.

     

    예외 객체 종류

    예외 객체 종류 설명
    NullPointerExcepion 참조변수가 null 값을 가지는 상태에서 참조변수를 통해 변수에 접근하거나 메서드를 호출하는 예외가 정의된 객체
    IndexOutOfBoundsExcepion 배열에서 존재하지 않는 인덱스에 접근하는 예외가 정의된 객체
    ClassNotFoundException 존지하지 않는 클래스를 사용하는 예외가 정의된 객체
    NoSuchFieldException 선언되지 않은 필드를 사용하는 예외가 정의된 객체
    NoSuchMethodException 선언되지 않은 메서드를 호출하는 예외가 정의된 객체
    IOException 외부 자원과 입출력    작업을 하면서 발생하는 예외가 정의된 객체
    NumberFormatException 문자열을 숫자로 변환할 때 숫자로 변환할 수 없는 문자열을 사용하는 예외가 정의된 객체
    ArithmeticException 어떤 수를 0으로 나누는 등과 같은 산술 연산에 관련된 예외가 정의된 객체

     

    2. 예외 발생 원리

     

    예외가 발생하면 JVM은 Exception을 상속하는 객체 중 발생한 예외가 정의된 객체를 생성한다. 프로그램이 실행될 때 JVM이 Exception의 하위 객체를 생성했다는 것은 예외가 발생했다는 의미다. 따라서 프로그램은 더는 진행하지 못하고 강제로 종료된다.

     

    예외 처리 방법

    예외가 발생했다고 해서 프로그램이 강제로 종료되면 안된다. 예외가 발생하더라도 프로그램이 정상 동작될 수 있도록 대처해야 한다. 이 때 try-catch 문을 사용한다.

     

    try {

             // 실행문

    } catch(변수선언) {

             // 예외 처리

    }

     

    try 블록에서 예외가 발생하면 JVM은 해당 예외 객체를 생성하고 예외 처리를 했는지 살펴본다. JVM 생성한 예외 객체를 참조할 수 있는 변수가 catch문 소괄호에 선언되었다면 해당 변수에 예외 객체를 저장하고 catch 블록을 실행한다. 그리고 try-catch 문 다음에 작성한 명령문이 정상으로 실행된다. 만약 예외를 catch 블록에서 처리하지 않았다면 프로그램은 그대로 종료된다.

     

    try {
    	String s = new String("java");
        s.length();
        s = null;
        s.length();
        
        int arr[] = new int[3];
        arr[3] = 30;
        
        System.out.println("OK");
        
    } catch(ArrayIndexOutOfBoundsException e1) {
    	
        // ArrayIndexOutOfBoundsException에 대응하는 실행문
        System.out.println("잘못된 배열의 인덱스 사용.");
        
    } catch(NullPointerException e2) {
    
    	// NullPointerException에 대응하는 실행문
        System.out.println("null을 참조하는 변수에 접근.");
    }
    
    [실행결과]
    null을 참조하는 변수에 접근.
    OK

    변수 s는 null이다. 변수 s가 참조하는 인스턴스가 존재하지 않으므로 s.length() 명령문은 실행될 수 없다. 이처럼 참조변수가 null 인 상태에서 인스턴스에 접근하면 NullPointerException이 발생한다.  JVM이 NullPointerException 객체를 생성한다. JVM은 예외 객체인 NullPointerException을 생성한 후 catch 문에서 해당 예외를 처리하고 있는지 살펴본다. catch문이 여러 개이면 작성한 순서대로 검사해서 예외 객체를 저장할 변수를 찾는다. 찾으면 해당 catch 블록에 선언된 변수에 예외 객체를 저장 후 예외 처리를 실행한다. 찾지 못하면 프로그램 실행은 중단한다.

     

    예외 처리 메서드

    앞에서 모든 예외 객체는 Throwable을 상속받는다고 했다. Throwable에 선언된 메서드 중 예외 처리에 자주 사용하는 메서드는 다음과 같다.

     

    제어자 및 타입 메서드 설명
    String getMessage() 발생한 예외 객체의 메시지 추출
    void printStackTrace() 예외가 발생하기까지 호출된 순서를 거꾸로 출력
    String toString() 발생한 예외 객체를 문자열로 추출

     

    finally문

    try 블록에서 파일 열기, 네트워크 연결, 데이터베이스 연결과 같은 작업을 했다면 작업이 완료된 후에는 파일 닫기, 네트워크 연결 종료, 데이터베이스 연결 종료와 같은 자원 해제 작업을 해야 한다. 만일 자원 해제 작업을 안하면 다른 프로그램에서 자원들이 필요할 때 사용할 수 없는 상황이 발생할 수 있다.

     

    finally 블록은 예외 발생과는 상관 없이 항상 실행된다. 자원 해제 기능을 담당하는 코드는 예외 발생과 상관 없이 항상 실행 되어야 하므로 finally 블록에 실행된다.

     

    try-with-reosurces

    try-catch 문에서 finally 블록을 사용하는 목적은 try 블록에서 사용한 자원을 해제하는 데 있다. Java SE 7부터 try-catch-finally 문을 간단하게 사용할 수 있도록 try-with-resources 문을 지원한다. finally 문을 지정하지 않아도 try 문이 정상으로 실행됐든 예외가 발생했든try-catch 문이 완료되면 자동으로 자원을 해제해준다.

     

    try(클래스명 변수명 = new 클래스명()) {

         // 실행문

    } catch (Exception e) {

         // 예외 처리

     

    try(FileInputStream fi = new FileStream("a.txt")) {
    	// 파일 처리 명령문
    } catch(Exception e) {
    	// 예외에 대응하는 명령문
    }

     

    한 걸음 더 나아가 Java SE 9부터는 try-with-resources 코드를 더 간소화할 수 있도록 try 문의 괄호 () 안에 자동 해제할 객체를 생성하지 않고, try 문 밖에서 생성한 후 변수만 지정할 수 있도록 했다.

     

     

    a.txt에는 'Hello'라는 문자열이 있다.

    public void test(){
    
    	FileInputStream fi = new FileInputStream("a.txt");
        
        try(fi) {
        	
            int c = fi.read();
            System.out.println((char) c);
            
        } catch(Exception e){
        
        	e.printStackTrace();
            
        }
    
    }
    
    [실행결과]
    H
    

     

    fi는 a.txt 파일과 연결하는 객체다. fi는 파일 자원을 사용했으므로 작업을 마친 후에는 자원을 해제해야 한다. 자원을 자동으로 해제하고자 try() 문 안에 객체를 생성했다. FileInputStream 객체는 AutoCloswable 인터페이스를 구현하였으므로 자동으로 해제되도록 사용할 수 있다. fi.read()는 a.txt 파일에서 한 글자를 읽어 정수로 반환한다. 문자를 char 타입으로 변경해 출력한다. 

     

    3. 예외 던지기: throws 문

    throws는 메서드를 호출하는 곳의 상황에 맞게 동적인 오류 처리를 하기 위해 사용하는 명령문이다. 메서 드내에서 try-catch 문으로 예외 처리를 하면 이 메서드가 실행될 땐 항상 똑같은 예외 처리를 하게 된다. 이처럼 고정된 예외 처리 방식이 아니라 메서드를 호출하는 곳에서 상황에 맞게 동적으로 예외 처리를 하고 싶다면 메서드 선언부에서 throws 다음에 메서드 내에서 처리할 Exception 객체를 선언하면 된다.

     

    만일 b() 메서드에서 c() 메서드를 호출할 때, 호출받은 c() 메서드에는 throws Exception이 선언되어 있으면, c() 메서드를 호출하는 b() 메서드에서 Exception을 처리해야 한다. 그렇지 않으면 컴파일 오류가 발생한다. 만약 b() 메서드에서 예외 처리를 하지 않고 다시 b() 메서드를 호출한 곳으로 예외를 던지고 싶다면 또다시 예외를 던질 수 있다. b() 메서드를 호출한 곳에서 처리해야 한다. 

     

    public static void main(String[] args) {
    	try{
        	FileInputStream fi = new FileInputStream("a.txt");
            int c = fi.read();
            System.out.println((char)c);
        }catch(Exception e){
        	e.printStackTrace();
        }
    }

     

    public FileInputStream(String name) throws FileNotFoundException

    public int read() throws IOException

     

    위의 생성자와 메서드는 이 메서드를 호출한 곳에서 예외를 처리해주어야 한다. 이를 생략하면 오류가 발생한다. try-catch 문을 사용하거나 throws를 사용해 예외를 ㄷㅏ시 던질 수 있다.  main() 메서드에서 예외를 던지면 main 메서드는 JVM에서 호출하므로 결국은 예외처리를 하지 않은 것과 같다.

     

    4. 사용자 정의 예외 객체

    사용자 정의 예외 객체는 Java SE API에서 제공하는 예외 객체 외에 개발자가 선언해서 사용하는 객체를 의미한다. Exception이나 Exception을 상속하는 클래스를 반드시 상속받아야 한다. 일반적으로 사용자 정의 예외 객체는 다음의 Exception에서 선언된 형태의 생성자 외에는 다른 멤버를 가지지 않는다.

     

    Exception()

    Exception(String message)

    Exception(String message, Throwable cause)

    Exception(Throwable cause)

     

    다음의 사용자 정의 예외 객체는 기본 생성자를 선언하고 super 문으로 Exception의 생성자를 호출한다. 따라서 Exception(String message) 생성자가 호출되며 예외 메시지를 지정한다.

     

    public class NagativeNumberException extends Exception {
    	public NagativeNumberException() {
        	super("음수는 허용하지 않습니다");
        }
    }

     

    예외 객체는 예외 상황이 발생하면 JVM이 자동으로 객체를 생성한다. 그러나 사용자 정의 예외 객체는 개발자가 필요해서 생성한 것이므로 JVM이 예외 상황을 인지하여  자동으로 예외 객체를 생성할 수 없다. 사용자 정의 예외 객체는 프로그램 내에서 예외 상황과 예외 객체 생성을 처리해주어야 한다. 예외 상황은 조건문으로 처리하고 예외 객체 생성은 throw 문으로 할 수 있다. 

     

     

    thorw new 예외객체명();

     

    예제를 보자. time 변숫값이 0보다 작을 때, 즉 음수일 때 예외 상황으로 인식하고 NagativeNumberException이 발생한다. throw는 예외를 강제로 발생하게 하는 것이므로 try-catch 문으로 예외를 처리한다. 만일 예외 처리를 하고 싶지 않다면 메서드 선언부에 throws 문을 이용해서 호출한 곳으로 예외를 던질 수 있다. 

     

    NagativeNumberException.java
    
    public class NagativeNumberException extends Exception {
    	public NagativeNumberException() {
        	super("음수는 허용하지 않습니다");
        }
    }
    
    
    
    
    Test.java
    
    public class Test {
    	int battery = 0;
        
        public void charge(int time){
        	if( time<0 ){
            	time = 0;
                
                try{
                	throw new NagativeNumberExcepiton();	//22번째 줄
                }catch(NagativeNumberExcepiton e){
                	e.printStackTrace();
                }
            }
            
            battery += time;
            System.out.println("현재 배터리 : " + battery);
        }
        
        public static void main(String[] args){
        	Test test = new Test();
            test.charge(30);
            test.charge(20);
            test.charge(-10);							//36번째 줄
        }
    }
    
    [실행결과]
    현재 배터리 : 30
    현재 배터리 : 50
    com.test.java.Test.NagativeNumberException: 음수는 허용되지 않습니다.
    	at com.test.java.Test.charge(Test.java:22)
        at com.test.java.Test.main(Test.java:36)
    현재 배터리 : 50
Designed by Tistory.