오늘은 Test code 작성, Spring AOP, 그리고 예외처리에 대해 공부했다.
모두 새롭고 신기했지만, 그중에서도 조금은 익숙했던 예외처리에 대해서만 얘기해볼까 한다.
그동안 null을 대비하여, 혹은 잘못된 요청을 대비하여 무수한 exception을 던졌지만 정확한 사용방법이나 원리를 이해하진 못하고 있었다. 하지만 어제 외부API를 가져오는 방법을 공부하며, 요청과 응답에 대해 깊게 공부하니 이번 예외처리 또한 쉰게 이해 할 수 있었다.
간단하게 상태코드는
가 있으며 더 자세한 것은 HttpStatus enum에 들어가 확인해 볼 수 있다.
다음과 같이 try-catch를 이용하여 잘 실행이 되면 try에서 끝나고 빠져나가고, 예외가 발생하면 throw된 exception이 catch로 들어간다.
(finally는 try 혹은 catch가 끝나고 무조건 실행된다.)
@PostMapping("/folders")
public ResponseEntity<RestApiException> addFolders(@RequestBody FolderRequestDto folderRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
try {
List<String> folderNames = folderRequestDto.getFolderNames();
folderService.addFolders(folderNames, userDetails.getUser());
return new ResponseEntity<>(HttpStatus.OK);
} catch(IllegalArgumentException ex) {
RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(
// HTTP body
restApiException,
// HTTP status code
HttpStatus.BAD_REQUEST);
}
}
ResponseEntity 로 return하는데, 어제 공부한 것처럼 Json타입으로 전달하는 것이다. 아래와 같이 들어가야 하는 값도 친절히 알려준다.
ResponseEntity는 Status code, headers, body에 대한 정보를 담을 수 있는데, 위의 코드에서는 그 중 body와 status code를 전해줬다.
(restApiException 이라는 dto의 역할을 하는 녀석을 만들어 주고, 거기에 메세지를 담아준다.)
그런데 일일히 저렇게 예외를 처리하고 다니는 것은 개발자로써 참을 수 없는 상황이기에... 이걸 해결해 줄 애너테이션이 있다.
모든 컨트롤러에서 발생한 예외를 처리하기 위한 클래스 레벨 애너테이션이다. @ExceptionHandler와 함께 이렇게 사용된다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({IllegalArgumentException.class})
public ResponseEntity<RestApiException> illegalArgumentExceptionHandler(IllegalArgumentException ex) {
...
}
}
( RestContollerAdvice == ControllerAdvice + Responsebody )
애너테이션을 타고 들어가서 보면 interface 애너테이션으로 선언되어있다. 또한 @Target(ElementType.TYPE)라고 되어있다.
(@Target: 해당 어노테이션을 어디에 적용할 수 있는지를 지정)
(ElementType.TYPE: 클래스, 인터페이스, Enum 등의 타입에 적용)
이렇게 설정해줘야 한글이 깨지지 않는다.
아래와 같이 작성하여 properties에서 받아온 메세지를 보내줄 수 있다.
private final MessageSource messageSource;
@Transactional
public ProductResponseDto updateProduct(Long id, ProductMypriceRequestDto requestDto) {
int myprice = requestDto.getMyprice();
if (myprice < MIN_MY_PRICE) {
throw new IllegalArgumentException(
messageSource.getMessage(
"below.min.my.price", //messages.properties의 key값
new Integer[]{MIN_MY_PRICE}, // 메시지 내에서 매개변수를 사용 할 때 전달하는 값(messages.properties 를 보면 배열로 되어있음)
"Wrong Price", // 디폴트 메세지
Locale.getDefault() //언어설정을 가져오는 것임. -> "국제화 할 때 사용 할 수 있겠구나" 라고 생각하면 됨.(어느 지역에서 요청이 왔는지에 따라 다른 언어로 가능)
)
);
}
getMessage에 들어가는 값
Exception 클래스를 만들고 extends 로 상속 받는다.
(이건 예시를 위해 만든 exception으로, 특별한 기능은 없다.)
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message); //부모쪽으로 메세지 전달
}
}
둘이 합쳐지면 이렇게 된다!
Product product = productRepository.findById(id).orElseThrow(() ->
new ProductNotFoundException(messageSource.getMessage(
"not.found.product",
null, //전해주는 값 없음
"Not Found Product",
Locale.getDefault()
))
내가 보내준 상태코드와, 자바가 만들어서 보내는 상태코드의 차이는 무엇일까? 라고 생각하며 restApiException과 HTTP status code의 상태코드를 서로 다르게 보내봤다.
@ExceptionHandler({ProductNotFoundException.class})
public ResponseEntity<RestApiException> notFoundProductExceptionHandler(ProductNotFoundException ex) {
RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(
// HTTP body
restApiException,
// HTTP status code
HttpStatus.OK
);
}
그랬더니 말 그대로 HTTP 상태코드는 OK를, Json 데이터는 404 error를 출력했다.
이게 어떤 식으로 출력되는지 확인해보고 싶었다.
그래서 일부러 messages.properties의 key값을 다르게 바꾼다음 요청을 보내봤더니 messages.properties에 있는 메세지가 아니라 디폴트 메세지가 들어가는 것이다!
그동안 실패 케이스들에 대해 제대로된 응답을 보내주지 못하는 것에 대해 불편했는데,
이번에 예외 처리에 대해 공부했으니 이쁘게 보내줄 수 있게 되어 매우 기쁘다.