API를 개발하는 도중 발생하게 되는 오류는 매우 다양하다.
문법 오류 뿐만 아니라 맞지 않는 데이터 타입, null값을 허용하지 않는 곳에서 사용 등등..
오늘 다루어볼 내용은 DB와의 데이터 교환중 다양하게 발생되는 Exception을 처리하는 3가지 방식에 대해서 정리해보려고 한다.
이 방식은 Exception이 발생하는 부분의 코드를 if-else로 감싸 처리하는 방식이다.
private final UserService userService;
@PostMapping
public ResponseEntity<?> createUser(@RequestBody UserDto userDto) {
// 입력값 검증
if (userDto.getEmail() == null || userDto.getEmail().isEmpty()) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "이메일은 필수 입력값입니다.", LocalDateTime.now()));
}
if (userDto.getPassword().length() < 8) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "비밀번호는 8자 이상이어야 합니다.", LocalDateTime.now()));
}
if (userService.existsByEmail(userDto.getEmail())) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse(409, "이미 존재하는 이메일입니다.", LocalDateTime.now()));
}
User user = userService.createUser(userDto);
return ResponseEntity.ok(user);
이 방식은 Exception이 발생하는 부분의 코드를 try-catch문으로 감싸 처리하는 방식이다.
아래의 예시에선 e.getMessage()를 사용해 상황에 맞는 에러메세지를 가져오도록 작성되어있다.
private final UserService userService;
@PostMapping
public ResponseEntity<?> createUser(@RequestBody UserDto userDto) {
try {
validateUserDto(userDto);
User user = userService.createUser(userDto);
return ResponseEntity.ok(user);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, e.getMessage(), LocalDateTime.now()));
} catch (DuplicateEmailException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse(409, "이미 존재하는 이메일입니다.", LocalDateTime.now()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(500, "서버 오류가 발생했습니다.", LocalDateTime.now()));
}
}
private void validateUserDto(UserDto userDto) {
if (userDto.getEmail() == null || userDto.getEmail().isEmpty()) {
throw new IllegalArgumentException("이메일은 필수 입력값입니다.");
}
if (userDto.getPassword().length() < 8) {
throw new IllegalArgumentException("비밀번호는 8자 이상이어야 합니다.");
}
이 방식은 Exception에 대한 오류메세지만 설정하고 Exception의 판별 및 처리는 ExceptionHandler가 모두 관리하게 하는 방식이다.
모든 Exception을 관리하는 GlobalExceptionHandler를 작성한다.
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody UserDto userDto) {
validateUserDto(userDto);
return ResponseEntity.ok(userService.createUser(userDto));
}
private void validateUserDto(UserDto userDto) {
if (userDto.getEmail() == null || userDto.getEmail().isEmpty()) {
throw new ValidationException("이메일은 필수 입력값입니다.");
}
if (userDto.getPassword().length() < 8) {
throw new ValidationException("비밀번호는 8자 이상이어야 합니다.");
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
ErrorResponse error = ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST.value())
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicateEmailException(DuplicateEmailException e) {
ErrorResponse error = ErrorResponse.builder()
.status(HttpStatus.CONFLICT.value())
.message("이미 존재하는 이메일입니다.")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllException(Exception e) {
ErrorResponse error = ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("서버 내부 오류가 발생했습니다.")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
프로젝트의 크기가 커질수록 @ExceptionHandler의 사용이 권장된다.
코드량이 많을수록 관리해줘야 하는 Exception의 종류가 늘어날 수 있고,
if-else 방식이나 try-catch 방식을 사용하게 되면 관리가 어려울 수 있기 때문이다.
하지만 극히 드물게 발생하는 Exception의 경우 그 코드 부분만 try-catch문을 이용해 처리하는 경우도 있다.
결론적으로 상황에 맞게 Exception을 처리해야 한다는 것이다.