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 블록을 작성할 때는 위에서부터 순서대로 조건(예외의 종류)을 검사합니다. 부모 예외 클래스일수록 아래쪽에 두어야 합니다. 모든 예외의 조상인 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("파일 오류 발생");
}
}
catch (ParentException | ChildException e) 형태는 컴파일 에러입니다. 두 예외가 상속 관계에 있으면 하나만 써도 됩니다. 예를 들어 IOException이 Exception의 자식이면 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
파일 처리 완료 (성공/실패 무관)
Java 7부터는 AutoCloseable을 구현한 자원(파일, DB 연결 등)을 try (...) 괄호 안에 선언하면, 블록이 끝날 때 자동으로 close()가 호출됩니다. finally에서 수동으로 닫는 것보다 훨씬 안전하고 간결합니다. 자세한 내용은 다음 장 try-with-resources 에서 다룹니다.
다음 장에서는 비즈니스 로직에 특화된 사용자 정의 예외(Custom Exceptions) 에 대해 배워보겠습니다.