에러 처리
기본 원칙
- 전역 예외 처리:
@RestControllerAdvice사용 - API 응답에 스택트레이스 노출 금지
- 에러 응답에 내부 구현 정보 (클래스명, SQL 등) 노출 금지
- 비즈니스 예외는 커스텀 Exception 클래스 작성
- 예상 못한 예외는 ERROR 레벨 로깅 후 공통 에러 응답 반환
에러 응답 형식
{
"success": false,
"errorCode": "400",
"message": "에러 메시지"
}
성공 응답 형식
{
"success": true,
"data": { ... }
}
목록 조회:
{
"success": true,
"data": [ ... ],
"totalCount": 100
}
GlobalExceptionHandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 비즈니스 예외 (400)
@ExceptionHandler(BusinessException.class)
public ResponseJson<Void> handleBusiness(BusinessException e) {
log.warn("비즈니스 예외: {}", e.getMessage());
return ResponseJson.fail(e.getErrorCode(), e.getMessage());
}
// 검증 실패 (400)
@ExceptionHandler(ValidationException.class)
public ResponseJson<Void> handleValidation(ValidationException e) {
log.warn("검증 실패: {}", e.getMessage());
return ResponseJson.fail(e.getErrorCode(), e.getMessage());
}
// Bean Validation 실패 (400)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseJson<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseJson.fail("400", message);
}
// 예상 못한 예외 — ERROR 레벨 로깅 후 공통 에러 응답
@ExceptionHandler(Exception.class)
public ResponseJson<Void> handleGeneral(Exception e) {
log.error("서버 오류 발생", e); // 로그에 스택트레이스 기록
return ResponseJson.fail("500", "서버 오류가 발생했습니다.");
}
}
커스텀 Exception 클래스
비즈니스 예외는 커스텀 Exception 클래스를 작성한다.
// 기본 비즈니스 예외
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String message) {
super(message);
this.errorCode = "400";
}
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// 도메인별 예외
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(Long userSn) {
super("404", "사용자를 찾을 수 없습니다: " + userSn);
}
}
public class DuplicateUserIdException extends BusinessException {
public DuplicateUserIdException(String userId) {
super("400", "이미 사용 중인 아이디입니다: " + userId);
}
}
커스텀 예외 사용 패턴
@Transactional(readOnly = true)
public UserVo get(UserVo vo) {
UserVo result = userMapper.get(vo);
if (result == null) {
throw new UserNotFoundException(vo.getUserSn());
}
return result;
}
@Transactional(rollbackFor = Exception.class)
public void regist(UserVo vo) {
if (userMapper.existsByUserId(vo.getUserId()) > 0) {
throw new DuplicateUserIdException(vo.getUserId());
}
userMapper.regist(vo);
}
HTTP 상태코드 기준
| 상황 | 상태코드 | 설명 |
|---|---|---|
| 성공 | 200 OK | 정상 처리 |
| 잘못된 요청 | 400 Bad Request | 검증 실패, 비즈니스 오류 |
| 미인증 | 401 Unauthorized | 토큰 없음/만료 |
| 권한 없음 | 403 Forbidden | 권한 부족 |
| 리소스 없음 | 404 Not Found | 존재하지 않는 리소스 |
| 서버 오류 | 500 Internal Server Error | 예상치 못한 오류 |
운영 환경 설정
# application-prod.yml
server:
error:
include-stacktrace: never # 스택트레이스 응답 포함 금지
include-message: never # 예외 메시지 응답 포함 금지
include-binding-errors: never
체크리스트
- [ ] 전역 예외 처리:
@RestControllerAdvice사용 - [ ] 비즈니스 예외: 커스텀 Exception 클래스 작성
- [ ] 예상 못한 예외:
log.error("설명", e)ERROR 로깅 후 공통 에러 응답 - [ ] 응답에 스택트레이스 미포함
- [ ] 응답에 내부 클래스명/SQL 미포함
- [ ] 운영 환경 에러 설정 (
include-stacktrace: never)