본 포스팅은 인프런 - Spring Boot를 이용한 RESTful Web Service 개발 강의를 바탕으로 공부하고 정리한 글입니다.
Exception Handling
프로그래밍에서 예외 처리는 아주 중요하면서도 아주 어렵다 😓
상세하고 다양하게 예외를 잡아 처리해준다면, 클라이언트도 그렇고 서버도 그렇고 더 안정적인 프로그램이 될 수 있도록 도와준다.
예외 처리를 하는 경우와 방법은 다양한데, 여러 예외 처리들을 적용하다보면 코드가 엄청나게 복잡해진다.
if문으로 잡든 try-catch로 잡든 상위 메소드로 예외처리를 위임하든 코드는 복잡해진다.
그렇게 되면 유지보수하기가 아주 어려워진다는 것이 문제이다.
또한 비즈니스 로직에 집중하기 어렵고, 비즈니스 로직과 관련된 코드보다 예외 처리를 위한 코드가 더 많아지는 경우도 생기기 마련이다.
이러한 문제를 개선하기 위한 예외 처리 방법을 알아보도록 하겠다.
간단한 사용자 관리 예제를 통해 알아보자.
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Date joinDate;
}
@Service
public class UserDaoService {
private static List<User> users = new ArrayList<>();
private static int usersCount = 3;
static {
users.add(new User(1, "Anne", new Date()));
users.add(new User(2, "Jolly", new Date()));
users.add(new User(3, "Alice", new Date()));
}
public User findOne(int id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}
}
users에 3개의 초기 데이터를 저장해주었다.
예외 핸들링 전
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserDaoService service;
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
return service.findOne(id);
}
}
현재 데이터가 3개 저장되어 있기 때문에 사용자 id값을 10으로 해서 예외를 발생시켜보았다.
findOne(10)의 반환값이 null로 예외가 발생하지 않고 성공 상태코드인 200 OK를 반환하고 있다.
id값이 10인 사용자는 없는데 성공 응답이라고....? 이것은 굉장히 잘못되었음을 알 수 있다.
예외 핸들링 후
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserDaoService service;
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%S] not found", id));
}
return user;
}
}
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
findOne(id)의 결과값이 null일 경우 즉, 찾을 수 없는 사용자 id값으로 조회를 할 경우 UserNotFoundException이 발생하도록 예외 클래스를 만들어 주었다.
그 결과 200 OK가 아닌 500 에러코드가 전달되는 것을 확인할 수 있다.
또한 예외 발생 시 전달해준 예외 메세지 또한 전달되는 것을 확인할 수 있다.
성공 상태코드가 아닌 에러 상태코드를 전달하는 것에는 성공했지만 아쉬운 점이 조금 있다.
조금 더 상세한 에러코드를 전달할 수 있을지? Body에 내가 전달하고자 하는 예외 내용만 담아서 보낼 순 없을지? 생각이 든다🤫
업그레이드 - 적절한 상태코드 지정 (@ResponseStatus)
@ResponseStatus(NOT_FOUND) // 404
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
이전에 만들어준 UserNotFoundException 예외 클래스에 @ResponseStatus를 붙여줘 전달하고자 하는 에러 상태코드를 지정해줄 수 있다.
예제에서는 HttpStatus.NOT_FOUND(404)를 지정해주었다.
Postman으로 테스트 해보니 500이 아닌 조금 더 적절한 404가 전달되는 것을 확인할 수 있다.
하지만 Body에 trace와 같이 굳이 필요하지 않은 내용까지 담겨 있다는 것이 아쉽다!
업그레이드 - 스프링 AOP 활용
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExceptionResponse {
private Date time;
private String message;
private String details;
}
@RestController
@ControllerAdvice // 모든 컨트롤러에서 발생할 수 있는 예외를 잡아 처리해주는 어노테이션
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ExceptionResponse handleAllException(Exception ex, WebRequest request) {
return new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
}
}
스프링의 AOP를 활용하면 해결할 수 있다.
이제 trace 없이 정확하게 전달하고자 하는 예외 메세지만 전달할 수 있게 되었다.
이때 사용된 @ControllerAdvice 같은 경우 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아서 처리해주는 어노테이션이며, @ExceptionHandler 같은 경우 이 예외 핸들러를 사용하겠다고 알리는 어노테이션이다.
@ExceptionHandler의 인자로 캐치하고 싶은 예외 클래스를 등록해 사용하면 된다.
여기서는 모든 예외 클래스의 상위 클래스인 Exception.class를 등록해 예외를 처리해주었다.
그 결과 ExceptionResponse에 맞게 전달하고자 하는 예외 메세지만 전달할 수 있게 되었지만, 이전에 만들어준 UserNotFoundException의 404가 아닌 500 상태코드가 전달된다.
404를 전달하기 위해서는 UserNotFoundException을 처리해주는 예외 핸들러도 추가로 등록해주면 된다.
@RestController
@ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public final ExceptionResponse handleUserNotFoundException(Exception ex, WebRequest request) {
return new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
}
}
UserNotFoundException을 처리하는 예외 핸들러를 추가로 등록해줌으로써 404 코드 전달까지 해결된 것을 확인할 수 있다.
Reference
인프런 - Spring Boot를 이용한 RESTful Web Service 개발
'🌱 Spring > REST' 카테고리의 다른 글
REST API를 설계할 때 고려해야 할 사항 (0) | 2022.09.22 |
---|---|
Response 데이터 제어하기 : @JsonIgnore, @JsonIgnoreProperties, SimpleBeanPropertyFilter (0) | 2022.09.15 |
HTTP Status Code 제어하기 : 201 Created 반환하기 (0) | 2022.09.06 |
REST API (0) | 2022.08.28 |