Contents
see ListJava 예외 처리 전략
효과적인 예외 처리는 안정적인 애플리케이션의 핵심입니다. 체크 예외와 언체크 예외를 적절히 사용하고, 의미 있는 예외 메시지와 로깅으로 디버깅을 쉽게 만들어야 합니다.
예외 계층 구조
Throwable
├── Error (시스템 오류, 처리 불가)
│ └── OutOfMemoryError, StackOverflowError
└── Exception
├── Checked Exception (컴파일러가 체크)
│ └── IOException, SQLException
└── RuntimeException (Unchecked, 런타임 체크)
└── NullPointerException, IllegalArgumentException체크 vs 언체크 예외 선택 기준
| 체크 예외 | 언체크 예외 |
|---|---|
| 호출자가 복구 가능할 때 | 프로그래밍 오류일 때 |
| 외부 리소스 접근 실패 | 잘못된 인자, null 참조 |
| 반드시 처리해야 할 때 | 개발자 실수 |
커스텀 예외 정의
// 비즈니스 예외 기본 클래스
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
// 에러 코드 enum
public enum ErrorCode {
USER_NOT_FOUND("U001", "사용자를 찾을 수 없습니다"),
INVALID_PASSWORD("U002", "비밀번호가 일치하지 않습니다"),
DUPLICATE_EMAIL("U003", "이미 등록된 이메일입니다");
private final String code;
private final String message;
// constructor, getters
}
// 구체적인 예외
public class UserNotFoundException extends BusinessException {
public UserNotFoundException() {
super(ErrorCode.USER_NOT_FOUND);
}
}예외 처리 Best Practices
// 1. 구체적인 예외를 먼저 catch
try {
// 작업
} catch (FileNotFoundException e) {
// 파일 없음 처리
} catch (IOException e) {
// 기타 I/O 오류
}
// 2. 예외 체이닝 (원인 보존)
try {
parseData(input);
} catch (ParseException e) {
throw new BusinessException(ErrorCode.PARSE_ERROR, e);
}
// 3. try-with-resources 사용
try (InputStream is = new FileInputStream(file);
BufferedReader reader = new BufferedReader(
new InputStreamReader(is))) {
return reader.readLine();
}
// 4. 불필요한 catch 금지
// BAD
try {
doSomething();
} catch (Exception e) {
throw e; // 의미없는 재던지기
}
// 5. 로깅 후 던지기 (중복 로깅 주의)
try {
process();
} catch (Exception e) {
log.error("처리 실패: {}", e.getMessage());
throw new ProcessException(e);
}Spring 글로벌 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException e) {
log.warn("비즈니스 예외: {}", e.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getErrorCode()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(
Exception e) {
log.error("예상치 못한 오류", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of("서버 오류가 발생했습니다"));
}
}
// 에러 응답 DTO
@Getter
public class ErrorResponse {
private String code;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
this.timestamp = LocalDateTime.now();
}
}Optional로 null 예외 방지
// Optional 활용
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
}
// null 체크 대신 Optional
Optional.ofNullable(value)
.map(String::trim)
.filter(s -> !s.isEmpty())
.orElse("default");예외 처리 안티패턴
- 빈 catch 블록 (예외 삼키기)
- Exception으로 모든 것 catch
- 예외를 흐름 제어에 사용
- 필요 이상으로 넓은 범위의 try