예외 처리
1. 커스텀 예외 구조 (이 프로젝트)
RuntimeException
└── BusinessException (추상 클래스 - 공통 부모)
├── EntityNotFoundException (404)
├── AccessDeniedException (403)
├── BusinessRuleException (400)
└── DuplicateResourceException (409)
// BusinessException.java - 공통 부모
@Getter
public abstract class BusinessException extends RuntimeException {
private final HttpStatus status;
private final String errorCode;
protected BusinessException(String message, HttpStatus status, String errorCode) {
super(message);
this.status = status;
this.errorCode = errorCode;
}
}
// EntityNotFoundException.java
public class EntityNotFoundException extends BusinessException {
public EntityNotFoundException(String message) {
super(message, HttpStatus.NOT_FOUND, "NOT_FOUND");
}
// 편의 메서드
public static EntityNotFoundException of(String entityName, Long id) {
return new EntityNotFoundException(entityName + "을(를) 찾을 수 없습니다: " + id);
}
}
2. Service에서 예외 던지기
// CommunityService.java
public CommunityDTO getCommunityDetail(Long id) {
CommunityEntity community = communityRepository.findById(id)
.orElseThrow(() -> EntityNotFoundException.of("게시글", id));
// 없으면 EntityNotFoundException 발생 → 404
return CommunityDTO.from(community);
}
public void updateCommunity(Long id, CommunityUpdateDTO dto, String username) {
CommunityEntity community = communityRepository.findById(id)
.orElseThrow(() -> EntityNotFoundException.of("게시글", id));
if (username != null && !community.isWrittenBy(username)) {
throw AccessDeniedException.forUpdate("게시글"); // 403
}
community.update(dto.getTitle(), dto.getContent());
}
3. @ControllerAdvice - 전역 예외 처리
모든 Controller에서 발생하는 예외를 한 곳에서 처리.
// SSR 방식 - 에러 페이지로 이동
// annotations = Controller.class: @Controller에서 발생하는 예외만 잡음
// (@RestController 예외는 ApiExceptionHandler가 JSON으로 처리)
@ControllerAdvice(annotations = Controller.class)
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound(EntityNotFoundException e, Model model) {
model.addAttribute("status", 404);
model.addAttribute("message", e.getMessage());
return "error"; // error.html로 이동
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleException(Exception e, Model model) {
model.addAttribute("status", 500);
model.addAttribute("message", "서버 내부 오류가 발생했습니다.");
return "error";
}
}
4. @ControllerAdvice vs @RestControllerAdvice
@ControllerAdvice |
@RestControllerAdvice |
|
|---|---|---|
| 반환 | 뷰 이름 (SSR) | JSON 데이터 (REST API) |
| 적합 | SSR (Thymeleaf) | REST API |
// REST API 방식
// annotations = RestController.class: @RestController에서 발생하는 예외만 잡음
@RestControllerAdvice(annotations = RestController.class)
public class ApiExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException e) {
return ResponseEntity.status(404)
.body(new ErrorResponse(404, "NOT_FOUND", e.getMessage()));
}
}
5. 예외 처리 흐름
Service에서 EntityNotFoundException 발생
↓
Controller까지 전파 (try-catch 없으면 그냥 올라옴)
↓
@ControllerAdvice의 @ExceptionHandler(EntityNotFoundException.class) 가 받음
↓
SSR: error.html 렌더링
REST: JSON ErrorResponse 반환