예외 처리(Exception Handling)

프로그램 이 실행 중에 어떤 원인에 의해 오작동을 하거나 비정상적으로 종료되는 경우가 있어요. 이런 결과를 초래하는 원인을 프로그램의 에러 또는 오류라고 해요.

발생 시점에 따라 컴파일 에러compile error와 런타임 에러runtime error로 나눌 수 있어요. 컴파일 에러는 컴파일할 때 발생하는 오류고, 런타임 에러는 프로그램 실행 도중에 발생하는 에러에요.

이 밖에도 논리적 오류logical error도 존재하는데, 이는 컴파일도 잘되고 프로그램 실행 상 문제는 없지만 의도한 것과 다르게 동작하는 것을 말해요. 예를 들면 시간은 0~24까지만 지정할 수 있는데 그 이상의 숫자나 음수가 들어가는 경우가 있어요.

컴파일 에러 : 컴파일 시 발생하는 문법적 오류
런타임 에러 : 프로그램 실행 도중에 발생하는 오류
논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 오류

소스를 컴파일해서 *.class 파일을 성공적으로 만들어도 이것이 정상적으로 동작한다는 보장은 없어요. 컴파일러는 실행 시 발생하는 잠재적 오류까지 잡을 순 없기 때문이에요. 그래서 자바를 사용하는 개발자는 런타임runtime 실행 시 발생하는 오류에 대비가 필요해요. 자바는 이러한 오류를 에러error와 예외exception로 구분했어요.

에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외 : 프로그램 코드에 의해서 수습될 수 있는 미약한 오류

예외 클래스의 계층구조


자바에서는 실행 시 발생할 수 있는 오류를 클래스로 정의할 수 있어요. 이 역시 최상위 클래스인 Object를 상속해요.

여기서 모든 예외의 최고 조상은 Exception 클래스에요. 그리고 Exception의 자식 클래스는 크게 두 그룹으로 나눠져요.

1. Exception 클래스와 그 자손들 \to 그림 기준 위
2. RuntimeException 클래스와 그 자손들 \to 그림 기준 아래

RuntimeException 클래스들은 주로 프로그래머의 실수에 의해서 발생되는 예외들이에요. 배열의 범위를 벗어나던가, null 변수를 사용하려한다던가하는 것이 대표적인 예에요.

Exception 클래스들은 주로 외부의 영향으로 발생하는 것들이에요. 그래서 사용자들의 동작에 의해서 발생하는 경우가 많아요. 존재하지 않는 파일 이름을 입력하거나, 실수로 클래스 이름을 잘못 적었던가하는 것이 예에요.

try-catch 문

프로그램 실행 도중에 발생하는 에러는 어쩔 수 없지만, 예외는 프로그래머가 처리를 미리 해줄 수 있어요.

예외 처리Error handling이란 프로그램 실행 시 발생하는 수 있는 예상 못한 예외 발생에 대비한 코드를 작성하는 것이에요. 또한 목적은 예외 발생으로 인한 프로세스의 비정상 종료를 막고, 정상적인 실행 상태를 유지하기 위한 것이에요.

발생한 예외를 처리하지 못하면, 프로세스는 비정상적으로 종료되며 처리 되지 못한 예외uncought exception는 JVM의 예외처리기UncaughtExceptionHandler가 받아서 원인을 화면에 출력시켜요.

// try - cath 구조
try {
	// 프로그램 실행 흐름
} catch (Exception1 e1) {
	// Exception1가 발생한 경우 처리 문
    try {
    } catch(Exception1 /*e1*/ e) {	// Error! 중복 선언되었어요. 변수 명을 바꾸면 컴파일은 잘되요
    
    }
} catch (Exception2 e2) {
	// Exception2가 발생한 경우 처리 문
} catch (Exception3 e3) {
	// Exception3가 발생한 경우 처리 문
}

try-catch문의 흐름은 아래와 같아요.

try 블럭 내에서 예외가 발생한 경우
1. 발생한 예외와 일치하는 catch 블록이 있는지 확인해요
2. 일치하는 catch 블록을 찾으면 해당 catch 블록 내의 문장들을 수행하고 전체 try-catch문을 빠져나가 다음 문장을 수행해요. 일치하는 catch 블록을 찾지 못하면 예외는 처리되지 못해요.
try 블록 내에서 예외가 발생하지 않은 경우
1. catch 블록을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속해요.

// 결과 : 
// 3
// 0
// 6
try {
	System.out.println(3);
	System.out.println(0/0);		// 여기서 예외 발생!
    								// catch문으로 바로 돌아가고
                                    // 다음 문장은 수행 안해요
	System.out.println(4);
} catch(ArithmaticExeption ae) {
	System.out.println(0);
}
System.out.println(6);				// catch문이 끝나고 얘를 수행해요

