본문 바로가기
카테고리 없음

예외처리1

by 낭만 코딩 2024. 9. 18.

1. 예외란

2. 예외 클래스

3. 예외 처리 방법

 

 

 

1. 예외란

프로그램 실행 중에 무엇인가 의해서 오작동을 하거나 비 정상적으로 종료되는 경우가 있습니다. 이를 우리는 흔히 에러(error)라고 말합니다. 보통 에러가 발생하면 프로그램은 에러가 발생한 곳에서 멈추게 되는데, 자바 프로그램에서 에러는 JVM에서 실행 중 문제가 생긴 것이므로 이런 에러는 개발자가 대처할 수 있는 방법은 없습니다.

에러는 그 종류와 프로그램에 미치는 영향이 각기 달라서 대처하는 방법도 다릅니다. 

예외란 에러 중에서 대처할 수 있는 에러라고 말할 수 있습니다. 예외 처리는 예외를 방치하거나 에러로 인한 프로그램 수행 결과를 잘못 되게 하는 것이 아니라, 에러를 없애고 정상적으로 처리하는 방법을 제공하는 것입니다.

 

오류와 에러, 예외 이 세가지 용어에 먼저 정리해 보겠습니다.

오류 : 에러와 예외 포함
에러 : 프로그램 코드에 의해서 해결 할 수 없는 심각한 오류
예외 : 프로그램 코드에 의해서 해결 할 수 있는 오류

 

 

이 에러를 발생 시점에 따라 컴파일 에러 (compile error)와 런타임 에러 (runtime error) 로 나눌 수 있는데, 글자 그대로 '컴파일 에러' 는 컴파일 할 때 발생하는 에러이고 프로그램의 실행 도중에 발생하는 에러를 '런타임 에러'라고 합니다. 소스코드(.java)를 컴파일할 때 컴파일러가 소스에 대해 오타나 잘못된 문법, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지를 미리 알려 줍니다. 컴파일러가 알려 준 에러들을 모두 수정해서 컴파일을 성공적으로 마치고 나면, 클래스 파일 (.class) 이 생성되고, 생성된 클래스 파일을 실행할 수 있게 되는 것입니다. 하지만, 컴파일을 에러없이 성공적으로 마쳤다고 해서 프로그램의 실행 중에도 에러가 발생하지 않는 것은 아닙니다.

컴파일러가 소스코드의 기본적인 오류는 컴파일시에 모두 걸러 줄 수는 있지만, 실행도중에 발생할 수 있는 잠재적인 오류까지 검사할 수는 없기 때문에 컴파일은 잘되었어도 실행 중 에러에 의해서 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있습니다.

 

여러분들은 이미 실행도중에 발생하는 런타임 에러를 여러 번 경험했을 것입니다. 예를 들면 갑자기 프로그램이 실행을 멈추거나 종료되는 경우 등을 말입니다. 이런 런타임 에러를 방지하기 위해서는 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 미리 예측하여 이에 대한 준비를 해야 합니다.

자바에서는 실행(runtime) 시 발생할 수 있는 프로그램 오류를 에러 (error)와 예외(exception) 두 가지로 구분하고 있는데, 에러는 메모리 부족 (OutOfMemoryError)이나 스택오버플로우(StackOverflowError) 와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 처리될 수 있는 비교적 덜 심각한 오류입니다.

 

에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있게됩니다.

 

 

2. 예외 클래스

자바는 모든 오류를 클래스로 제공하고 있는데, 모든 비정상적인 동작을 Throwable 라는 클래스로 표현하고 다시 Error Exception 클래스로 나눕니다. Exception 클래스 자손들의 예외가 발생하면 덜 치명적인 오류라고 보고 프로그램을 강제로 종료하는 것보다는 오류 메시지 등을 내보내고 오류 발생 가능성이 있는 부분에 대해서 미리 프로그램으로 처리를 해주는 것입니다. 즉 예외처리의 대상은 Exception 클래스 및 자손 클래스들이 됩니다.

 

예외 클래스는 java.lang 패키지 내에 속하지만 IOException 클래스 및 그 하위 클래스는 java.io 패키지에 속합니다.

 

Error 클래스의 하위 클래스는 다음과 같습니다. 그외에도 무수히 많은 하위 클래스가 있지만 자세한 내용은 API 를 참조하기 바랍니다.

 

 

이제 Exception 클래스의 하위 클래스들을 살펴보겠습니다.

여기에 기술한 객체가 Exception 객체의 전부는 아니지만, 가장 많이 다루어지는 예외 객체들만 살펴보도록 하겠습니다.

 

 

 

예외 클래스 예외 발생 원인
RuntimeException 실행 중 예외가 발생
CloneNotSupportedException 객체가 복제되지 않은 상태에서 복제 시도
InterruptedException 쓰레드가 중지된 경우
NoSuchMethodException 메서드가 없는 경우
ClassNotFoundException 클래스를 찾지 못하는 경우
IOException 입출력관련 예외가 발생하는 경우

 

