본문 바로가기
Java/Java

예외) 에러와 예외의 차이점, 일반 예외, 실행 예외, 예외 처리

by 박채니 2022. 3. 28.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


오류의 종류

① 컴파일 에러(Compile-Time Error) : 소스 상의 문법 Error

② 런타임 에러(Runtime Error) : 입력 값이 틀렸거나, 배열의 인덱스 범위를 벗어났거나, 계산식의 오류 등에 의해 발생

③ 논리 에러(Logical Error) : 문법 상 문제가 없고, 런타임 에러도 발생하지 않지만, 개발자의 의도대로 작동하지 않음

④ 시스템 에러(System Error) : 컴퓨터 오작동으로 인한 에러 → 소스 구문으로 해결 불가

 

에러와 예외의 차이점

에러(오류) 

- JVM 자체에서 발생하는 오류로 개발자가 해결할 수 없는 치명적인 오류

예외

- 개발자가 해결할 수 있는 미약한 오류 (적절한 코드를 이용하여 수습할 수 있는 오류)

 

에러의 최상위 클래스는 Error 클래스, 예외의 최상위 클래스는 Exception 클래스 입니다.

Exception 클래스는 일반 예외(checked Exception)과 실행 예외(Unchecked Exception)으로 나뉘게 됩니다.

 

일반 예외 (checked Exception)컴파일 전에 예외 발생 문법을 검사하며, 예외처리를 하지 않으면 문법 오류 발생

실행 예외 (Unchecked Excepton)실행할 때 발생하는 예외로, 예외 처리를 따로 하지 않아도 문법 오류 발생 X

 

먼저, 에러가 발생하는 경우에 대해서 알아보겠습니다.


에러

- 프로그램 수행 중 복구 불가한 치명적 상황이 발생한 경우

- 예외처리와 같은 방식으로는 처리할 수 없음

 

☞ OutOfMemoryError

- JVM이 할당한 heap 영역을 모두 소진한 경우

- heap 메모리를 늘리거나, 적절히 코드를 수정

long[] arr = new long[Integer.MAX_VALUE];

@콘솔출력값
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at com.ce.java.ErrorStudy.test1(ErrorStudy.java:10)
	at com.ce.java.ErrorStudy.main(ErrorStudy.java:6)

 

☞ StackOverflowError

- 메소드 호출 스택을 모두 소진한 경우

- 종료 조건이 적절히 설정되지 않은 재귀 메소드 호출 시 발생하기 쉬움

System.out.println("test2");
test2();

@콘솔출력값
test2
test2
test2
...
Exception in thread "main" java.lang.StackOverflowError
...

 

... 이외에 많지만 대표적인 2가지만 소개하였습니다.


예외

- 예외처리를 통해 처리 될 수 있는 미약한 오류

- 프로그램의 비정상 종료를 방지

 

예외 처리 문법 

try, catch, finally(생략 가능)으로 구성

try {
	//예외 발생 가능 코드
} catch(예외 클래스명 참조변수명) {
	//예외 발생 시 처리 코드
} finally {
	//예외 발생 여부에 상관없이 반드시 실행
	//생략 가능
}

 

예외 처리를 하지 않았을 때

public class ExceptionStudy {
	public static void main(String[] args) {
		ExceptionStudy study = new ExceptionStudy();
		study.test1();
		System.out.println("정상종료!");
	}
	
	public void test1() {
		Scanner sc = new Scanner(System.in);
		System.out.print("> 정수 1 입력 : ");
		int a = sc.nextInt();
		System.out.print("> 정수 2 입력 : ");
		int b = sc.nextInt();
		System.out.printf("%d / %d의 결과는 %d입니다.\n", a, b, a/b);
	}
}

@콘솔출력값
> 정수 1 입력 : 4
> 정수 2 입력 : 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.ce.java.ExceptionStudy.test1(ExceptionStudy.java:18)
	at com.ce.java.ExceptionStudy.main(ExceptionStudy.java:8)

분모가 0이 될 수 없기 때문에 4 / 0을 하니 ArithmeticException(실행예외)가 발생 되었습니다.

main 메소드의 "정상종료!"가 출력되지 않았기 때문에 프로그램이 비정상 종료 된 것을 알 수 있죠.

 

이러한 예외가 발생할 수 있는 코드를 try {~}절에 입력해주고, 발생할 수 있는 예외들을 처리해주어 프로그램이 비정상 종료되지 않도록 해보았습니다.

 

예외 처리를 했을 때

public class ExceptionStudy {
	public static void main(String[] args) {
		ExceptionStudy study = new ExceptionStudy();
		study.test1();
		System.out.println("정상종료!");
	}
	
