01. 예외
1. 오류 vs 예외
| 오류 (Error) |
예외 (Exception) |
|
| 의미 | 시스템 레벨에서 발생하는 치명적인 문제 | 개발자가 처리할 수 있는 문제 |
| 예시 | 메모리 부족(OutOfMemoryError), 스택 오버플로우(StackOverflowError) |
NullPointerException, IllegalArgumentException |
| 복구 가능 여부 | 거의 불가능 | 비교적 복구 가능 |
| 처리 방법 | JVM 차원 처리 | 개발자가 try-catch 등으로 처리 |
2. 예외 처리의 중요성
예외 처리는 모든 프로그래밍 언어에서 매우 중요한 개념이다. 프로그램이 실행되는 동안 파일 부재, 네트워크 연결 실패, 잘못된 입력 제공, DB 연결 문제 등 다양한 상황이 발생하는데 이때 예외 처리가 없다면 프로그램은 즉시 종료되기 때문이다. 하지만 자바는 체계적인 예외 계층 구조를 제공하여 프로그램의 문제를 감지하고 적절하게 대응할 수 있다.
그러므로 예외의 원인은 명확하게 기록해야 하며, 특수한 문제 상황을 다루는 용도로만 사용해야 한다.
| 예외 처리의 주요 역할 | |
| 프로그램 안정성 | 예기치 못한 종료 방지 |
| 사용자 경험 향상 | 친절한 오류 메시지 제공 |
| 에러 로깅 | 문제 원인 분석 가능 |
| 방어적 프로그래밍 | 잠재적인 문제를 미리 차단 |
3. 자바의 예외 계층 구조
자바는 모든 예외를 하나의 체계적인 계층 구조로 관리한다.
| 예외 계층 구조 | 설명 |
| Throwable | 자바에서 던질 수 있는 모든 객체의 최상위 부모 |
| Error | 시스템 단계에서 발생하는 치명적인 오류로, 개발자가 처리하지 않음 |
| Exception | 개발자가 처리 가능한 모든 예외의 부모 클래스 |
| RuntimeException | 실행 중 자주 발생하는 예외들의 부모 |


