Checked Exception, Unchecked Exception 그리고 예외 처리

Jeonghwa·2024년 1월 6일
1

이 주제는 자바개발자라면 무조건 알아야하는 기본적인 내용입니다. 최근 클린코드 7장 오류처리을 읽으면서 정리의 필요성을 느꼈고 블로그에 글을 작성하게 되었습니다.


1. 프로그램 오류

프로그램은 어떤 원인에 의해 오작동 또는 비정상적 종료가 일어나는 경우가 있습니다. 이러한 결과를 초래하는 원인을 프로그램 오류 또는 에러라고하며 발생 시점에 따라 3가지로 나뉩니다.

  • 컴파일 에러 : 컴파일 시에 발생하는 오류
  • 런타임 에러 : 실행 시 발생하는 오류
  • 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

컴파일러는 소스코드(.java) 오타, 잘못된 구문, 자료형 체크 등 기본적인 검사를 수행하여 오류가 있는지 알려줍니다. 이때 발생하는 것이 컴파일 에러입니다. 그리고 이 컴파일 과정이 끝나면 클래스 파일(.class)이 생성되고 생성된 클래스 파일을 실행할 수 있게 됩니다.
하지만, 컴파일이 잘되었어도 실행 도중 발생할 수 있는 잠재적 오류까지 검사할 수 없기 때문에 실행 도중 오류가 발생할 수 있습니다. 이때 발생하는 것이 런타임 에러이며 자바에서는 이를 에러(Error)예외(Exception)로 구분합니다.

