Java - Exception

develkkm·2025년 12월 22일

0부터 시작하는 Java

목록 보기
27/27

Java에서 예외 처리는 정상 흐름과 예외 흐름을 분리하고 자원을 안전하게 관리하며 코드의 의도를 명확하게 드러내기 위한 설계 도구다.
예외 처리는 반환 값 기반 오류 처리의 한계를 인식하는 것에서 시작해 예외 계층 설계와 실무 전략으로 확장된다.


예외 처리 필요성

초기에는 메서드의 반환 값으로 성공과 실패를 구분하려는 시도를 하게 된다.

int result = send(data);
if (result == ERROR) {
    // 오류 처리
}

이 방식은 정상 흐름과 예외 흐름이 섞여 코드의 핵심이 보이지 않는다.
예외 처리를 위한 분기 코드가 정상 로직보다 더 많아진다.
중간 return으로 인해 자원 해제가 누락될 가능성이 높아진다.

특히 네트워크 파일 데이터베이스처럼 외부 자원을 사용하는 경우에는
실패 상황에서도 반드시 자원을 반납해야 하기 때문에 구조적 문제가 된다.


기본 개념

자바의 예외는 객체다.
오류 상황을 값이 아니라 객체로 표현하고 throw를 통해 흐름을 중단한다.

throw new IllegalStateException("전송 실패");

예외가 발생하면 현재 흐름은 즉시 중단되고
가장 가까운 catch 블록으로 이동한다.

정상 흐름과 예외 흐름이 코드 구조로 명확히 분리된다.


try catch 구조

예외 처리는 try catch 구조를 중심으로 이루어진다.

try {
    client.connect();
    client.send(data);
} catch (NetworkException e) {
    System.out.println(e.getMessage());
}

try 블록에는 정상 흐름만 작성한다.
catch 블록에는 예외 흐름만 작성한다.

이 구조를 사용하면 코드의 읽는 방향이 자연스럽다.
정상 흐름이 위에서 아래로 한눈에 들어온다.


finally

finally는 예외 발생 여부와 관계없이 반드시 실행되는 블록이다.

try {
    client.connect();
    client.send(data);
} catch (Exception e) {
    log.error(e.getMessage());
} finally {
    client.disconnect();
}

finally는 다음 모든 경우에 실행된다.

  • 정상 종료된 경우.
  • catch에서 예외를 처리한 경우.
  • catch에서 잡지 못한 예외가 발생한 경우.

외부 자원 해제 로직은 반드시 finally 또는 이에 준하는 구조에 위치해야 한다.


try with resources

자바 7부터는 try with resources 문법을 제공한다.
AutoCloseable을 구현한 자원은 try 블록이 끝나면 자동으로 close 된다.

try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    return br.readLine();
}

이 구조는 다음 장점을 가진다.

  • finally를 직접 작성하지 않아도 된다.
  • 자원 해제 누락 가능성이 사라진다.
  • 코드가 짧고 명확해진다.

try with resources는 파일 스트림 소켓 커넥션 등
반드시 닫아야 하는 자원을 다룰 때 가장 우선적으로 고려해야 한다.


예외 계층 구조

자바 예외는 상속 구조를 가진다.

Object  
Throwable  
Exception  
RuntimeException  

Exception은 애플리케이션에서 처리 가능한 예외의 최상위 타입이다.

예외를 계층화하면 다음이 가능해진다.
부모 예외로 여러 자식 예외를 한 번에 처리한다.
구체적인 예외를 먼저 잡아 세밀한 처리를 한다.

try {
    client.connect();
    client.send(data);
} catch (ConnectException e) {
    // 연결 오류
} catch (NetworkException e) {
    // 네트워크 공통 오류
}

catch는 위에서 아래로 검사되므로
항상 구체적인 예외를 먼저 작성해야 한다.


Check 예외

Check 예외는 컴파일러가 처리를 강제하는 예외다.

public void call() throws IOException {
    throw new IOException();
}

Check 예외의 장점은 예외 누락 방지다.
단점은 처리할 수 없는 예외까지 계속 전파해야 한다는 점이다.

라이브러리가 많아질수록 throws 선언이 폭발적으로 증가한다.
결국 모든 예외를 throws Exception으로 던지는 상황이 발생하기 쉽다.

이는 Check 예외의 의도를 무너뜨리는 결과를 낳는다.


Runtime(Uncheck) 예외

Runtime 예외는 컴파일러가 강제하지 않는다.

throw new IllegalArgumentException("잘못된 입력");

주로 다음 상황에서 사용한다.

  • 복구가 불가능한 오류.
  • 프로그래밍 실수.
  • 호출자가 처리할 이유가 없는 오류.

실무에서는 시스템 예외와 비즈니스 예외를 구분해 Runtime 예외 중심으로 설계하는 경우가 많다.

런타임 예외 중심 설계

복구 가능한 경우만 명시적으로 catch 한다.
그 외의 모든 예외는 런타임 예외로 던진다.

비즈니스 규칙 위반이나 사용자 입력 오류처럼 의미 있는 처리가 가능한 경우에만 예외를 잡는다.

try {
    service.process(command);
} catch (BusinessException e) {
    return errorResponse(e.getMessage());
}

이때 BusinessException 역시
체크 예외가 아니라 RuntimeException을 상속한 예외다.

profile
알던것을 더 확실하게

0개의 댓글