/* 예외로 인한 프로그램 종료 예시 */
public class Example {
public static void main(String[] args) {
methodA();
}
static void methodA() {
methodB();
}
static void methodB() {
String s = null;
s.length(); // NullPointerException → methodB → methodA → main 순으로 전달
}
}
02. 체크드 예외 (Checked Exception) vs 언체크드 예외 (Unchecked Exception)
우선 체크드 예외란 컴파일 시점에 반드시 처리해야 하는 예외이다. 만약 체크드 예외를 처리하지 않고 프로그램을 그대로 실행시키면, 컴파일 에러가 발생한다. 대표적으로 입출력 예외(IOException), 데이터베이스 예외(SQL Exception), 파일 부재 예외(FileNotFoundException)가 있다.
그에 비해 언체크드 예외란 컴파일러가 처리 여부를 강제하지 않는 예외를 의미한다. 즉, try-catch를 쓰지 않아도 아무런 문제없이 컴파일되지만, 실행 중 실패하는 예외이다. 대표적으로 Null 예외(NullPointerException), 인자 값 예외 (IllgalArgumentException), 인덱스 예외(IndexOutOfBoundsException)가 있다.
| 주요 특징 | 체크드 예외 | 언체크드 예외 |
| 처리 강제 여부 | 반드시 처리해야 함 | 강제되지 않음 |
| 예외 처리 방식 | try-catch 혹은 throws | 선택적 처리 |
| 발생 시점 | 컴파일 단계에서 감지 | 실행 중 (Runtime) |
| 발생 이유 | 외부 환경의 불확실성 >> 파일, 네트워크, DB 등 |
주로 개발자의 실수 또는 논리 오류 |
| 복구 가능성 | 있음 | 없음 |
| 대표 발생 주체 | JVM 외부 시스템 | 코드 내부 논리 |
| 실무 활용 | 안전성 보장을 위해 꼭 필요한 경우 사용 | 일반적으로 선호 |
| 사용 상황 | 외부 시스템과 상호작용 >> 실패가 예상되고, 복구할 방법이 있을 때 |
잘못된 파리미터 전달, 비지니스 규칙 위반, 복구 불가능 오류 표현 >> 예외 메시지를 자세히 남겨야 함 |
// 1. 체크드 예외
public void readFile(String path) throws IOException {
Files.readAllLines(Path.of(path));
}
// 2. 언체크드 예외 (파리미터 검증)
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("나이는 음수가 될 수 없습니다.");
}
}
03. 예외 처리 문법
1. throws 키워드
throws 키워드는 메서드에서 발생할 수 있는 예외의 책임을 호출한 쪽으로 넘기는 문법이다. 그렇기에 하위 메서드를 호출하는 코드는 직접 try-catch 문을 통해 예외를 잡아서 처리하거나, 다시 throws를 통해 상위 클래스로 전파해야 한다.
2. try-catch-finally 기본 문법
try-catch-finally 블록은 예외 처리의 핵심이다. 해당 구조는 프로그램이 예외를 만났을 떄, 종료되지 않고 안전하게 복구하거나 정리 작업을 수행하도록 만들어준다.
| 블록 종류 | 목적 | |
| try | 예외가 발생할 가능성이 있는 코드를 감싸는 영역 | 정상 흐름에서 실행해야 할 코드를 작성 >> 예외가 발생하는 즉시 catch 블록으로 흐름 이동 |
| catch | try 블록에서 발생한 예외를 처리하는 영역 | 예외 처리를 위한 catch 블록을 위에서 아래 방향으로 실행 >> 발생한 예외 타입과 catch의 예외 타입이 일치하면 실행 |
| finally | 예외가 발생하든 발생하지 않든 반드시 실행되는 영역 | 자원을 담는 역할을 수행 >> 파일, 소켓, DB 커넥션 등 |
// 1. 단일 try-catch 문
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다.");
}
// 2. 다중 try-catch 문
try {
FileReader fr = new FileReader("test.txt");
int data = fr.read();
} catch (FileNotFoundException e) {
System.out.println("파일을 찾을 수 없습니다.");
} catch (IOException e) {
System.out.println("파일을 읽는 도중 오류가 발생했습니다.");
}
// 3. try-catch-finally 문
try {
FileInputStream fis = new FileInputStream("data.txt");
} catch (IOException e) {
System.out.println("IOException 발생");
} finally {
System.out.println("항상 실행됩니다!");
}
3. finally 실행 여부표
| 상황 | finally 실행 여부 |
| 예외 발생 | ✔ 실행됨 |
| 예외 발생하지 않음 | ✔ 실행됨 |
| catch에서 return 실행 | ✔ 실행됨 |
| try에서 return 실행 | ✔ 실행됨 |
| catch에서 예외 발생 | ✔ 실행됨 |
4. try-with-resources
자원을 사용하는 작업에서 finally 블록의 가독성 개선을 위해 등장한 문법이다. try (...) 내부에 선언한 자원은 자동으로 close()를 호출할 뿐만 아니라 close 호출 순서를 역순으로 보장하여, finally 블록을 사용하지 않아도 된다. 단, TWR에 넣는 객체는 반드시 AutoCloseable 인터페이스를 구현해야 한다. 객체는 자체적으로 close() 함수를 호출하고, 만약 try 블록 혹은 인터페이스에서 예외가 발생한다면, 해당 인터페이스를 구현한 것이 자원 객체이므로 프로그램은 예외를 들고 catch 블록으로 이동하여 예외를 처리한다.
// 1. try-catch-finally 문 사용
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
} catch (IOException e) {
// ...
} finally {
if (fis != null) {
try { fis.close(); } catch (IOException e) {}
}
}
// 2. try-with-resources 문 사용
try (FileInputStream fis = new FileInputStream("data.txt")) {
int data = fis.read();
} catch (IOException e) {
e.printStackTrace();
}
// 3. AutoCloseable 인터페이스
public interface AutoCloseable {
void close() throws Exception;
}
5. TWR과 finally 자원 해제 비교
| try-finally | try-with-resources | |
| 자원 해제 위치 | finally 내부 | 자동 close() |
| 코드 길이 | 길고 복잡 | 매우 짧고 직관적 |
| 예외 중첩 처리 | 어려움 | suppressed exception으로 자동 관리 |
| 실무 권장 | 비추천 | 적극 권장 |
04. 예외 처리 시 고려사항 및 베스트 프랙티스
예외 처리는 프로그램의 안정성과 유지보수에 직접적인 영향을 미치는 설계 작업이다. 예외 처리를 잘 설계한다면 시스템을 튼튼하게 유지하지만 반대로 잘못된 예외 처리 설계는 문제를 숨기거나 버그를 추적하기 어렵게 만든다.
1. Fail-fast 원칙
Fail-Fast란 잘못된 입력, 잘못된 상태, 예기치 못한 흐름 등을 발견했을 때 최대한 빠르게 오류를 발생시키는 것을 의미한다. 즉, 문제가 발생한 즉시 예외를 던져 더 큰 사고로 번지지 않도록 막는 것이다. 이러한 Fail-Fast는 오류를 초기 단계에서 발견할 수 있을 뿐만 아니라 디버깅 및 문제 분석을 쉽게 하고, 버그가 전파되는 것을 막아 시스템 전체의 안전성을 확보한다.
// 1. Fail-Fast 적용
public class UserService {
public User findUserById(Long id) {
// 예제 1: null 값 허용하지 않음
if (id == null) {
throw new IllegalArgumentException("사용자 ID는 null일 수 없습니다.");
}
User user = repository.findById(id);
// 예제 2: 유효하지 않은 상태 즉시 중단
if (user == null) {
throw new IllegalStateException("해당 ID의 사용자를 찾을 수 없습니다. id=" + id);
}
return user;
}
}
// 2. Fail-Fast 미적용
public User findUserById(Long id) {
if (id == null) return null; // 문제를 조용히 넘기는 행위
User user = repository.findById(id);
return user; // user가 null이어도 호출한 쪽에서 예외를 만나 문제 발생
}
2. 예외 삼키키 (Swallowing Exception)
예외 삼키기란 예외를 catch 했지만 아무 처리를 하지 않고 넘어가 버리는 것을 의미한다. 즉, catch 블록이 비워져 있는 상태로, 개발자가 절대 해서는 안 되는 행위 중 하나다.
| 예외 삼키기의 문제점 | |
| 버그 조기 발견 불가능 | 오류는 발생했지만 로그가 없어 개발자가 인지하지 못함 |
| 장애 전파 | 예외가 숨겨지면 더 큰 문제로 번짐 |
| 상태 불일치 | 예외 후의 상태가 정상인지 보장할 수 없음 |
| 디버깅 불가능 | 무엇이 문제였는지 기록이 없어 원인 추적 어려움 |
// 1. 예외 숨기기 예제
public void updateUser(User user) {
try {
repository.save(user);
} catch (Exception e) {
// 예외 삼키기
}
}
2. 올바른 catch 블록
catch (Exception e) {
log.error("사용자 저장 실패: {}", user.getId(), e);
throw e; // 다시 던져 호출자가 처리하도록 위임
}
3. 예외 전환 (Exception Chaining) 및 래핑 (Wrapping)
예외 전환이란 낮은 레벨의 예외를 더 의미 있는 상위 레벨의 예외로 바꿔 던지는 기술이다. 즉, 예외는 낮은 레벨는 맥락만 추가하고 높은 레벨에서 처리함을 의미한다. 예외 전환은 내부 구현을 노출하지 않고 추상화 수준을 유지하여, 호출자에게 예외의 의미와 적절한 정보를 명확하게 전달한다.
이러한 예외 전환을 할 때에는, 에러의 원인 정보를 담고 있는 예외 객체(e)와 에러가 발생하기까지 거쳐온 메서드들의 목록인 스택 트레이스(Stack Trace)를 함께 로깅(Logging)해야 한다. 즉, 오류의 원인과 원인이 발생하기까지의 경로를 함께 기록해야만 오류를 빠르고 정확하게 바로 잡을 수 있다.
| 좋은 예외 메시지 작성 기준 | |
| 주체성 | 무엇이, 왜, 어떤 값 때문에 발생했는지 |
| 맥락 제공 | 상황, 입력값, 시스템 상태 등을 포함 |
| 사용자 메시지와 개발자 메시지 분리 | 운영 사용자에게 과도한 내부 정보 노출 금지 |
// 1. 잘못된 예외 전환 예시
catch (SQLException e) {
throw new RuntimeException("저장 실패"); // 원인 정보가 사라짐
}
// 2. 올바른 예외 전환 예시
public void saveOrder(Order order) {
try {
repository.insert(order);
} catch (SQLException e) {
throw new OrderSaveFailedException("주문 저장 중 문제가 발생했습니다.", e);
}
}
// 3. 좋은 메시지 예시
throw new IllegalArgumentException(
"상품 가격(price=\" + price + ")은 0보다 커야 합니다. 입력값을 확인해주세요."
);
// 4. 나쁜 메시지 예시
throw new IllegalArgumentException("오류 발생"); // 추상적인 설명
'[Codeit Review] > 객체 지향 프로그래밍' 카테고리의 다른 글
| [06] UML을 활용한 객체 지향 설계 시각화 (0) | 2026.01.26 |
|---|---|
| [04] 내부 클래스와 익명 클래스, 그리고 열거 타입(ENUM)의 활용 (0) | 2026.01.26 |
| [03] SOLID 원칙 (0) | 2026.01.26 |
| [02] 객체 지향 프로그래밍의 4가지 핵심 개념 (0) | 2026.01.26 |
| [01] 객체 지향 프로그래밍 (0) | 2026.01.16 |