만약에 ArithmaticExeption 클래스와 모든 예외 클래스의 부모인 Exception이 함께 있다면 어떻게 될까요? 답은 ArithmaticExeption 관련된 예외만 ArithmaticExeption로, 나머지 등록되지 않은 예외는 Exception catch문으로 들어가게 되요.

try {
	// 무언가...
} catch(ArithmaticExeption ae) {
	// ArithmaticExeption 발생 시에만 처리
} catch(Excetpion e) {
	// 그 밖에 catch문에 등록 안된 모든 예외
}

printStackTrace() & getMessage()

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있어요. getMessage()printStackTrace()를 통해 그 정보를 얻을 수 있어요.

catch 블록의 인자로 들어온 참조 변수를 통해 이 인스턴스에 접근할 수 있어요.

printStackTrace : 예외 발생 당시 호출 스택call stack에 있었던 메서드의 정보와 예외 메시지를 화면에 출력해요
getMessage : 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있어요.

try{
} catch(Exception1 ex) {
	ex.printStackTrace();	// 콜 스택 표기
    System.out.println("메시지 출력 : " + ex.getMessage());
} catch(Exception2 ex) {
	// 파일 입출력 스트림을 통해 발생한 예외에 대한 정보를 파일에 저장할 수도 있어요.
    ex.printStackTrace(new PrintStream());
    ex.printStackTrace(new PrintWriter());
}

멀티 catch 블록

JDK1.7부터 catch 블록 인자 부분에 | 연산자를 통해 하나의 블록으로 합칠 수 있게 되었고, 이를 멀티 catch 블록이라고 해요. | 기호로 연결할 수 있는 예외 클래스 개수는 제한이 없어요.

여기서 멀티 catch는 하나의 catch 블록으로 여러 예외를 처리하는 것이기 때문에, 멀티 내에서는 실제로 어떤 예외가 발생한 것인지 정확히 알 수 없어요. 그래서 멀티 블록으로 연결된 참조 변수 e예외 클래스들의 부모 예외 클래스에 선언된 멤버만 사용할 수 있어요.

물론 필요하다면 instanceof로 처리는 할 수 있겠지만, 이렇게 하면서까지 합칠 이유는 없겠죠? 그래서 공통 분모 멤버만 필요할 때 사용해요.

try {
} catch(Exception1 | Exception2 e) {
	// 두개를 합쳤어요!
    e.commonMethod();					// 공통 분모 멤버만 사용가능해요!
    // e.exMethod1();					// Exception1의 메서드 사용 못해요!
    if(e instanceof Exception1){		// 물론 형변환으로 할 수는 있어요
    	Exception1 e1 =(Exception1)e;
        e1.exMetho1();	
    }
}

예외 발생시키기

throw 키워드를 사용해서 고의로 예외를 발생시킬 수 있어요.

1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만들어요

Exception e = new Exception("고의로 발생");

2. throw를 사용해서 예외를 발생시켜요

throw e;

예시

try {
	Exception e = new Exception("고의로 발생");
    throw e;
    // 위에 내용이랑 똑같아요
    // throw  new Exception("고의로 발생");
} catch(Exception e) {
	// 에러 처리...
}

예외를 날리는 코드를 작성하실 때, 예외 처리를 해줘야할 가능성이 있는 문장에 반드시 예외 처리 코드가 있어야 해요. 그렇지 않으면 컴파일 과정 중 에러가 떠요.

public static void main(String[] args) {
	throw new Exception();	// Error! try-catch 예외 처리 코드가 없어요
}

대신 RuntimeException과 자식 예외 클래스는 그대로 선언이 가능해요. 왜냐하면 이 예외는 프로그래머에 의해 실수로 발생하는 것들이기 때문에 예외 처리를 강제하지 않는 것이에요.

public static void main(String[] args) {
	throw new RuntimeException();	// 프로그래머가 실수한 거라 바로 진행해요
    
    try { 
    	int[] arr = new int[10];
        System.out.println(arr[11]);
    } catch (ArrayIndexOutOfBoundsException ex) {
    	// 물론 예외 처리를 해주고 싶다면 이렇게 명시해도 되고요.
    }
}

여기서 컴파일러가 예외처리를 확인하지 않는 RuntimeException 클래스들은 unchecked 예외라고 부르고, 예외 처리를 확인하는 Exception 클래스들은 checked 예외라고 해요.

메서드에 예외 선언하기

예외를 처리하는 방법에는 try-catch 문 말고도 메서드에 선언하는 방법도 존재해요.

메서드에 예외를 선언하려면 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주면 되요. 예외가 여럿 일때엔 쉼표,로 구분해요.

void method() throws Exception1, Exception1, ... { }