자바는 필수적으로 예외 처리를 해야합니. 그런데 지금까지 우리는 예외처리 없이 프로그램을 작성했습니다. 예외 중 RuntimeException 은 예외 처리 생략이 가능한 예외가 있는데, 그래서 우리는 이제 까지 예외처리를 생략했던 것입니다.

 

RuntimeException 의 하위 클래스

예외 클래스 예외 발생 원인
ArithmeticException 0으로 나누는 경우
NegativeArraySizeException 배열의 크기가 음수인 경우
NullPointerException null 객체에 접근하는 경우
ClassCastException 객체가 형변환이 잘못된 경우
IndexOutOfBoundException 인덱스의 범위를 벗어나는 경우

 

 

3. 예외 처리 방법

try 블록 안에 예외가 발생할 가능성이 있는 문장 코드를 넣고, 예외가 발생하면 catch 블록에서 처리합니다.

catch 블록은 예외가 발생하지 않으면 실행되지 않습니다.

 

try ~ catch문의 구조

try {
    // 예외가 발생할 가능성이 있는 문장 코드
} catch (Exception1 e1) {
    // Exception1이 발생할 경우, 실행될 문장
} catch (Exception2 e2) {
    // Exception2이 발생할 경우, 실행될 문장
...
} catch (ExceptionX eX) {
    // ExceptionX가 발생할 경우, 실행될 문장
}

 

하나의 try 블록 밑에는 여러 종류의 예외를 처리할 수 있도록 여러 개의 catch블록이 올 수 있는데, 이 중 발생한 예외의 종류와 일치하는 한 개의 catch블록만 수행되고, try~catch문은 종료됩니다. 발생한 예외의 종류와 일치하는 catch블록이 없으면 예외는 처리되지 않습니다. 주의할 점은 try~catch문은 if문이나 for, while문과 같이 한줄이라고 해서 중괄호를 생략할 수 없습니다.

 

package example;

public class ExceptionEx0 {

	public static void main(String[] args) {
		System.out.println(1);
		System.out.println(2);
		System.out.println(3/0);
		System.out.println(4);
		System.out.println(5);
		System.out.println(6);
	}
	
}

 

실행 결과

1
2
Exception in thread "main" java.lang.ArithmeticException : / by zero
        at chapter11.ExceptionEx0.main(ExceptionEx0.java:8)

 

 

위 예제는 3/0 연산을 하는 경우 예외가 발생하는 예제입니다. 메시지를 보면 ArithmeticException이 발생하였고, / by zero 라고 출력된것을 알 수 있습니다. 1, 2까지 출력되다가 3/0에서 출력되지 않고 프로그램이 종료된 것입니다.

이 코드를 try~catch문을 이용해 수정해보도록 하겠습니다.

 

package example;

public class ExceptionEx1 {

	public static void main(String[] args) {
		System.out.println(1);
		try {
			System.out.println(2);
			System.out.println(3/0);
			System.out.println(4);
		} catch(ArithmeticException e) {
			System.out.println(5);
		}
		System.out.println(6);
	}
	
}

 

실행 결과

1
2
5
6

 

ExceptionEx1 클래스에서는 main()메서드 안에서 1을 출력하고 try 블록안에 2를 출력하고 30으로 나누고 있는데 마찬가지로 여기서 exception이 발생합니다. try 블록안에서 예외가 발생했기 때문에 catch 블록이 실행되어 4를 출력하는 구문은 건너띄고 catch 블록 안의 5가 출력 되었습니다. 그리고 try~catch 구문이 끝나고 6이 출력됩니다.

만약 이 예제에서 try~catch 구문을 사용하지 않았거나, 예외가 발생할 가능성이 있는 구문을 try블록에 넣지 않았거나, catch 구문에 ArithmeticException을 넣지 않았으면, 이전 예제와 마찬가지로 2까지만 출력되고 프로그램은 비정상적으로 종료되었을 것입니다.

try~catch문은 예외가 발생한 경우와 발생하지 않은 경우의 실행문의 실행순서가 달라지는데, 이 두 가지의 경우를 다시 정리해보겠습니다.

 

* try 블록 안에서 예외가 발생한 경우
  - 발생한 예외와 일치하는 catch 문이 있는지 확인한다.
  - 만약 일치하는 catch문이 있다면, 해당 catch문의 블럭 내의 실행문들을 실행하고, 전체 try-catch구문이 종료된다. 만약 일치하는 catch문이 없으면 예외 처리를 하지 못한다.

* try 블록 안에서 예외가 발생하지 않은 경우
  - catch 구문을 모두 확인하지 않고, 전체 try-catch 구문일 종료된다.

 

package example;

public class ExceptionEx2 {

	public static void main(String[] args) {
		System.out.println(1);
		try {
			System.out.println(2);
			System.out.println(3);
			System.out.println(4);
		} catch(ArithmeticException e) {
			System.out.println(5);
		}
		System.out.println(6);
	}
	
}

 

위 예제는 try블록 내에서 예외가 발생하지 않았기 때문에 catch문의 실행문이 실행되지 않습니다.

 

 

다중 catch

여러개의 catch문이 존재하는 구문으로 발생된 예외별로 다른 예외처리를 할 수 있습니다.

package example;

public class ExceptionEx3 {

