Java 예외 처리 전략


효과적인 예외 처리는 안정적인 애플리케이션의 핵심입니다. 체크 예외와 언체크 예외를 적절히 사용하고, 의미 있는 예외 메시지와 로깅으로 디버깅을 쉽게 만들어야 합니다.



예외 계층 구조


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