Java & Kotlin

Java Checked vs Unchecked Exception: 예외 처리의 원칙과 트랜잭션 롤백

백엔드 유성 2024. 3. 3. 20:42

Checked Exception과 Unchecked Exception에 대한 이해

Java에서 예외 처리(Exception Handling)는 매우 중요한 요소입니다.

저는 아키텍처 설계에서 가장 중요하다고 생각되는게 Exception 처리라고 생각합니다.

 

예외는 프로그램 실행 중에 발생할 수 있는 비정상적인 상황을 나타내며, 이 예외를 적절히 처리하지 않으면 프로그램이 예기치 않게 종료될 수 있습니다.

Java에서는 예외를 두 가지 유형으로 나눌 수 있는데, 바로 Checked ExceptionUnchecked Exception입니다. 이번 글에서는 두 가지 예외의 차이점과 사용 사례, 그리고 언제 어떤 예외를 사용하는 것이 적절한지에 대해 알아보겠습니다.

 

Checked Exception이란

Checked Exception은 컴파일 타임에 발생할 수 있는 예외로, Java 컴파일러가 이 예외를 체크하고 처리하도록 강제합니다. 즉, 이러한 예외는 반드시 try-catch 블록으로 처리하거나, 메서드 선언부에서 throws 키워드를 사용하여 해당 예외를 던져야 합니다.

 

주요 특징

  • 컴파일 타임에서 강제 처리: Checked Exception은 발생 가능성이 있는 예외를 컴파일 타임에 미리 확인하여, 예외 처리를 강제합니다. 처리하지 않으면 컴파일 오류가 발생합니다.
  • 예상 가능한 예외: Checked Exception은 주로 외부 자원(파일, 네트워크, 데이터베이스)과 관련된 작업에서 발생할 수 있는, 예측 가능한 예외 상황을 처리하는 데 사용됩니다.

 

Checked Exception 예시

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            File file = new File("somefile.txt");
            FileReader reader = new FileReader(file);
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        }
    }
}

 

위 코드에서 FileReaderFileNotFoundException이라는 Checked Exception을 발생시킬 수 있습니다. 이 예외는 반드시 처리되거나, 메서드 선언에 throws로 던져야 합니다.

 

대표적인 Checked Exception들

  • IOException
  • SQLException
  • ClassNotFoundException
  • FileNotFoundException

 

Unchecked Exception이란

Unchecked Exception은 런타임에 발생하는 예외로, 컴파일러가 예외 처리를 강제하지 않습니다. 즉, 이 예외는 try-catch 블록으로 처리하지 않아도 컴파일 오류가 발생하지 않으며, 실행 중에 예기치 않게 발생할 수 있습니다.

 

주요 특징

  • 런타임에서 발생: Unchecked Exception은 주로 프로그래머의 실수나 프로그램의 논리적 오류로 인해 발생하며, 런타임에만 발생하는 예외입니다.
  • 예외 처리 강제 없음: 컴파일러가 강제로 예외 처리를 요구하지 않기 때문에, 예외를 처리하지 않아도 컴파일에는 문제가 없지만, 런타임에서 예외가 발생할 수 있습니다.

 

Unchecked Exception 예시

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.println(numbers[3]); // ArrayIndexOutOfBoundsException 발생
    }
}

 

위 코드에서 ArrayIndexOutOfBoundsException은 Unchecked Exception입니다. 예외 처리를 강제하지 않기 때문에 컴파일 오류가 발생하지 않지만, 실행 중에 인덱스 범위를 벗어난 접근으로 인해 예외가 발생합니다.

 

대표적인 Unchecked Exception들

  • RuntimeException
  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • ArithmeticException
  • IllegalArgumentException

 

Checked Exception과 Unchecked Exception의 차이점

특징 Checked Exception Unchecked Exception
처리 시점 컴파일 타임에 확인됨 런타임에 발생
처리 강제 여부 예외 처리를 강제 (try-catch 또는 throws 필요) 예외 처리 강제 아님 (try-catch 선택 사항)
예외 발생 이유 예측 가능한 예외 상황 (파일, 네트워크, DB 등 외부 자원) 프로그래머 실수 또는 논리적 오류 (null 참조, 배열 범위 초과 등)
대표적인 예외 클래스 IOException, SQLException, ClassNotFoundException NullPointerException, ArrayIndexOutOfBoundsException

 

언제 어떤 예외를 사용해야 할까?