위의 메서드는 이 메서드를 사용하려면 Exception1, Exception1, ..., 선언된 클래스에 대한 예외처리가 필수라는 것을 명시해줘요.

throws Exception을 선언하면, 이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 의미에요. 예외를 선언하면 선언된 것과 그 선언된 예외의 자식까지도 발생할 수 있다는 점을 주의해요. 이 의미는 예외 클래스의 상속 관계도 고려해야 한다는 것을 의미해요. 그러니 Exception 같은 예외를 선언해주면 밑에 자식 예외 클래스 모두를 고려해야 해요.

void method() throws Exception { }

그리고 이렇게 예외를 명시해주는 것은 예외를 처리하는 것이 아닌, '나는 예외가 발생할 수 있어, 그니까 처리해줘!'라고 하면서 예외 처리를 떠맡기는 것이에요.

예외를 전달받은 메서드가 다시 자신을 호출한 메서드에게 전달할 수 있어요. 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main 메서드에서도 예외 처리가 되지 않으면, main 메서드도 종료되어 프로세스 전체가 종료되요.

// 이렇게 호출 스택에서 타고 들어가, 최종적에 main에 도착했는데
// 예외 처리를 하지 않았으니 프로그램은 런타임에서 종료되요.
public static void main(String[] args) throws Exception {
	method();
    System.out.println("Hello World!");		// 이거 실행 안되요!
}

private static void method() throws Exception {
	method2();
}

// method() 대신 method1()을 사용하면 예외 처리를 하기 때문에
// main에도 따로 throws 선언할 필요도 없게 되요.
private static void method1() {
	try {
    	method2();
    } catch(Exception e) {
		System.out.println("에러 발생했어용");
        e.printStackTrace();
    }
}

private static void method2() throws Exception {
	throw new Exception();
}

finally 블록

finally 블록은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용되요. try-catch 문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally 순으로 구성되요.

try {
	// 실행
} catch(Exception e) {
	// 예외 발생!
} finally {
	// 정상 실행하든, 예외가 발생하든 
    // 마지막에 실행되어요.
}

그럼 try 문 내부에 강제 메서드 종료를 알리는 return문이 있다면 어떻게 될까요? finally 문은 try-catch가 종료되면 무조건 호출되기 때문에 finally가 호출되요.


public class FinallyTest2 {
    public static void execute() {
        method1();
        System.out.println("method1()의 수행을 마치고 main 메서드로 돌아왔어요.");
    }

    private static void method1(){
        try {
            System.out.println("method1이 호출되었어요.");
            return;
        } catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("method1()의 finally 블록이 실행되었어요.");
        }
    }
}

실행 결과

자동 자원 반환 try-with-resources

JDK1.7부터 try-with-resources문이라는 try-catch의 변형문이 추가되었어요. 입출력과 관련된 클래스에 유용해요. 입출력에 사용되는 클래스 중에는 사용 후에 꼭 닫아줘야 하는 것들이 있어요. 그래야 사용했던 자원resources이 반환되기 때문이에요.

try-catch-finally로 처리

try {
	fis = new FileInputStream("source.dat");
    dis = new DataInputStream(fis);
} catch(IOException e) {
	ie.printStackTrace();
} finally {
	// try-catch-finally로 처리는 할 수 있지만....
    // dis.close에서 예외가 발생할 때 처리도 생각해야 해요.
    // 물론 여기에 finally 블록에 try-catch-finally를 추가할 순 있어요.
   	// 하지만 코드가 복잡해지겠죠.
	dis.close();
}

try-with-resources

try (
	fis = new FileInputStream("source.dat");
    dis = new DataInputStream(fis)				// 마지막에 ; 생략
) {
	while (true) {
		score = dis.readInt();
        System.out.println(score);
        sum += score;
    } 
} catch (EOFException e) {
	System.out.println("점수의 총합은 " + sum + "이에요.");
} catch (IOException ie) {
	ie.printStackTrace();
}

이처럼 괄호() 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블록을 벗어나는 순간 자동적으로 호출이 되요. 그 이후에 catchfinally 블록이 수행되요.

물론, try-with-resources 문에 의해 자동으로 close()가 호출될 수 있으려면, 클래스가 AutoCloseable이라는 인터페이스로 구현한 것이어야 해요.

public interface AutoCloseable {
	void close() throws Exception;
}
try(
	// int i = 0;		// Error! AutoCloseable을 상속하지 않았어요
	FileInputStream fis = new FileInputStream("input.txt");
){

} catch(Exception e){

}

