본문으로 건너뛰기

8.2 try-catch와 finally 구문 (try-catch & finally Blocks)

예외 처리를 직접 구현할 때 가장 기본적이고 핵심이 되는 문법이 바로 try-catch 구문입니다. 자바는 에러가 발생했을 때 프로그램이 곧바로 종료되는 대신, 이 구문을 통해 발생한 문제를 적절히 회피하거나 수습할 수 있는 안전망 역할을 제공합니다.

1. try-catch 문의 기본 구조

예외 처리를 위한 try-catch 블록은 크게 예외가 발생할 가능성이 있는 try 블록 과 예외가 발생했을 때 그 예외를 전담해서 처리하는 catch 블록 으로 나뉩니다.

try {
// 예외가 발생할 가능성이 있는 코드들을 여기에 작성합니다
} catch (Exception1 e1) {
// Exception1 타입의 예외가 발생했을 때 수행될 코드
} catch (Exception2 e2) {
// Exception2 타입의 예외가 발생했을 때 수행될 코드
}
  • try 블록: 코드가 정상적으로 실행되는지 자바가 모니터링하는 구간입니다.
  • catch 블록: try 안에서 예외가 발생하면(throw), 자바는 즉시 try 내부 실행을 멈추고 catch 블록 내부의 코드를 실행하여 예외 상황을 수습합니다. 괄호 안의 참조 변수(예시의 e1, e2)에는 발생한 예외 객체의 상세 정보가 담겨서 넘어옵니다.

예제: 0으로 나누기 (ArithmeticException)

public class TryCatchBasic {
public static void main(String[] args) {
System.out.println("프로그램을 시작합니다.");

int num = 100;
int result = 0;

try {
// 이 줄에서 0으로 나누려다가 에러 발생!
result = num / 0;
// 에러가 발생한 뒤의 코드는 실행되지 않고 바로 catch 블록으로 이동
System.out.println("결과: " + result);

} catch (ArithmeticException e) {
System.out.println("예외 발생! 0으로 나눌 수 없습니다.");
System.out.println("에러 메시지: " + e.getMessage());
}

// catch 구문 덕분에 프로그램이 비정상 종료되지 않고 여기까지 도달합니다
System.out.println("프로그램을 정상적으로 종료합니다.");
}
}

실행 결과:

프로그램을 시작합니다.
예외 발생! 0으로 나눌 수 없습니다.
에러 메시지: / by zero
프로그램을 정상적으로 종료합니다.

2. 다중 catch 블록

try 블록 안에서 여러 종류의 예외가 발생할 수 있을 때는 여러 개의 catch 블록을 줄지어서 쓸 수 있습니다.

public class MultiCatchExample {
public static void main(String[] args) {
String[] data = {"10", null, "abc", "20"};

for (String item : data) {
try {
// 두 가지 예외가 발생할 수 있는 코드
int value = Integer.parseInt(item); // NumberFormatException 가능
int result = 100 / value; // ArithmeticException 가능
System.out.println("100 / " + value + " = " + result);

} catch (NullPointerException e) {
System.out.println("null 값이 입력되었습니다.");

} catch (NumberFormatException e) {
System.out.println("숫자 형식이 아닙니다: " + item);

} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다.");

} catch (Exception e) {
// 위의 catch 블록에서 처리되지 않은 나머지 모든 예외를 처리
System.out.println("알 수 없는 오류: " + e.getMessage());
}
}
}
}

실행 결과:

100 / 10 = 10
null 값이 입력되었습니다.
숫자 형식이 아닙니다: abc
100 / 20 = 5
주의: catch 블록 순서

다중 catch 블록을 작성할 때는 위에서부터 순서대로 조건(예외의 종류)을 검사합니다. 부모 예외 클래스일수록 아래쪽에 두어야 합니다. 모든 예외의 조상인 Exception이 맨 위에 있으면, 그 밑에 있는 catch 블록들은 절대 실행될 기회를 얻지 못해 컴파일 에러가 발생합니다.

3. multi-catch: Java 7+ 기능

Java 7부터는 여러 예외를 |로 연결하여 하나의 catch 블록에서 처리할 수 있습니다.

import java.io.IOException;
import java.sql.SQLException;

public class MultiCatchJava7 {
public static void main(String[] args) {
try {
riskyOperation();
} catch (IOException | SQLException e) {
// IOException 또는 SQLException이 발생하면 이 블록에서 처리
System.out.println("IO 또는 DB 오류: " + e.getMessage());
// 참고: multi-catch에서 변수 e는 사실상 final처럼 취급됩니다
} catch (Exception e) {
System.out.println("기타 오류: " + e.getMessage());
}
}

static void riskyOperation() throws IOException, SQLException {
// 데모용으로 예외를 직접 발생
throw new IOException("파일 오류 발생");
}
}
multi-catch 활용 시 주의

