[Spring] AOP를 이용하여 Global Exception Handling 구현

Donghoon Jeong·2023년 11월 24일
0

Spring

목록 보기
12/15
post-thumbnail

저번 AOP 포스팅에선 메소드 이름, 파라미터, 반환값을 로깅하는 작업을 구현해 보았습니다. 이번 포스팅에선 AOP의 또 다른 대표 예시인 예외처리를 @RestControllerAdvice 어노테이션을 사용해서 전역적으로 발생하는 예외를 한곳에서 처리하는 작업을 구현해 보겠습니다.


AOP를 이용하여 Global Exception Handling 구현

ErrorCode Enum 생성

@AllArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ErrorCode {

    USER_NOT_FOUND("사용자가 존재하지 않습니다."),
    TODO_NOT_FOUND("투두가 존재하지 않습니다."),
    DUPLICATE_USER_ID("이미 사용 중인 회원 아이디입니다."),
    WRONG_PASSWORD("비밀번호가 틀렸습니다."),
    TOKEN_NOT_FOUND("토큰이 존재하지 않습니다."),
    INVALID_TOKEN("유효하지 않은 토큰입니다.");

    private final String message;

    public String getStatus(){
        return name();
    }

    public String getMessage(){
        return message;
    }
    
}

enum은 상수의 집합이며, 이는 에러 코드가 일관되게 사용되고 유지되도록 도와줍니다. 잘못된 문자열이나 상수로 인한 오류를 방지할 수 있습니다.

CustomException 생성

@AllArgsConstructor
@Getter
public class CustomException extends RuntimeException {

    private final ErrorCode errorCode;

}

RuntimeException을 상속받아 에러코드를 출력하는 CustomException을 생성합니다.

GlobalExceptionHandler 생성

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({CustomException.class, NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<Object> handleException(Exception e) {
        HttpHeaders headers = new HttpHeaders();
        HttpStatus status = determineHttpStatus(e);

        Map<String, Object> errors = new HashMap<>();
        errors.put("Status", getStatus(e));
        errors.put("ErrorMessage", getErrorMessage(e));
        errors.put("Date", String.valueOf(new Date()));

        return new ResponseEntity<>(errors, headers, status);
    }

    private HttpStatus determineHttpStatus(Exception e) {
        if (e instanceof CustomException) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        } else if (e instanceof NoHandlerFoundException) {
            return HttpStatus.NOT_FOUND;
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
            return HttpStatus.METHOD_NOT_ALLOWED;
        }
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }

    private String getStatus(Exception e) {
        if (e instanceof CustomException) {
            return ((CustomException) e).getErrorCode().getStatus();
        } else if (e instanceof NoHandlerFoundException) {
            return String.valueOf(HttpStatus.NOT_FOUND);
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
            return String.valueOf(HttpStatus.METHOD_NOT_ALLOWED);
        }
        return null;
    }

    private String getErrorMessage(Exception e) {
        if (e instanceof CustomException) {
            return ((CustomException) e).getErrorCode().getMessage();
        } else if (e instanceof NoHandlerFoundException) {
            return "해당 요청을 찾을 수 없습니다.";
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
            return "지원되지 않는 HTTP 메서드입니다.";
        }

        return "Internal Server Error";
    }

}

@RestControllerAdvice 어노테이션을 사용하여 전역 예외 처리 클래스임을 나타냅니다.

CustomException.class, NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class가 발생 했을 경우 이 클래스에서 예외 처리를 담당하게됩니다.


결과

회원가입 시 아이디가 중복된 경우

ErrorCode를 사용하여 에러 코드를 정의하고 CustomException에서 이를 활용함으로써, 일관된 오류 처리 및 응답을 구현할 수 있습니다.

이를 통해 코드의 가독성이 향상되고 유지보수가 쉬워집니다.

@Service
@RequiredArgsConstructor
public class UserService {

	private final UserRepository userRepository;
    
	@Transactional
    public UserResponseDto signup(UserRequestDto userRequestDto) {

        User user = User.builder()
                .username(userRequestDto.getUsername())
                .password(passwordEncryptionService.encrypt(userRequestDto.getPassword()))
                .build();

        if (userRepository.existsByUsername(user.getUsername())) {
            throw new CustomException(ErrorCode.DUPLICATE_USER_ID);
        }

        userRepository.save(user);

        return UserResponseDto.builder()
                .username(userRequestDto.getUsername())
                .build();
    }
    
    ...
    
}
@Service
@RequiredArgsConstructor
@Transactional
public class TodoService {

	private final TodoRepository todoRepository;
    
	@Transactional(readOnly = true)
    public TodoListDto findTodoById(Long id, HttpServletRequest request, HttpServletResponse response) {

        User user = getUserFromServlet(request);
        user = getUser(response, user);

        Todo todo = todoRepository.findByIdAndUser(id, user)
                .orElseThrow(() -> new CustomException(ErrorCode.TODO_NOT_FOUND));

        return TodoListDto.builder()
                .title(todo.getTitle())
                .completed(todo.getCompleted())
                .build();
    }
    
    ...
    
}

또한 위 코드처럼 Service 계층에서 try-catch 문의 사용을 최소화 함으로서 비즈니스 로직에 더욱 집중할 수 있습니다.

클라이언트가 서버에 보낸 HTTP 요청의 메서드가 서버에서 지원되지 않는 경우

POST 요청을 보내야 하는 상황에서, GET 요청을 보냈을 경우 HttpRequestMethodNotSupportedException이 터지면서 위에서 정의한 GlobalExceptionHandler에서 Exception을 잡게 됩니다.

profile
정신 🍒 !

0개의 댓글