그리고 try-catch-resourcesclose()에 대한 예외 처리도 trycatch을 거쳐온 것에 상관 없이 처리해주니 안심하셔도 되요. 이에 대한 예제는 여기 TryWithResources.java를 참고해주세요. 이 예제는 try를 마치고 close()에서 예외를 발생시키는 것과, try 도중 예외가 발생해 이를 처리하고 close()에서 예외를 발생시키는 것이에요.

결론은, 전자는 close()에 대한 예외 처리를 무사히 마치고, 후자는 catch문에 대한 예외 처리를 마치고 이후 Suppress를 표시하면서 close()에 대한 예외를 처리해요. 두 예외가 동시에 발생할 수 없기 때문에 이런 표시를 하는데, 여기서 CloseException은 실제 발생한 예외인 WorkException에 저장되요.

Throwable 인터페이스에는 억제된 예외와 관련된 다음과 같은 메서드가 정의되어 있어요.

void addSuppressed(Throwable exception); 	// 억제된 예외 추가
Throwable[] getSuppressed();				// 억제된 예외(배열) 반환

연결된 예외(Chained Exception)

한 예외가 다른 예외를 발생시킬 가능성이 있어요. 예외A가 예외B를 발생시켰다면, AB의 원인 예외cause exception이라고 해요.

이때 initCasue()를 통해 B에 원인 예외인 A를 원인 예외로 등록해서 throw를 수행할 수 있어요.

이렇게 하는 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서에요. 다수의 AException이나 BException을 같은 부모로 지정해서 한다해도 정확히 어떤 예외가 발생했는지 알 수 없을 뿐더러, 심하면 상속 관계를 변경하는 경우도 있기 때문에 이렇게 연결하는 방식을 취할 수 있게 했어요.

try {
	throw new AException;
} catch(AException ae) {
	BException be = new BException;	
    be.initCause(ae);				// 원인 예외 등록!
    throw be;						// 다시 예외를 던져요!
}

또한 checked 예외unchecked 예외로 바꿀 수 있게 하기 위함이에요. checked예외로 강제한 이유는 프로그래밍 경험이 적은 사람도 보다 견고하게 작성할 수 있도록 유도하기 위한 것이었어요.

그래서 checked 예외가 발생해도 예외를 처리할 수 없는 상황이 하나둘 발생하기 시작했어요. 의미 없는 try-catch문을 추가해서 처리해야하는데, checked 예외 처리를 unchecked로 바꾸면 예외 처리가 선택적으로 바뀌어서 억지로 예외 처리를 하지 않아도 되요.

// checked 예외
void func() throws AExcetpion, BException {
	if(/*A 문제 발생*/)
    	new throw AException();
    // 이렇게 또 추가를 해줘야 했는데...
    if(/*B 문제 발생*/)
    	new throw BException();    
}
// unchecked 예외
void func() throws AExcetpion {
	if(/*A 문제 발생*/)
    	new throw AException();
    // 이제 throws에 따로 추가할 필요가 없게 되요!
    if(/*B 문제 발생*/)
    	throw new RuntimeException(new throw BException());    
}

연결된 예외에 대한 예제는 여기를 참고해주세요. 아니다, 그냥 소스도 올릴게요 ㅎㅎ.

public class ChainedExceptoinEx {
   public static void execute() {
       try {
           install();
       } catch(InstallException e){
           e.printStackTrace();
       } catch(Exception e){
           e.printStackTrace();
       }
   }
   
   private static void install() throws InstallException {
       try {
           startInstall();
           copyFiles();
       } catch(SpaceException se) {
           InstallException ie = new InstallException("설치 중 예외 발생");
           ie.initCause(se);
           throw ie;
       } catch (MemoryException me ) {
           InstallException ie = new InstallException("설치 중 예외 발생");
           ie.initCause(me);
           throw ie;   
       } finally {
           deleteTempFiles();      // 설치에 사용된 임시 파일들 삭제
       }
   }
   
   private static void startInstall() throws SpaceException, MemoryException {
       if(!enoughSpace()) {
           throw new SpaceException("설치할 공간이 부족해요.");
       }
       if(!enoughMemory()){
           throw new MemoryException("메모리가 부족해요.");
           //throw new RuntimeException(new MemoryException("메모리가 부족해요."));
       }
   }

   private static void copyFiles() {
       System.out.println("파일을 복사해요");
   }

   private static void deleteTempFiles() {
       System.out.println("임시 파일을 삭제해요");
   }

   private static boolean enoughSpace() {
       return true;
   }

   private static boolean enoughMemory() {
       return false;
   }
}

class InstallException extends Exception {
   InstallException(String msg){
       super(msg);
   }
}

class SpaceException extends Exception {
   SpaceException(String msg){
       super(msg);
   }
}

class MemoryException extends Exception {
   MemoryException(String msg){
       super(msg);
   }
}


profile
#행복 #도전 #지속성

0개의 댓글