	public void test1() {
		try {
			Scanner sc = new Scanner(System.in);
			System.out.print("> 정수 1 입력 : ");
			int a = sc.nextInt();
			System.out.print("> 정수 2 입력 : ");
			int b = sc.nextInt();
			System.out.printf("%d / %d의 결과는 %d입니다.\n", a, b, a/b);
		} catch(ArithmeticException e) {
			System.out.println("0으로 나눌 수 없습니다.");
		} finally {
			System.out.println("finally");
		}
	}
}

@콘솔출력값
> 정수 1 입력 : 4
> 정수 2 입력 : 0
0으로 나눌 수 없습니다.
finally
정상종료!

발생할 수 있는 예외를 잡아 처리 해주었더니 프로그램이 정상 종료 하는 것을 알 수 있습니다.

 

예외 처리 과정

try {~} 구문 실행

→ 예외가 발생하지 않는다면 catch(){} 블록은 실행하지 않음 - finally 구문이 있다면 실행

→ 예외가 발생한다면 JVM이 먼저 인식하여 발생한 예외 타입의 객체를 생성하여 catch(){} 블록의 매개변수로 전달 - finally 구문이 있다면 실행

만일 JVM이 생성해 넘겨 준 객체 타입을 catch블록이 받을 수 없을 때(알맞은 catch 블록이 존재하지 않을 때)는 예외 처리가 되지 않아 프로그램이 비정상 종료하게 됩니다.

public static void main(String[] args) {
		ExceptionStudy study = new ExceptionStudy();
//		study.test1();
		study.test2();
		System.out.println("정상종료!");
	}
	
public void test2() {
	System.out.println(1);
		
	try {
		System.out.println(2);
			
		String s = null;
		//예외 발생
		System.out.println(s.length());
			
		System.out.println(3);
	} catch(NullPointerException e) {
		System.out.println(4);
	} finally {
		System.out.println("finally");
	}
	System.out.println(5);
}
    
@콘솔출력값
1
2
4
finally
5
정상종료!

예외가 발생하면 하위 코드는 실행하지 않고 catch절로 넘어가기 때문에 "3"은 출력 되지 않았습니다.

또한 finally는 예외가 발생해도, 발생 하지 않아도 반드시 실행되는 구문이기 때문에 "finally"까지 출력된 것을 확인할 수 있습니다.

 

public static void main(String[] args) {
		ExceptionStudy study = new ExceptionStudy();
//		study.test1();
		study.test2();
		System.out.println("정상종료!");
	}
	
public void test2() {
	System.out.println(1);
	
	try {
		System.out.println(2);
		
		//조기리턴
		if(true)
			return;
		
		System.out.println(3);
	} catch(NullPointerException e) {
		System.out.println(4);
	} finally {
		System.out.println("finally");
	}
	System.out.println(5);
}

@콘솔출력값
1
2
finally
정상종료!

try 절에서 조기 리턴 시에도 finally 구문은 실행 되는 것을 확인할 수 있습니다.

finally 절에는 사용한 자원(메모리) 반납 등의 반드시 일어나야 하는 구문이 들어가면 좋겠죠?

 

다중 예외 처리

- catch(){} 블록도 예외 타입에 따라 여러 개를 포함할 수 있음

 

방법 1) 다형성 적용

