예외 처리 전략
기본 원칙
- API 응답에 스택트레이스 노출 금지
- 에러 응답에 내부 구현 정보 (클래스명, SQL 등) 노출 금지
- 예외 발생 시 적절한 HTTP 상태코드 사용
- 전역 예외는
GlobalExceptionHandler에서 일관 처리 ERROR레벨 예외 발생 시 Slack 알람 자동 발송
GlobalExceptionHandler
// com.scraping.agent.global.exception.GlobalExceptionHandler
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private final SlackService slackService;
// 비즈니스 예외 (400)
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(ApiResponse.fail(e.getMessage()));
}
// 서버 상태 오류 (500) — Slack 알람 발송
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ApiResponse<Void>> handleIllegalState(
IllegalStateException e, HttpServletRequest request) {
slackService.sendError(request.getRequestURI(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail(e.getMessage()));
}
// 기타 서버 오류 (500) — Slack 알람 발송
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGeneral(
Exception e, HttpServletRequest request) {
slackService.sendError(request.getRequestURI(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail("서버 오류가 발생했습니다."));
}
}
Slack 알람 연동
GlobalExceptionHandler에서 SlackService.sendError()를 호출하여 500 에러 발생 시 Slack 채널에 자동 알림이 발송된다.
*[500 에러]* 서버 오류 발생
• 경로: `/api/v1/posts`
• 예외: `NullPointerException`
• 메시지: Cannot invoke method getId() on null object
슬랙 알람이 발송되는 시점:
IllegalStateException발생 (서버 상태 오류)Exception기반 일반 서버 오류SlackEventListener를 통한 서버 시작/종료 이벤트
// 배치 실패 시 직접 호출
slackService.sendBatchFail("DigestScheduler", exception);
검증 실패 처리
MethodArgumentNotValidException을 핸들러에 추가하여 @Valid 검증 실패를 처리한다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(ApiResponse.fail(message));
}
사용자 정의 예외
// 도메인별 예외 클래스 — domain/{도메인}/ 패키지에 위치
public class PostNotFoundException extends RuntimeException {
public PostNotFoundException(Long id) {
super("포스트를 찾을 수 없습니다: " + id);
}
}
public class NewsCollectException extends RuntimeException {
public NewsCollectException(String source, Throwable cause) {
super("뉴스 수집 실패: " + source, cause);
}
}
Service에서 사용:
public PostDetailRes getPost(Long id) {
return postRepository.findById(id)
.map(PostDetailRes::from)
.orElseThrow(() -> new IllegalArgumentException("포스트를 찾을 수 없습니다: " + id));
}
null 반환 금지
// 잘못된 예 — null 반환
public PostDetailRes getPost(Long id) {
return postRepository.findById(id)
.map(PostDetailRes::from)
.orElse(null); // 금지
}
// 올바른 예 — 예외로 처리
public PostDetailRes getPost(Long id) {
return postRepository.findById(id)
.map(PostDetailRes::from)
.orElseThrow(() -> new IllegalArgumentException("포스트를 찾을 수 없습니다: " + id));
}
// 또는 Optional 반환
public Optional<PostDetailRes> findPost(Long id) {
return postRepository.findById(id).map(PostDetailRes::from);
}
Thymeleaf 에러 페이지
templates/
└── error/
├── 400.html # Bad Request
├── 403.html # Forbidden
├── 404.html # Not Found
└── 500.html # Internal Server Error
운영 환경 설정
# application-prod.yml
server:
error:
include-stacktrace: never # 스택트레이스 응답 포함 금지
include-message: never # 예외 메시지 응답 포함 금지
include-binding-errors: never
에러 응답 형식
// 400 Bad Request (입력값 오류)
{
"success": false,
"data": null,
"error": "title: 제목은 필수입니다."
}
// 500 Internal Server Error
{
"success": false,
"data": null,
"error": "서버 오류가 발생했습니다."
}
체크리스트
- [ ]
@RestControllerAdvice전역 예외 처리 확인 - [ ] 500 에러 시
SlackService.sendError()호출 확인 - [ ] API 응답에 스택트레이스 미포함
- [ ]
null반환 없음 (Optional 또는 예외 처리) - [ ] 운영 환경
include-stacktrace: never설정 확인