	public static void main(String[] args) {
		
		try {
			int[] arr = {1,2,3};
			System.out.println(arr[3]); // 예외 발생
			System.out.println(3/0); // 예외 발생
			Integer.parseInt("a"); // 예외 발생
		} catch(ArithmeticException e) {
			System.out.println("0으로 나눌 수 없음");
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("인덱스 범위 초과");
		}
	}
	
}

 

실행 결과

인덱스 범위 초과

 

이 예제는 배열의 인덱스 범위가 초과되는 코드와 30으로 나누고, 숫자가 아닌 문자열을 숫자로 변환하는 총 3가지의 예외가 발생하는 코드입니다. 그 중 맨 위에 있는 arr[3] 여기서 예외가 발생하고 해당 예외 ArrayIndexOutOfBoundsException의 catch 블록이 실행되게 됩니다. 

그래서 두번째 예외 (3/0)과, 세번째 예외 (문자열을 정수로 변환)은 실행되지 않게 됩니다.

 

그럼 try ~ catch 블록의 예외가 발행하는 부분을 수정해보도록 하겠습니다.

System.out.println(arr[2]);
System.out.println(3/1);
Integer.parseInt("a"); // 예외 발생

 

세번째 부분만 예외가 발생하고, 위 두개는 예외가 발생하지 않도록 수정했습니다.

그러고 실행을 해보시면, 예외가 발생하게 됩니다.

3
3
Exception in thread "main" java.lang.NumberFormatException: For input string: "a"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:668)
    at java.base/java.lang.Integer.parseInt(Integer.java:786)
    at test/example.ExceptionEx3.main(ExceptionEx3.java:11)

 

그럼 이제 어떻게 해야할까요?

네, 저 NumberFormatException 을 catch 블록에 추가해주면 됩니다.

package example;

public class ExceptionEx3 {

	public static void main(String[] args) {
		
		try {
			int[] arr = {1,2,3};
			System.out.println(arr[2]);
			System.out.println(3/1);
			Integer.parseInt("a"); // 예외 발생
		} catch(ArithmeticException e) {
			System.out.println("0으로 나눌 수 없음");
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("인덱스 범위 초과");
		} catch (NumberFormatException e) {
			System.out.println("숫자포맷 예외발생");
		}
	}
	
}

 

실행 결과

3
3
숫자포맷 예외발생

 

예외는 발생하지만 프로그램이 종료되지 않고, 정상적으로 실행되었습니다.

 

그런데, 그럼 이렇게 매번 어떤 예외가 발생할지 미리 예측해서 catch 블록에 하나하나 일일히 다 적어야 할까요? 물론 그렇게 코딩해도 되지만, 사실 우리는 어떤 예외가 발생하는지 세세히 분류해서 처리하는 경우보다는 어떤 오류던지, 프로그램이 중지되지 않고, 처리되도록 하는것이 더 중요하기 때문에 아래처럼 처리하는 경우도 많습니다.

package example;

public class ExceptionEx3 {

	public static void main(String[] args) {
		
		try {
			int[] arr = {1,2,3};
			System.out.println(arr[2]);
			System.out.println(3/1);
			Integer.parseInt("a"); // 예외 발생
//		} catch(ArithmeticException e) {
//			System.out.println("0으로 나눌 수 없음");
//		} catch (ArrayIndexOutOfBoundsException e) {
//			System.out.println("인덱스 범위 초과");
//		} catch (NumberFormatException e) {
//			System.out.println("숫자포맷 예외발생");
//		}
		} catch (Exception e) {
			System.out.println("예외 발생");
		}
	}
	
}

 

finally 구문

finally는 마지막에 실행된다는 의미로, 예외 없이 정상적으로 실행이 되던, 예외가 발생하던 무조건 실행되는 구문입니다.

package example;

public class ExceptionEx4 {

	public static void main(String[] args) {
		System.out.println("DB연결 시작");
		try {
			System.out.println("DB작업");
			System.out.println(3/0);
		} catch(Exception e) {
			System.out.println("DB작업 중 예외발생");
		} finally {
			System.out.println("DB연결 종료");
		}
	}
	
}

 

실행 결과

DB연결 시작
DB작업
DB연결 종료

 

이 예제는 try구문 안에서 0으로 나누는 연산때문에 예외가 발생합니다. 따라서 catch문의 “DB작업 중 예외발생이 출력되는데, 이렇게 예외가 발생하는 경우도 finally문의 “DB연결 종료는 출력이 됩니다. \

만약 이 예외가 발생하는 코드를 제거해서 예외가  발생하지 않는 경우에도 무조건 실행이 되는 구문이 바로 finally 구문인데, 대표적으로 DataBase 작업을 하는 경우 DB 연결 후 작업을 하고, 작업이 끝나면 DB연결을 종료하는 형태로 프로그램을 작성합니다.

만약 DB 연결 후 작업 중 예외가 발생하면 DB 연결을 종료하지 못하는 문제가 생기고, 서버 자원을 낭비하는 문제가 발생하게 됩니다. 그래서 이런 경우 예외가 발생하던 발생하지 않던 무조건 DB 연결을 종료하는 구문이 필요하게 됩니다.