1. Checked Exception을 사용하는 경우:

  • 외부 자원 사용: 파일 입출력, 네트워크 통신, 데이터베이스 연결과 같은 작업에서 발생할 수 있는 예외는 개발자가 예상 가능한 예외 상황입니다. 이러한 예외는 반드시 처리되어야 하므로, Checked Exception이 적합합니다.
  • 필수적 예외 처리: 파일을 읽거나 네트워크 연결이 실패하는 등의 상황은 애플리케이션에서 반드시 예외 처리되어야 하는 중요한 이벤트입니다. 이 경우 Checked Exception을 사용하여 예외 처리를 강제하는 것이 좋습니다.
  • 예시: 파일을 읽는 작업에서 파일이 없을 경우 발생하는 FileNotFoundException

2. Unchecked Exception을 사용하는 경우:

  • 프로그래밍 오류: NullPointerException이나 ArrayIndexOutOfBoundsException과 같은 예외는 주로 프로그래머의 실수로 발생합니다. 이 경우는 컴파일 타임에서 확인할 수 없으며, 모든 가능성을 고려해 예외 처리를 강제할 필요가 없습니다.
  • 논리적 오류: IllegalArgumentException처럼 잘못된 값이 메서드에 전달되었을 때 발생하는 예외는 논리적 오류로 간주할 수 있으며, 런타임에 발생할 가능성이 있는 경우입니다.
  • 예시: 배열 인덱스를 잘못 참조하거나 null 참조로 인해 발생하는 NullPointerException

 

예외 처리 에 대한 모범 사례

1. 필요한 예외만 처리

  • Checked Exception이 발생할 가능성이 있는 코드에서만 예외 처리를 수행하고, 불필요한 곳에서 예외를 남용하지 않는 것이 중요합니다.
  • 또한 현재는 Checked Exception를 쓰지 않는것이 추세입니다. 이유는 가독성, 형식적인 try-catch 블럭, 함수형 프로그래밍과의 호환성 문제 등이 있습니다.

2. 적절한 예외 전환

  • 특정 예외 상황에서 의미 있는 커스텀 예외를 정의해 사용하는 것도 좋은 방법입니다. 예를 들어, 특정 도메인에 맞는 예외 처리를 위해 Checked Exception을 잡아서 Unchecked Exception으로 전환할 수 있습니다.

3. 예외 메시지 명확화:

  • 예외를 처리할 때 적절한 로그와 명확한 예외 메시지를 남기는 것이 중요합니다. 이를 통해 디버깅이나 유지보수 시 예외 발생 원인을 더 쉽게 파악할 수 있습니다.

 

결론

Checked ExceptionUnchecked Exception은 Java에서 예외를 처리하는 두 가지 주요 방식입니다. Checked Exception은 컴파일 타임에 강제로 처리해야 하며, 주로 외부 자원과 상호작용할 때 발생하는 예상 가능한 예외를 다룹니다. 반면, Unchecked Exception은 주로 프로그래밍 오류나 논리적 문제로 인해 발생하며, 런타임에서 처리되는 예외입니다.

 

실무에서 Checked Exception은 코드의 복잡성을 증가시키고 불필요한 예외 처리로 이어질 수 있기 때문에, 가능하면 Unchecked Exception을 사용하는 것이 좋습니다. Unchecked Exception은 코드의 가독성을 높이고, 예외 처리를 더 유연하게 할 수 있습니다.

 

조슈아 블로크(Java Collection Framework 개발자)
“Checked Exception은 복구 가능한 상황에서 사용하고, 프로그래밍 오류에 대해서는 런타임 예외를 사용하라.”

 

Kotlin에서는 이러한 불필요한 예외 처리 문제를 해결하기 위해 Checked Exception 개념이 없습니다. 즉, Kotlin에서는 모든 예외가 Unchecked Exception으로 처리되며, 예외 처리를 강제하지 않습니다.

 


추가로 @Transactional에 관련하여 CheckedException, UncheckedException의 처리는 다소 다릅니다.

API Java doc
'기본적으로 트랜잭션은 RuntimeException과 Error에서 롤백되지만, Checked Exception(비즈니스 예외)에서는 롤백되지 않습니다. 자세한 설명은 org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)를 참조하십시오.'

 

  • Unchecked Exception(즉, RuntimeException과 그 하위 클래스들)이나 Error가 발생하면 자동으로 트랜잭션이 롤백됩니다.
  • Checked Exception(즉, Exception의 하위 클래스들)은 자동으로 롤백되지 않습니다. Checked Exception은 일반적으로 비즈니스 로직에서 발생하는 예외로 간주되며, 이를 복구하려고 시도할 수 있기 때문입니다.
  • 만약 Checked Exception에서도 트랜잭션을 롤백하고 싶다면, 이를 명시적으로 설정해주어야 합니다 (rollbackFor 속성 등).

 

감사합니다.