Exception 처리

Metronon·2024년 12월 20일

REST-API

목록 보기
1/12
post-thumbnail

API를 개발하는 도중 발생하게 되는 오류는 매우 다양하다.
문법 오류 뿐만 아니라 맞지 않는 데이터 타입, null값을 허용하지 않는 곳에서 사용 등등..

오늘 다루어볼 내용은 DB와의 데이터 교환중 다양하게 발생되는 Exception을 처리하는 3가지 방식에 대해서 정리해보려고 한다.

1. if-else를 사용한 Exception 처리

이 방식은 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);

if-else 방식의 장점

  • 코드가 직관적으로 작성되어 이해가 쉽다.
  • 디버깅이 쉽다.

if-else 방식의 단점

  • 중복되는 코드가 많아져 코드가 길어지고 복잡해진다.
  • 일관성 있는 예외 처리가 어렵다. (조건을 확실히 명시해야 하기 때문이다)

2. try-catch를 사용한 Exception 처리

이 방식은 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자 이상이어야 합니다.");
       }

try-catch 방식의 장점

  • 예외 처리 로직을 분리할 수 있다.
  • 예외 발생 시점에서 즉시 처리할 수 있다.

try-catch 방식의 단점

  • if-else 방식과 동일하게 중복이 발생한다.
  • 예외 처리 로직이 비즈니스 로직과 섞인다.

3. @ExceptionHandler를 사용한 Exception 처리

이 방식은 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 방식의 장점

  • 예외 처리 로직이 한 곳에 집중되어 추가 작성시 편리하다.
  • 일관된 예외 처리가 가능해진다.
  • 코드가 깔끔하고 유지보수가 쉬워진다.
  • 비즈니스 로직과 예외 처리 로직이 분리된다.

@ExceptionHandler 방식의 단점

  • 세부적인 예외 처리가 필요한 경우, 추가적으로 코드를 작성해야 한다.
  • 예외 클래스를 많이 만들어야 하는 경우도 있다.

프로젝트의 크기가 커질수록 @ExceptionHandler의 사용이 권장된다.
코드량이 많을수록 관리해줘야 하는 Exception의 종류가 늘어날 수 있고,
if-else 방식이나 try-catch 방식을 사용하게 되면 관리가 어려울 수 있기 때문이다.

하지만 극히 드물게 발생하는 Exception의 경우 그 코드 부분만 try-catch문을 이용해 처리하는 경우도 있다.
결론적으로 상황에 맞게 Exception을 처리해야 한다는 것이다.

profile
비전공 개발 지망생의 벨로그입니다!

0개의 댓글