저번 AOP 포스팅에선 메소드 이름, 파라미터, 반환값을 로깅하는 작업을 구현해 보았습니다. 이번 포스팅에선 AOP의 또 다른 대표 예시인 예외처리를 @RestControllerAdvice 어노테이션을 사용해서 전역적으로 발생하는 예외를 한곳에서 처리하는 작업을 구현해 보겠습니다.
@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은 상수의 집합이며, 이는 에러 코드가 일관되게 사용되고 유지되도록 도와줍니다. 잘못된 문자열이나 상수로 인한 오류를 방지할 수 있습니다.
@AllArgsConstructor
@Getter
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
}
RuntimeException을 상속받아 에러코드를 출력하는 CustomException을 생성합니다.
@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 문의 사용을 최소화 함으로서 비즈니스 로직에 더욱 집중할 수 있습니다.
POST 요청을 보내야 하는 상황에서, GET 요청을 보냈을 경우 HttpRequestMethodNotSupportedException이 터지면서 위에서 정의한 GlobalExceptionHandler에서 Exception을 잡게 됩니다.