public class ExceptionStudy {
	public static void main(String[] args) {
		ExceptionStudy study = new ExceptionStudy();
//		study.test1();
//		study.test2();
		study.test3();
		System.out.println("정상종료!");
	}
	
public void test3() {
	try {
		switch((int)(Math.random()*3)) {
		case 0: System.out.println(100/0);	//ArithmeticException 발생
		case 1: String s = null; s.hashCode();	//NullPointerException 발생
		case 2: int[] a = new int[3]; System.out.println(a[3]);	//ArrayIndexOutOfBoundsException 발생
		}
	} catch(ArithmeticException e) {
		e.printStackTrace();	//예외발생 시 callstack 정보 확인
	} catch(NullPointerException e) {
		e.printStackTrace();
	} catch(ArrayIndexOutOfBoundsException e) {
		e.printStackTrace();
	}
}

@콘솔출력값
java.lang.ArithmeticException: / by zero
	at com.ce.java.ExceptionStudy.test3(ExceptionStudy.java:18)
	at com.ce.java.ExceptionStudy.main(ExceptionStudy.java:11)
정상종료!

java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at com.ce.java.ExceptionStudy.test3(ExceptionStudy.java:20)
	at com.ce.java.ExceptionStudy.main(ExceptionStudy.java:11)
정상종료!

java.lang.NullPointerException
	at com.ce.java.ExceptionStudy.test3(ExceptionStudy.java:19)
	at com.ce.java.ExceptionStudy.main(ExceptionStudy.java:11)
정상종료!

위 처럼 catch(){} 블록을 1개 이상 사용할 수 있습니다.

 

하지만 예외 발생 시 실행해야 되는 구문이 모두 같다면, 다형성을 이용하여 해결할 수도 있겠죠?

try {
		switch((int)(Math.random()*3)) {
		case 0: System.out.println(100/0);	//ArithmeticException 발생
		case 1: String s = null; s.hashCode();	//NullPointerException 발생
		case 2: int[] a = new int[3]; System.out.println(a[3]);	//ArrayIndexOutOfBoundsException 발생
		}
	} catch(RuntimeException e) {
		e.printStackTrace();	//예외발생 시 callstack 정보 확인
	}

발생하는 예외(ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException) 모두 RuntimeException의 자손 클래스로 부모 클래스인 RuntimeException 으로 예외를 받아 처리할 수 있습니다.

 

다만, 다형성 적용 시 주의해야 하는 점이 있습니다.

만일 NullPointerException만 처리 구문이 다르다면?

try {
		switch((int)(Math.random()*3)) {
		case 0: System.out.println(100/0);	//ArithmeticException 발생
		case 1: String s = null; s.hashCode();	//NullPointerException 발생
		case 2: int[] a = new int[3]; System.out.println(a[3]);	//ArrayIndexOutOfBoundsException 발생
		}
	} catch(NullPointerException e) {
		System.out.println("NULL NULL 하구먼!");
	}
	catch(RuntimeException e) {
		e.printStackTrace();	//예외발생 시 callstack 정보 확인
	}

여러 개의 catch블록이 있을 때는 실행할 catch(){} 블록의 선택 과정은 항상 위에서부터 확인합니다.

따라서 순서가 굉장히 중요합니다.

 

위 코드의 같은 경우 먼저 NullPointerException이 발생하는 지 확인하고 발생했다면 처리 구문 실행, 발생하지 않았다면 RuntimeException으로 받아 처리하게 되겠죠.

 

만일 catch블록의 순서가 바뀐다면,

RuntimeException에 해당하는 지 먼저 확인하고 처리 구문을 실행하게 되기 때문에 반드시 부모/자식 클래스 간의 catch블록 순서는 범위가 좁은 것부터 차례대로 순서를 지정해줘야 합니다.

try {
		switch((int)(Math.random()*3)) {
		case 0: System.out.println(100/0);	//ArithmeticException 발생
		case 1: String s = null; s.hashCode();	//NullPointerException 발생
		case 2: int[] a = new int[3]; System.out.println(a[3]);	//ArrayIndexOutOfBoundsException 발생
		}
	} catch(RuntimeException e) {
		e.printStackTrace();	//예외발생 시 callstack 정보 확인
	}
	//도달할 수 없는 코드
//	catch(NullPointerException e) {
//		System.out.println("NULL NULL 하구먼!");
//	}

부모 클래스인 RuntimeException으로 받아 모두 처리 하기 때문에 NullPointerException만을 받아 처리하는 것은 불가능하겠죠.

 

방법 2) | or 연산자 이용

	try {
		switch((int)(Math.random()*3)) {
		case 0: System.out.println(100/0);	//ArithmeticException 발생
		case 1: String s = null; s.hashCode();	//NullPointerException 발생
		case 2: int[] a = new int[3]; System.out.println(a[3]);	//ArrayIndexOutOfBoundsException 발생
		}
	} catch(ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException e) {
		e.printStackTrace();	//예외발생 시 callstack 정보 확인
	} 
}

 

 

사용자로부터 2개의 정수를 입력받아서 합/차/곱/나누기몫/나머지를 출력.
 * - 사용자는 숫자가 아닌 문자를 입력할 수 있다.
 * - 계속할 지 여부를 묻고, 처리하세요.

Scanner sc = new Scanner(System.in);
while(true) {

	try {
		System.out.print("> 정수 1을 입력하세요 : ");
		int a = sc.nextInt();
		System.out.print("> 정수 2을 입력하세요 : ");
		int b = sc.nextInt();
		System.out.printf("----------\n"
						+ "합 : %d\n"
						+ "차 : %d\n"
						+ "곱 : %d\n"
						+ "나머지 몫 : %d\n"
						+ "나머지 : %d\n"
						+ "----------\n", (a+b), (a-b), a*b, a/b, a%b);	
	} catch(ArithmeticException e) {
		System.out.println("0으로 나눌 수 없습니다.");
	} catch(InputMismatchException e) {
		System.out.println("숫자만 입력할 수 있습니다.");
	} finally {
		System.out.print("> 계속 하시겠습니까?(y/n) : ");
		char yn = sc.next().charAt(0);

		if(yn == 'n')
			break;
	}
}

@콘솔출력값
> 정수 1을 입력하세요 : 3
> 정수 2을 입력하세요 : 4
----------
합 : 7
차 : -1
곱 : 12
나머지 몫 : 0
나머지 : 3
----------
> 계속 하시겠습니까?(y/n) : y
> 정수 1을 입력하세요 : 5
> 정수 2을 입력하세요 : 6
----------
합 : 11
차 : -1
곱 : 30
나머지 몫 : 0
나머지 : 5
----------
> 계속 하시겠습니까?(y/n) : n
정상종료!