공개 글

예외 처리(Exception Handling)

영발개발 2025. 11. 14. 08:00

 JAVA에서 예외가 무엇인지부터 예외 처리를 어떻게 하는지, 또 이게 실무에서 왜 중요한지에 대해서 정리해볼까 한다.


목차

- 에러와 예외의 차이

- 예외의 종류

- 예외 처리 방법

- 예외 처리의 중요성

에러와 예외의 차이
에러(Error)

 메모리 부족이나 시스템 오류처럼 복구 불가능한 심각한 문제를 뜻한다. 시스템 수준에서 발생하여 프로그램 종료로 이어지는 경우가 많고, 에러는 컴파일 시점에 확인할 수 없으며 주로 실행 중에 발생한다. 즉 예측이 불가능해 개발자가 따로 코드 수정과 같은 것들로 처리할 수 없다.


ex)  OutOfMemoryError, StackOverflowError
 

예외(Exception)

 개발자가 코드를 통해 처리할 수 있는 비교적 덜 심각한 오류를 뜻한다. 정상적인 프로그램 흐름을 방해하는 이벤트로 컴파일 시점에 체크되는 경우와 실행 중에 발견되는 경우가 있다. 에러와 다르게 예측이 가능하기 때문에 'try-catch'와 같은 예외처리 구문을 통해 코드를 수정하여 수습이 가능하다.
 
ex ) NullPointerException, IOException, IllegalArgumentException


예외의 종류
출처 : https://sorjfkrh5078.tistory.com/104

 
 Exception은 크게 RuntimeException과 그 외 다른 Exception으로 나눌 수 있는데, OtherException은 컴파일 시점에 확인되는 Checked Exception이고 RuntimeException은 컴파일 시점이 아닌 런타임 시점에 발생하는 Unchecked Exception이다.
 
 즉 Checked Exception은 컴파일 시점에 확인되기 떄문에 예외 처리를 해주지 않으면 컴파일 자체가 실패해서 프로그램 실행이 되지 않고, Unchecked Exception은 컴파일 시점에는 확인이 안되기 때문에 실행 중에 에러가 터질 수 있다.


<요약>

  Checked Exception Unchecked Exception
발생 시점컴파일 시점런타임 시점
예외 처리 필요 여부반드시 필요선택 사항
대표 클래스IOException, SQLExceptionNullPointerException, ArithmeticException
원인외부 요인 (파일, 네트워크 등)개발자 실수 (논리 오류 등)

예외 처리 방법

 
 그렇다면 시스템이 비정상적으로 종료되지 않게 하려면 이 예외를 처리해야 하는데 코드에서 어떻게 처리할 수 있을까? Java에서는 try-catch, throw, throws를 통해 예외를 처리한다.


try-catch

 try-catch는 가장 기본적인 예외 처리 방식으로, 예외가 발생할 수 있는 코드를 try 블록 안에 작성하고 예외가 발생했을 때 수행할 동작을 catch 블록에 작성한다.

try {
    int result = 10 / 0;  // ArithmeticException 발생
} catch (ArithmeticException e) {
    System.out.println("0으로 나눌 수 없습니다.");
}

 try 블록 내에서 예외가 발생하면, 해당 예외를 처리할 수 있는 catch 블록으로 제어가 이동하며 프로그램은 정상적으로 이어진다.
 

throw

 throw는 직접 예외를 발생시킬 때 사용한다.

public void checkAge(int age) {
    if (age < 19) {
        throw new IllegalArgumentException("성인만 접근 가능합니다.");
    }
}

 즉, throw는 예외 객체를 던지는 역할을 한다. 던져진 예외는 현재 메서드를 호출한 쪽에서 처리해야 한다.
 
 보통 실제 프로젝트에서는 @RestControllerAdvice라는 어노테이션이 선언된 Exception Handler 클래스에서 던져진 예외를 받아 화면으로 return하는 식으로 예외를 처리한다.
 

throws

 throws는 메서드 선언부에서 이 메서드가 어떤 예외를 던질 수 있는지 명시하는 키워드다.

public void readFile(String filePath) throws IOException {
    FileReader reader = new FileReader(filePath);  // CheckedException
    reader.read();
}

 throws는 이 메서드를 사용할 때 이런 예외가 발생할 수도 있다라는 사전 경고 역할을 한다. 해당 메서드를 호출한 곳에서 반드시 예외를 처리해야 컴파일이 통과된다.
 

try-catch + throw

 실무에서는 try-catch 안에서 throw로 예외를 던지는 경우를 많이 볼 수 있다. 아래에서 얘기할 거지만 실제 설계 단계에서 예외를 어떻게 구분할지를 정하고 개발 단계에 들어가기 때문에,  catch로 일단 예외를 잡고 throw로 개발자가 던지고자 하는 예외로 변환해서 던지는 것이다.

try {
    userRepository.save(user);
} catch (DataIntegrityViolationException e) {
    // DB 제약조건 위반 등 Spring 내부 예외를 감싸서
    // 비즈니스 예외로 바꿔 던짐
    throw new BusinessException("USER_DUPLICATE", "이미 존재하는 사용자입니다.");
}

 


예외 처리의 중요성

 
 실제 프로젝트를 개발하다 보면 단순히 예외를 잡는 것 이상으로, 예외를 어떻게 설계하고 구분할지가 시스템 안정성과 유지보수성에 큰 영향을 미친다.
 
 예를들어 실제 프로젝트에서는 단일 애플리케이션만 있는게 아니라 프레임워크, ESB, EDMS, 그 외 외부 연계 시스템 등 서로 다른 다수의 시스템이 연결되어 있는데, 이런 환경에서는 한 곳에서 발생한 예외가 다른 시스템에까지 영향을 줄 수 있기 때문에, 예외를 명확하게 구분하고 설계하는게 매우 중요하다.
 
ex)

  • FrameworkException → 내부 공통 모듈에서 발생한 예외
  • IntegrationException → ESB나 외부 연계 구간에서 발생한 예외
  • BusinessException → 사용자의 잘못된 요청으로 발생한 예외
  • SystemException → DB, 서버 자원, 네트워크 등의 시스템 문제

이렇게 예외를 구분해두면 문제가 발생했을 때 로그만 봐도 “이건 프레임워크 담당이 봐야 하는 문제구나”, “이건 외부 연계 쪽 에러니까 ESB 담당한테 전달해야겠네” 하는 식으로 담당 영역을 빠르게 특정하고 조치를 취할 수 있다.