catch (ParentException | ChildException e) 형태는 컴파일 에러입니다. 두 예외가 상속 관계에 있으면 하나만 써도 됩니다. 예를 들어 IOExceptionException의 자식이면 catch (IOException | Exception e)는 불필요한 중복입니다.

4. finally 블록: 무조건 실행되는 정리 코드

예외가 발생하든, 정상적으로 실행되든 상관없이 맨 마지막에 반드시 실행되어야 하는 정리 코드 가 있을 때 finally 블록을 사용합니다. 데이터베이스 연결, 파일 스트림, 네트워크 소켓 등을 사용 후 반드시 닫아야 할 때 쓰입니다.

public class FinallyExample {
static String databaseConnection = null;

public static void main(String[] args) {
System.out.println("=== 정상 실행 케이스 ===");
processData(10);

System.out.println("\n=== 예외 발생 케이스 ===");
processData(0);
}

static void processData(int divisor) {
try {
databaseConnection = "DB 연결됨";
System.out.println(databaseConnection);

int result = 100 / divisor; // divisor가 0이면 예외 발생
System.out.println("결과: " + result);

} catch (ArithmeticException e) {
System.out.println("계산 오류: " + e.getMessage());

} finally {
// 예외 발생 여부와 관계없이 항상 실행됩니다
// try나 catch 안에 return이 있어도 finally는 실행됩니다!
databaseConnection = null;
System.out.println("DB 연결을 안전하게 닫았습니다.");
}
}
}

실행 결과:

=== 정상 실행 케이스 ===
DB 연결됨
결과: 10
DB 연결을 안전하게 닫았습니다.

=== 예외 발생 케이스 ===
DB 연결됨
계산 오류: / by zero
DB 연결을 안전하게 닫았습니다.

finally와 return의 관계

public class FinallyWithReturn {
public static int testFinally() {
try {
System.out.println("try 블록 실행");
return 1; // return이 있어도!
} finally {
System.out.println("finally 블록은 반드시 실행됩니다");
// 주의: finally 안에 return을 두면 try의 return을 덮어씁니다
}
}

public static void main(String[] args) {
int result = testFinally();
System.out.println("결과: " + result); // 1
}
}

5. 예외 전파(Exception Propagation)

예외가 발생했을 때 해당 메서드에서 처리하지 않으면, 예외는 호출 스택(Call Stack)을 따라 위쪽 메서드로 전달됩니다.

public class ExceptionPropagation {
// 가장 깊은 메서드: 예외를 처리하지 않고 위로 던짐
static void methodC() {
int result = 10 / 0; // ArithmeticException 발생
}

// 중간 메서드: 예외를 처리하지 않고 위로 전달
static void methodB() {
methodC(); // methodC의 예외가 여기로 전파됨
}

// 최상위 메서드: 예외를 최종적으로 처리
static void methodA() {
try {
methodB(); // methodB의 예외가 여기로 전파됨
} catch (ArithmeticException e) {
System.out.println("methodA에서 예외를 처리: " + e.getMessage());
}
}

public static void main(String[] args) {
methodA();
System.out.println("정상 종료");
}
}

실행 결과:

methodA에서 예외를 처리: / by zero
정상 종료

6. throws 키워드: 예외 처리 책임 위임

메서드 선언부에 throws를 사용하면 해당 메서드에서 발생한 예외의 처리 책임을 호출자에게 위임 합니다.

import java.io.FileInputStream;
import java.io.IOException;

public class ThrowsExample {

// throws로 IOException 처리를 호출자에게 위임
static String readFileContent(String filePath) throws IOException {
FileInputStream fis = new FileInputStream(filePath);
// 파일 읽기 로직 (생략)
fis.close();
return "파일 내용";
}

// throws로 여러 예외를 위임할 수도 있습니다
static void complexOperation(String path, String numStr)
throws IOException, NumberFormatException {
String content = readFileContent(path);
int value = Integer.parseInt(numStr);
System.out.println("처리 완료: " + content + ", " + value);
}

public static void main(String[] args) {
// 호출자인 main에서 예외를 처리해야 합니다
try {
readFileContent("config.txt");
} catch (IOException e) {
System.out.println("파일 오류: " + e.getMessage());
}

// 또는 main도 throws를 선언해서 JVM에 위임할 수 있습니다 (권장하지 않음)
}
}

7. Checked Exception을 Unchecked로 변환하는 패턴

실무에서는 Checked Exception을 RuntimeException으로 감싸서 던지는 패턴을 자주 사용합니다.