2. 에러 vs 예외

  • 에러(Error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외(Exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

에러는 메모리 부족(OutOfMemoryError)나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 적절한 코드(try-catch)를 미리 작성해 놓음으로써 프로그램의 비정상적 종료를 막을 수 있는 비교적 덜 심각한 오류입니다.

3. 에러와 예외 클래스 계층 구조

자바에서는 실행 시 발생할 수 있는 오류(Runtime Error)ExceptionError 클래스로 정의하였습니다. 모든 조상은 Object클래스 이므로 이 역시 Object클래스의 자손들입니다.

그리고 Exception클래스는 두 그룹으로 나눌 수 있습니다.

  1. Exception클래스와 자손들 (RuntimeException클래스와 그 자손들 제외) ➡️ Checked Exception
  2. RuntimeException클래스와 자손들 ➡️ Unchecked Exception

4. Checked Exception vs Unchecked Exception

CheckedException(Exception클래스와 자손들)

  • 컴파일러가 예외처리를 강제(확인)하는 예외
  • 사용자(외부)의 동작에 의해서 발생될 수 있는 예외
  • ex) 존재하지 않는 파일의 입력(FileNotFoundException), 클래스 이름을 잘못 입력(ClassNotFoundException) 등

Checked Exception은 Java에만 있으며 다른 언어(C#, C++, Python 등)에는 없습니다.

UncheckedException(RuntimeException클래스와 자손들)

  • 컴파일러가 예외처리를 강제(확인)하지 않는 예외
  • 프로그래머의 실수에 의해 발생될 수 있는 예외
  • ex) null인 참조변수의 멤버호출(NullPointException), 배열의 범위를 벗어난 동작(ArrayIndexOutOfBoundException) 등

💡 중요 : 가장 큰 차이점은 예외에 대한 처리를 컴파일 시점에 강제하냐 안하냐의 차이입니다. (예외를 언제 던지냐의 차이가 아닙니다.) 코드에 Checked Exception에 대한 예외 처리가 안되어 있을 때 발생하는 건 컴파일 에러입니다.

예외라고 부르는 Exception은 오직 Runtime에만 발생합니다.

5. 왜 예외처리를 강제하지 않을까?

RuntimeException클래스와 자손들은 프로그래머에 의해 실수로 발생하는 것들이기 때문에 강제하지 않습니다.
만일 RuntimeException클래스들에 속하는 예외에도 예외처리를 필수로해야한다면? 아래와 같이 참조변수와 배열이 사용되는 모든 곳에 예외처리를 해주어야할 것입니다.

try {
	int[] arr = new int[10];
    System.out.println(arr[0]);
} catch(ArrayIndexOutOfBoundException ae) {
	...
} catch(NullPointerException ne) {
	...
}

이외에도 예외처리가 불필요한 경우 try-catch문을 무조건 넣어야 하므로 코드가 복잡해지게 됩니다.

사실 CheckedException이 처음 나오게 된 이유는 견고한 프로그램 작성이란 이유도 있지만 예외가 던져진 상황에서 개발자가 handling할 수 있을거란 믿음이 있었기 때문입니다. 예외를 복구 할 수 있는 방법이 확실하다면 괜찮지만, 추후 해당 메서드를 사용하는 개발자가 알 수 있을지는 미지수이기 때문에 UncheckedException을 사용하자는 것이 중론입니다.

6. 예외처리 전략

예외 처리란, 프로그램 실행 시 예외 발생에 대비한 코드를 작성하는 것을 말합니다.

6-1. try-catch

try {
	// 예외가 발생할 가능성이 있는 코드
} catch(Exception1 e1) {
	// 예외1이 발생했을 경우, 처리하기 위한 코드
} catch(Exception2 e1) {
	// 예외2이 발생했을 경우, 처리하기 위한 코드
} catch(Exception3 e1) {
	// 예외3이 발생했을 경우, 처리하기 위한 코드
} finally {
	// 예외 발생여부에 관계없이 항상 수행되어야하는 문장을 넣는다. (선택 사항)
}

여러 종류의 예외를 처리할 수 있도록 여러 catch 블럭을 작성할 수 있으며, 예외의 종류와 일치하는 단 한개의 catch블럭만 수행됩니다.

  • 예외의 일치여부는 instanceof연산자를 이용하며, 결과가 true인 catch블럭을 만날 때까지 검사합니다. (자손 타입도 catch가능)
  • 만일 일치하는 예외가 없다면, 프로그램은 비정상적으로 종료됩니다.

finally 블럭은 선택적으로 덧붙여 사용할 수 있으며 예외가 발생한 경우 try - catch - finally 순으로, 예외가 발생하지 않은 경우는 try - finally 순으로 실행됩니다.

  • 만일 일치하는 예외가 없어도 try - finally 순으로 실행된 후 비정상적으로 종료됩니다.
  • try블럭이던 catch블럭이던 안에서 return을 만나도 finally블럭은 실행됩니다.

try-catch를 활용한 예외 복구

for(int i = 0; i < MAX_RETRY; i++) {
	try {
		// 예외가 발생할 가능성이 있는 코드
        return; // 작업 성공 시 return
    } catch(SomeException e) {
    	// 로그 출력 및 정해진 시간만큼 대기
    } finally {
    	// 정리 작업
    }
}
throw new RetryFailedException(); // 최대 시도 횟수 넘기면 직접 예외 발생

예외복구의 핵심은 예외가 발생해도 프로그램이 정상적으로 진행되게 하는 것입니다.

  • 위 예시는 예외가 발생하면 일정시간 대기 후 재시도를 반복하며, 정해진 횟수를 넘기면 예외를 직접 발생시킵니다.
  • 재시도를 통해 프로그램은 정상적으로 진행되거나, 다른 흐름으로 유도시키도록 구현하면 예외가 발생했을 지라도 프로그램의 비정상적인 종료는 피할 수 있습니다.

6-2. 메서드에 예외 선언하기(예외처리 회피)

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

메서드의 선언부에 예외를 선언함으로 써 해당 메서드를 사용하는 클라이언트 코드는 해당 예외들을 처리해야함을 알 수 있습니다.

  • 해당 예외 뿐 아니라 자손타입의 예외까지 발생할 수 있다는 점을 주의해야합니다.

사실 이는 예외를 처리하는게 아닌, 예외 발생 시 자신을 호출한 클라이언트 코드에게 예외를 전달하여 처리를 떠맡기는 것입니다.

  • 만일 전달받은 클라이언트 코드마저 예외를 처리하지 않는다면, 프로그램은 비정상적으로 종료됩니다.

일반적으로 반드시 처리해주어야 하는 예외들만 선언하기 때문에, UncheckedException클래스는 적지 않지만 선언한다고 해서 문제가 되지는 않습니다.

6-3. 연결된 예외(예외 전환)

예외를 전환시키는 이유에는 2가지 이유가 있습니다.

1️⃣ 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해

catch(SpaceException e){
	InstallException ie = new InstallException("설치 중 예외발생");
    ie.initCause(e);
    throw ie;
}

InstallException의 원인예외로 SpaceException등록하여 처리하는 쪽에서 정확한 원인을 파악할 수 있도록 합니다.

2️⃣ checked exception을 unchecked exception으로 바꾸기 위해

throw new RuntimeException(new MemoryException("메모리가 부족합니다."));

checked exception이 발생해도 예외를 처리할 수 없는 상황이 발생할 수 있습니다. 이때 의미없는 try-catch문 작성 또는 다른 계층으로 처리를 넘기기보다 uncheckd예외로 바꾸면 예외처리가 선택적이되므로 억지로 예외를 처리하지 않아도됩니다.


(추가) Exception의 Rollback 여부

Checked Exception, Unchecked Exception발생 시 트랜잭션의 롤백 여부가 다릅니다.
Checked Exception의 경우 롤백하지 않고 commit 후 예외를 던지지만, Unchecked Exception의 경우 롤백 후 예외를 던집니다.

왜 Checked Exception은 롤백해주지 않는걸까?

CheckedException은 예외처리가 컴파일러에 의해 강제되어있기 때문에 롤백 혹은 다른 처리를 개발자가 진행할 수 있는 기회가 있습니다. 하지만 Unchecked Exception은 예외 처리가 되어있지 않은 경우가 훨씬 많습니다. 이 경우 commit은 치명적일 수 있기때문에 롤백을 수행해준다라고 합니다.


참고 :

profile
backend-developer🔥

0개의 댓글

관련 채용 정보