[2026] Java 예외 처리 | try-catch, throws, 커스텀 예외

[2026] Java 예외 처리 | try-catch, throws, 커스텀 예외

이 글의 핵심

Java 예외 처리: try-catch, throws, 커스텀 예외. 예외 계층 구조·try-catch-finally.

들어가며

Java의 예외 처리는 실패를 타입으로 표현하는 쪽에 가깝습니다. Checked Exception은 컴파일러가 try/throws 누락을 잡아 주는 특징이 있습니다. 이 글에서는 예외 계층, trycatch, throws, 커스텀 예외, try-with-resources까지 실무 패턴으로 정리합니다.

왜 중요한가?

  • 컴파일 타임 안전성: Checked Exception으로 예외 처리 누락 방지
  • 리소스 관리: try-with-resources로 메모리 누수 예방
  • 디버깅: 스택 트레이스로 오류 원인 추적
  • API 설계: 예외를 통한 명확한 에러 시그널링

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

1. 예외 계층 구조

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Throwable
├── Error (시스템 오류, 처리 불가)
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception (처리 가능한 예외)
    ├── IOException (Checked)
    ├── SQLException (Checked)
    └── RuntimeException (Unchecked)
        ├── NullPointerException
        ├── ArithmeticException
        └── IndexOutOfBoundsException

2. try-catch-finally

기본 예외 처리

아래 코드는 java를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

public class ExceptionExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
            System.out.println(result);
        } catch (ArithmeticException e) {
            System.out.println("에러: " + e.getMessage());
        } finally {
            System.out.println("항상 실행");
        }
    }
}

여러 예외 처리

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

try {
    String str = null;
    System.out.println(str.length());
    
    int[] arr = {1, 2, 3};
    System.out.println(arr[10]);
} catch (NullPointerException e) {
    System.out.println("Null 참조");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("배열 범위 초과");
} catch (Exception e) {
    System.out.println("기타 예외: " + e.getMessage());
}

다중 예외 처리 (Java 7+)

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

try {
    // 코드
} catch (IOException | SQLException e) {
    System.out.println("I/O 또는 DB 에러: " + e.getMessage());
}

3. throws - 예외 전파

기본 throws

다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.io.*;
public class FileHandler {
    public void readFile(String path) throws IOException {
        FileReader fr = new FileReader(path);
        BufferedReader br = new BufferedReader(fr);
        String line = br.readLine();
        br.close();
    }
    
    public static void main(String[] args) {
        FileHandler handler = new FileHandler();
        
        try {
            handler.readFile("file.txt");
        } catch (IOException e) {
            System.out.println("파일 읽기 실패: " + e.getMessage());
        }
    }
}

4. 커스텀 예외

Checked 예외

다음은 java를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);
    }
}
public class User {
    private int age;
    
    public void setAge(int age) throws InvalidAgeException {
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("나이는 0~150 사이여야 합니다");
        }
        this.age = age;
    }
}

Unchecked 예외

다음은 java를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class InvalidEmailException extends RuntimeException {
    public InvalidEmailException(String message) {
        super(message);
    }
}
public class User {
    private String email;
    
    public void setEmail(String email) {
        if (!email.contains("@")) {
            throw new InvalidEmailException("유효하지 않은 이메일");
        }
        this.email = email;
    }
}

5. try-with-resources

다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.io.*;
// 기존 방식
public void readFile1(String path) {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader(path));
        String line = br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
// try-with-resources (Java 7+)
public void readFile2(String path) {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        String line = br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

6. 실전 예제

예제: 사용자 검증

다음은 java를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class ValidationException extends Exception {
    public ValidationException(String message) {
        super(message);
    }
}
class User {
    private String name;
    private int age;
    private String email;
    
    public void validate() throws ValidationException {
        if (name == null || name.isEmpty()) {
            throw new ValidationException("이름은 필수입니다");
        }
        
        if (age < 0 || age > 150) {
            throw new ValidationException("나이는 0~150 사이여야 합니다");
        }
        
        if (email == null || !email.contains("@")) {
            throw new ValidationException("유효하지 않은 이메일");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        User user = new User();
        user.setName("");
        user.setAge(25);
        user.setEmail("invalid");
        
        try {
            user.validate();
            System.out.println("검증 성공");
        } catch (ValidationException e) {
            System.out.println("검증 실패: " + e.getMessage());
        }
    }
}

7. 예외 처리 베스트 프랙티스

1) 구체적인 예외를 먼저 catch

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

try {
    // 코드
} catch (FileNotFoundException e) {
    // 파일이 없을 때
} catch (IOException e) {
    // 기타 I/O 오류
} catch (Exception e) {
    // 최후의 안전망
}

주의: 부모 예외(Exception)를 먼저 catch하면 하위 예외가 도달하지 않습니다.

2) 예외를 무시하지 말 것

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 예
try {
    riskyOperation();
} catch (Exception e) {
    // 아무것도 안 함
}
// ✅ 좋은 예
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("Operation failed", e);
    throw new RuntimeException("Failed to process", e);
}

