예외
정상적인 프로그램 흐름을 방해하는 비정상적인 상황 즉, 프로그램 실행 도중 발생할 수 있는 에러 상황을 객체로 표현한 것
예외처리란 이러한 예외 상황이 발생했을 때, 적절히 대응하는 것
예외처리의 목적
- 서비스 안정성 확보 :
- 예외가 발생하더라도 전체 애플리케이션이 완전히 중단되지 않도록 보호하는 게 1차 목표.
- @Scheduled 배치에서 예외 터지고 잡지 않으면 스케줄러 자체가 멈춤 (Spring에서는 실제로 발생)
- 예외가 발생하더라도 전체 애플리케이션이 완전히 중단되지 않도록 보호하는 게 1차 목표.
- 사용자 경험 보호 (UX 보호)
- 에러가 발생했을 때 친절한 메시지로 알려주고, 최대한 정상적인 흐름처럼 보여주는 게 중요해. 사용자에게 ‘500 에러’ 같은 시스템 메시지를 그대로 노출하지 않음.
- “비밀번호가 틀렸습니다.” vs “Internal Server Error”
- 에러가 발생했을 때 친절한 메시지로 알려주고, 최대한 정상적인 흐름처럼 보여주는 게 중요해. 사용자에게 ‘500 에러’ 같은 시스템 메시지를 그대로 노출하지 않음.
- 문제 진단 용이성 확보
- 개발자나 운영자가 문제를 빠르게 파악할 수 있도록 로그를 남기는 것이 핵심. 예외 메시지, 원인, stack trace 등을 남기면 디버깅이나 추후 개선이 쉬움.
- log.error("회원 가입 중 이메일 중복 오류", e);
- 개발자나 운영자가 문제를 빠르게 파악할 수 있도록 로그를 남기는 것이 핵심. 예외 메시지, 원인, stack trace 등을 남기면 디버깅이나 추후 개선이 쉬움.
- 비즈니스 로직 보호
- 예외로 인해 데이터가 중간 상태로 저장되거나, 손상되거나, 유실되는 걸 방지하는 것이 핵심. 이때 주로 트랜잭션 + 롤백과 함께 작동함.
- 결제 중 에러 발생 → 결제 취소 처리도 롤백
- 예외로 인해 데이터가 중간 상태로 저장되거나, 손상되거나, 유실되는 걸 방지하는 것이 핵심. 이때 주로 트랜잭션 + 롤백과 함께 작동함.
- 예측 가능한 흐름 유지
- 코드가 예상한 흐름대로 동작하도록 하고, 예외 흐름은 따로 관리해서 의도치 않은 부작용을 방지하는 게 목적이야. 즉, 비정상 흐름을 명시적으로 관리.
- - try-catch 로 감싸고 예외 타입별로 분기 처리
- - 사용자 에러 vs 시스템 에러 분리 대응
- 코드가 예상한 흐름대로 동작하도록 하고, 예외 흐름은 따로 관리해서 의도치 않은 부작용을 방지하는 게 목적이야. 즉, 비정상 흐름을 명시적으로 관리.
예외 상황에서의 다양한 대응 방식 (예외 처리 전략)
- 사용자 통보 (Throw with 메시지)
- 목적 : 사용자에게 명확한 피드백을 주기 위한 예외 처리
- 흐름 : ❌ 중단됨 (ControllerAdvice 등에서 처리)
if (!user.isValid()) {
throw new BadRequestException("유효하지 않은 사용자입니다.");
}
- 재시도 / 대체 처리 (Retry / Fallback)
- 케이스 : 외부 API 실패, 네트워크 오류, 일시적 장애
- 목적 : 시스템이 멈추지 않도록 예외를 흡수하고 대체 처리
- 흐름 : 유지됨 (대체로 비동기, resilient)
Fallback
try {
return kakaoApiClient.call(...);
} catch (Exception e) {
log.warn("외부 API 실패, 기본값으로 처리");
return defaultRegionCode(); // fallback
}
Retry
public class RetryUtils {
public static <T> T retry(Supplier<T> supplier, int maxAttempts, long delayMillis) {
Exception lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return supplier.get();
} catch (Exception e) {
lastException = e;
if (attempt < maxAttempts) {
try {
Thread.sleep(delayMillis);
} catch (InterruptedException ignored) {}
}
}
}
throw new RuntimeException("All retry attempts failed", lastException);
}
}
- 무시하고 계속 진행 (Skip)
- 상황 : 배치 처리, 대량 작업 중 일부 실패
- 목적 : 하나가 실패해도 전체 작업은 끝내고 싶을 때
- 방법 : try-catch로 감싸고 실패 건만 로그 처리
for (Item item : itemList) {
try {
process(item);
} catch (Exception e) {
log.warn("항목 처리 실패: {}", item.getId());
continue; // 다음 항목으로
}
}
- 치명적 예외 처리 (Abort)
- 상황 : DB 연결 불가, 파일 시스템 문제, 필수 리소스 없음
- 방법 : 예외 발생 후 로그 → 시스템 종료, 트랜잭션 롤백
- 내부 처리 후 흐름 유지 (Logging + Isolation)
- 상황 : 영향이 크지 않은 예외 (로깅만 필요함)
- 목적 : 예외를 감지하고 기록하되, 흐름은 유지
try {
auditLogger.logSensitiveAction(user);
} catch (Exception e) {
log.error("감사 로그 실패, 하지만 계속 진행");
}
Exception 종류
Throwable
├── Error (Not usually caught by applications)
│ ├── LinkageError (Errors related to linking problems with classes)
│ │ ├── NoClassDefFoundError
│ │ └── ExceptionInInitializerError
│ ├── VirtualMachineError (Errors produced by the JVM making continuation impossible)
│ │ ├── OutOfMemoryError
│ │ └── StackOverflowError
│ └── AssertionError (Used for internal assertions in Java; normally not caught)
── Exception (The main exception class for catchable conditions)
├── IOException (Checked: Exceptions that must be caught or declared)
│ ├── FileNotFoundException (File not found)
│ └── EOFException (End of file reached unexpectedly)
├── RuntimeException (Unchecked: Represents programming errors)
│ ├── NullPointerException (Null object usage)
│ ├── IndexOutOfBoundsException (Accessing illegal indexes)
│ │ ├── ArrayIndexOutOfBoundsException (Invalid array index access)
│ │ └── StringIndexOutOfBoundsException (Invalid string index access)
│ ├── IllegalArgumentException (Calling methods with improper arguments)
│ │ ├── NumberFormatException (Failed number conversion)
│ │ └── IllegalThreadStateException (Operations on threads at illegal states)
│ └── ArithmeticException (Errors in arithmetic operations, e.g., division by zero)
└── SQLException (Checked: Errors related to database access)
1. 예외처리 키워드
- throw: 예외를 강제로 발생시키는 키워드입니다.
- throws: 메서드 내에서 예외를 직접 처리하지 않고 호출자에게 예외 처리를 위임한다는 의미입니다.
- try-catch : 던져진 exception을 해당 scope 내에서 코드로 받아서 처리를 진행함.
- try-catch-final : try catch 구문후 마지막으로 finally 안의 구문은 반드시 실행시킴.
- 예외가 발생해서 throws를 한다고해도 마지막 처리를 도와줌. 예를 들면 파일 연결끊기와 같은 자원정리라던지.
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Exception caught: " + e.getMessage());
} finally {
System.out.println("This block is always executed.");
}
2. 예외의 분류
- Checked 예외: 컴파일 단계에서 반드시 처리해야 함을 확인시키는 예외들로, 일반적으로 예상 가능한 예외
- 예: IOException, FileNotFoundException, SQLException
- * 최근에는 checked exception을 선호하지 않고 unchecked excpetion으로 변환하여 throw 하는 추세임
- Unchecked Exception이 선호되는 이유 : Flexibility with less code ; checked excpetion은 선택의 여지가 없이 직접 errohandling를 해야하나, unchecked excpetion은 errorhandling을 할지 안할지 결정할 수 있음. 그리고 handling시에는 별도의 코드복잡성 증가 없이 Global Exception Handler에서 일괄처리할 수 있기때문.
- 예: IOException, FileNotFoundException, SQLException
class Parent {
void method() throws InterruptedException {
// ...
} }
class Child extends Parent {
@Override
void method() throws Exception {
// ...
} }
public class Test {
public static void main(String[] args) {
Parent p = new Child();
try {
p.method();
} catch (InterruptedException e) {
// InterruptedException 처리 하지만 그 외 예외는 놓치게 된다
}
}
- Unchecked 예외: 실행 시점(Runtime)에서 발생하며, 컴파일러가 강제로 예외 처리를 요구하지 않습니다. 주로 프로그램의 버그, 잘못된 입력 등 예측 불가능한 상황에서 발생합니다.
- 예: IllegalArgumentException, IllegalStateException, NullPointerException, IndexOutOfBoundsException, ArithmeticException
- 상속관계에서의 예외처리 - 부모 메서드가 체크 예외를 method에서 throws하지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없다. 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다. 단, unchecked 예외는 무관하기때문에 checked excpetion을 catch한후 unchecked exception을 던지는 식으로 처리한다.
- Java에서 다형성을 이용하여 부모변수로 자식생성자를 받을때, 컴파일러는 부모변수의 구조만 읽을 수 있기때문에, 예외처리의 컴파일 검사를 부모기준으로 적용할 수 밖에없다. 때문에, 자식메소드는 부모의 제약에 따르도록 강제한다.
예외와 에러
- 에러 : 프로그램의 정상적인 실행을 방해하는 심각한 문제로, 애플리케이션 코드에서 복구할 수 없는 상황. 주로 JVM(Java Virtual Machine) 레벨에서 발생. 대부분 시스템 리소스 부족, JVM의 내부 문제로 인해 발생하므로 복구가 불가능.
- OutOfMemoryError: 메모리가 부족할 때.
- StackOverflowError: 재귀 호출이 너무 깊을 때.
- InternalError: JVM 내부 오류.
- 프로그램 실행 중 예상 가능한 문제(예: 잘못된 입력값, 네트워크 연결 문제 등)로 인해 발생. 대부분의 경우, 적절한 **예외 처리(try-catch)**를 통해 복구 가능. 예외를 처리하지 않으면 애플리케이션이 종료될 수 있으므로 예외 처리를 명시적으로 구현.
예외처리기법
- try-catch 예외처리 :
- try{ 예외가 발생할 수 있는 코드} catch(Exception e ){예외시 실행할 코드}
- Exception class
- .getMessage() : 오류에 대한 기본적인 내용을 출력해준다.
- .toString() : 더 자세한 예외 정보를 제공한다
- .printStackTrace() : 메소드가 내부적으로 예외 결과를 화면에 출력한다.
- try-catch 다중 예외처리 : 예외의 종류에 따라서 실행코드를 달리하고 싶을 경우 사용
- try{ 예외가 발생할 수 있는 코드} catch(예외클래스 인스턴스1; ArithmeticException ) { 예외시 실행할 코드; } catch (예외클래스 인스턴스2; ArrayIndexOutOfBoundsException){예외시 실행할 코드} cath(Exception e){예외시 실행할 코드}
- finally : try{ 예외가 발생할 수 있는 코드} catch(구체적인 예외 case ) { 예외시 실행할 코드} finally {예외와 관계없이 항상실행되는 코드}
- 사용자 정의 Exception : class를 생성하고 RuntimeException을 extends해준다. checked exception은 transaction을 rollback해주지 않음 그리고 무엇이든 throw/try-catch해주어야하는 불편함이 존재하기 때문에 unchecked exception인 runtimeexception을 extends한다
Nullpointexcpetion
- Common Sources of null Values:
- Uninitialized Variables
- Method Returning null
- Database Queries Returning null
- How to Prevent NullPointerException:
- UseOptional
- java.util.Optional
- Optional<T> : Optional이라는 구현체를 return해서 inflow가 존재하지 않더라도 null을 반환하지않고 empty optional을 return함. List를 반환하는 쿼리에는 null이 아니라 empty list가 발생하므로, single value를 return하는 메소드에 적합함.
- .ofNullable() : to safely create an Optional instance that may or may not contain a null value.
- isPresent(): Returns true if there is a value present, otherwise false.
- get(): Returns the value if present; throws NoSuchElementException if no value is present.
- filter(list -> !list.isempty) : empty가 아닌 list만 통과시키고 그 외에는 optional.empty()를 반환함.
- orElse(T other): Returns the value if present; returns other otherwise.
- orElseGet(Supplier<? extends T> other): Returns the value if present; returns the result produced by the supplier otherwise.
- orElseThrow(Supplier<? extends X> exceptionSupplier): Returns the contained value, if present, otherwise throws an exception to be created by the provided supplier.
- 여기서 throw가 되면 exceptionhandler가 제어흐름을 받아서 예약된 로직을 수행함.
- Optional<T> : Optional이라는 구현체를 return해서 inflow가 존재하지 않더라도 null을 반환하지않고 empty optional을 return함. List를 반환하는 쿼리에는 null이 아니라 empty list가 발생하므로, single value를 return하는 메소드에 적합함.
- java.util.Optional
- NULLCHECKING
- 예외상황 checking
- ObjectUtils.isEmpty(company) vs company == null
- company == null - Basic Check: This is a straightforward nullity check. It simply tests whether the company object reference is null. This means it checks if the object exists at all.
- ObjectUtils.isEmpty(company) - Advanced Check: It checks if the object is either null, empty, or in some cases, based on the type of the object, if it has a default uninitialized state.
- ObjectUtils.isEmpty(company) vs company == null
- 예외상황 checking
- UseOptional
Best Practices: When to Log in a Spring Boot Application
- INFO : Log Application Start & Stop
- INFO : Log Incoming HTTP Requests & Responses
- INFO : Log Important Business Logic Events
- INFO : Log External API Calls
- DEBUG : Log Database Queries (When Necessary)
- INFO : Log Scheduled Jobs & Background Tasks
- ERROR : Log Exception Handling
- WARN : Log Security-Related Events
Best Practices: When to Use try catch in a Spring Boot Application
- to Customize log an error, return a specific response,
- To Recover from an Exception (Retry Logic)
- or retry an operation before throwing a new exception.
- To Clean Up Resources (Using finally)
- When Calling External APIs (Handling API Failures)
- Handling Multiple Exceptions in One Block
'개발기술 > Java' 카테고리의 다른 글
Java Blocking I/O vs non-Blocking I/O (0) | 2025.04.06 |
---|---|
Java 코딩구현 - Java NIO, Spring Multipart (0) | 2025.04.04 |
Java의 메모리 영역과 static의 의미 (0) | 2025.01.15 |
Java 코딩구현 심화 : 스트림,람다식 (0) | 2024.09.20 |
Java 멀티쓰레드 (0) | 2024.08.12 |