import java.io.IOException;

public class WrapExceptionPattern {

// Checked Exception을 Unchecked로 감싸서 던짐
static void loadConfig(String path) {
try {
readConfigFile(path);
} catch (IOException e) {
// IOException을 RuntimeException으로 감싸서 다시 던짐
throw new RuntimeException("설정 파일 로드 실패: " + path, e);
}
}

static void readConfigFile(String path) throws IOException {
throw new IOException("파일을 찾을 수 없음: " + path);
}

public static void main(String[] args) {
try {
loadConfig("app.properties");
} catch (RuntimeException e) {
System.out.println("오류: " + e.getMessage());
System.out.println("원인: " + e.getCause().getMessage());
}
}
}

8. 예외 정보 확인 메서드 완전 정리

메서드반환 타입설명
getMessage()String예외 메시지 반환
toString()String예외 클래스명 + 메시지
printStackTrace()void예외 발생 경로 전체 출력
getClass().getName()String예외 클래스 풀 경로 이름
getCause()Throwable이 예외를 유발한 원인 예외
public class ExceptionMethodsDemo {
public static void main(String[] args) {
try {
String s = null;
s.length(); // NullPointerException 발생
} catch (NullPointerException e) {
System.out.println("getMessage(): " + e.getMessage());
System.out.println("toString(): " + e.toString());
System.out.println("getClass(): " + e.getClass().getName());
// e.printStackTrace(); // 실제 디버깅 시 활용
}

try {
// 원인 예외를 포함한 예외 생성
try {
Integer.parseInt("abc");
} catch (NumberFormatException cause) {
throw new RuntimeException("데이터 처리 실패", cause);
}
} catch (RuntimeException e) {
System.out.println("메시지: " + e.getMessage());
System.out.println("원인: " + e.getCause().getMessage());
}
}
}

9. 실전 예제: 파일 읽기 시뮬레이션

public class FileReadSimulation {

// 파일 읽기를 시뮬레이션하는 메서드
static String readFile(String filename) throws Exception {
if (filename == null) {
throw new NullPointerException("파일명이 null입니다");
}
if (filename.isEmpty()) {
throw new IllegalArgumentException("파일명이 비어있습니다");
}
if (!filename.endsWith(".txt")) {
throw new UnsupportedOperationException("txt 파일만 지원합니다: " + filename);
}
if (filename.equals("missing.txt")) {
throw new Exception("파일을 찾을 수 없습니다: " + filename);
}

return "파일 내용: Hello, Java!";
}

public static void main(String[] args) {
String[] testFiles = {"data.txt", null, "", "image.png", "missing.txt"};

for (String file : testFiles) {
System.out.println("--- 파일 읽기 시도: " + file + " ---");
try {
String content = readFile(file);
System.out.println("성공: " + content);

} catch (NullPointerException e) {
System.out.println("오류: " + e.getMessage());

} catch (IllegalArgumentException e) {
System.out.println("입력값 오류: " + e.getMessage());

} catch (UnsupportedOperationException e) {
System.out.println("지원하지 않는 파일 형식: " + e.getMessage());

} catch (Exception e) {
System.out.println("파일 오류: " + e.getMessage());

} finally {
System.out.println("파일 처리 완료 (성공/실패 무관)\n");
}
}
}
}

실행 결과:

--- 파일 읽기 시도: data.txt ---
성공: 파일 내용: Hello, Java!
파일 처리 완료 (성공/실패 무관)

--- 파일 읽기 시도: null ---
오류: 파일명이 null입니다
파일 처리 완료 (성공/실패 무관)

--- 파일 읽기 시도: ---
입력값 오류: 파일명이 비어있습니다
파일 처리 완료 (성공/실패 무관)

--- 파일 읽기 시도: image.png ---
지원하지 않는 파일 형식: txt 파일만 지원합니다: image.png
파일 처리 완료 (성공/실패 무관)

--- 파일 읽기 시도: missing.txt ---
파일 오류: 파일을 찾을 수 없습니다: missing.txt
파일 처리 완료 (성공/실패 무관)
고수 팁: try-with-resources

Java 7부터는 AutoCloseable을 구현한 자원(파일, DB 연결 등)을 try (...) 괄호 안에 선언하면, 블록이 끝날 때 자동으로 close()가 호출됩니다. finally에서 수동으로 닫는 것보다 훨씬 안전하고 간결합니다. 자세한 내용은 다음 장 try-with-resources 에서 다룹니다.

다음 장에서는 비즈니스 로직에 특화된 사용자 정의 예외(Custom Exceptions) 에 대해 배워보겠습니다.