3) 예외 체이닝 활용

아래 코드는 java를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

public void processData(String data) throws DataProcessingException {
    try {
        // 복잡한 처리
        parseJson(data);
    } catch (JsonParseException e) {
        // 원본 예외를 cause로 포함
        throw new DataProcessingException("Invalid data format", e);
    }
}

4) 리소스는 try-with-resources 사용

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 자동으로 close() 호출됨
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 파일 읽기
} catch (IOException e) {
    // 예외 처리
}
// close()가 자동 호출됨 (역순으로)

5) Checked vs Unchecked 선택 기준

상황선택이유
복구 가능한 오류 (파일 없음, 네트워크 끊김)Checked (extends Exception)호출자가 반드시 처리하도록 강제
프로그래밍 오류 (null 참조, 배열 범위 초과)Unchecked (extends RuntimeException)코드 수정으로 해결해야 함
비즈니스 로직 위반상황에 따라복구 가능하면 Checked, 아니면 Unchecked

8. 실전 패턴

패턴 1: 예외 변환 (Exception Translation)

아래 코드는 java를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

public class UserService {
    private UserRepository repository;
    
    public User findUser(Long id) throws UserNotFoundException {
        try {
            return repository.findById(id);
        } catch (SQLException e) {
            // DB 예외를 도메인 예외로 변환
            throw new UserNotFoundException("User not found: " + id, e);
        }
    }
}

패턴 2: 예외 집계 (Exception Aggregation)

다음은 java를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

public class BatchProcessor {
    public void processBatch(List<Item> items) throws BatchProcessingException {
        List<Exception> errors = new ArrayList<>();
        
        for (Item item : items) {
            try {
                processItem(item);
            } catch (Exception e) {
                errors.add(e);
            }
        }
        
        if (!errors.isEmpty()) {
            throw new BatchProcessingException("일부 항목 처리 실패", errors);
        }
    }
}

패턴 3: Retry 패턴

다음은 java를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

public <T> T retryOperation(Supplier<T> operation, int maxRetries) 
        throws Exception {
    Exception lastException = null;
    
    for (int i = 0; i < maxRetries; i++) {
        try {
            return operation.get();
        } catch (TransientException e) {
            lastException = e;
            Thread.sleep(1000 * (i + 1)); // 지수 백오프
        }
    }
    
    throw new RuntimeException("Max retries exceeded", lastException);
}

9. 흔한 실수와 해결

실수 1: 예외를 흐름 제어로 사용

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 나쁜 예 - 예외를 일반 로직으로 사용
try {
    return map.get(key);
} catch (NullPointerException e) {
    return defaultValue;
}
// ✅ 좋은 예
return map.getOrDefault(key, defaultValue);

실수 2: 너무 넓은 catch

다음은 java를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 예
try {
    complexOperation();
} catch (Exception e) {
    // 모든 예외를 동일하게 처리
}
// ✅ 좋은 예
try {
    complexOperation();
} catch (IOException e) {
    // I/O 오류 처리
} catch (ValidationException e) {
    // 검증 오류 처리
}

실수 3: finally에서 return

아래 코드는 java를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 나쁜 예 - finally의 return이 try의 return을 덮어씀
public int getValue() {
    try {
        return 1;
    } finally {
        return 2; // 항상 2가 반환됨
    }
}

10. 성능 고려사항

예외는 비용이 크다

아래 코드는 java를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 스택 트레이스 생성 비용
long start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    try {
        throw new Exception();
    } catch (Exception e) {
        // 처리
    }
}
long elapsed = System.nanoTime() - start;
// 일반 제어 흐름보다 100~1000배 느림

최적화 팁:

  • 정상 흐름에서는 예외를 사용하지 말 것
  • 고빈도 경로에서는 예외 대신 Optional이나 Result 타입 고려
  • 스택 트레이스가 필요 없으면 fillInStackTrace() 오버라이드

정리

핵심 요약

  1. Checked vs Unchecked: 복구 가능성으로 판단
  2. try-catch-finally: 예외 처리와 리소스 정리
  3. try-with-resources: AutoCloseable 리소스 자동 관리
  4. 예외 체이닝: 원본 예외를 cause로 보존
  5. 베스트 프랙티스: 구체적 catch, 예외 무시 금지, 흐름 제어 금지

실무 체크리스트

  • Checked Exception은 복구 가능한 경우만 사용
  • 예외 메시지에 충분한 컨텍스트 포함
  • 원본 예외를 cause로 전달
  • 리소스는 try-with-resources 사용
  • 로깅과 예외 던지기를 동시에 하지 말 것

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3