@RestControllerAdvice을 통해 전역 예외처리를 명시하고 @ExceptionHanlder을 사용해 특정 예외처리를 구현해 보자!
들어가며
기능을 구현하다 보면 예외가 발생할 수 있습니다. 중복된 예외를 처리하게 되면 코드가 지저분하게 될 수 있습니다. 그래서 @RestControllerAdvice 에노테이션을 통해 예외를 사용할 범위를 설정하고 @ExceptionHandler을 통해서 특정 예외를 처리하는 기능을 구현하도록 하겠습니다.
사전 준비
만약 게시판 프로젝트를 하시려는 분은
게시글 오류페이지 처리하기까지 따라 해 만들어주시기 바랍니다!
전체 코드는 깃에 올려두었습니다.
구현내용
1. 공통적으로 발생하는 예외를 던지기만 하면 @RestControllerAdvice에서 예외를 처리하게 한다.
2. 예외를 직접 만들어 중복된 예외를 @ExceptionHandler로 처리해 반환하도록 한다.
글을 삭제할 때 작성자가 아닌 사용자가 삭제를 하면 요청로그와 응답로그 사이에 Login_RestException을 발생시키며 Login과 관련된 예외는 모두 Login_RestException 예외를 처리하며 오류내용은 오류내용별로 다르게 처리하는 것을 구현해 볼 것입니다.
패키지 구조
- Exception패키지 : 새롭게 만들 예외 클래스 패키지
- handler : 에러 코드와 메시지를 담는 ErrorResult.java 패키지
- handler.advice : 전역으로 예외를 처리하고 담당하는 클래스
@RestControllerAdvice, @ControllerAdvice
현재 게시판 프로젝트는 @GetMapping는 스프링 MVC 구조로 하고 있고 , 나머지 Mapping방식은 Rest API 형식으로 개발하고 있다. 하지만 @RestControllerAdvice는 Rest API를 적용하는 것이다. 그래서 @ControllerAdvice를 사용하는 MVCControllerAdvice클래스를 따로 만들었습니다.
ErrorResult
ErrorResult
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
간단하게 에러코드와 에러 메시지를 담는 ErrorResult 클래스이다.
Excetpion 만들기
이제 간단하게 Exception을 만들어보자. 현재 지금까지 만든 것 중에 예외를 생각해 보자
(더 많은 예외가 있을 수 있지만.. 현재 저의 생각에서 나올 수 있는 예외를 생각해 봤습니다.)
1. 로그인 관련 발생할 수 있는 오류
2. 이용 중 해당페이지가 다른 누군가가 삭제했을 때 찾을 수 없다는 오류
3. 수정 시 비밀번호 검증 실패했을 때 발생하는 오류
4. 글을 작성할 때 발생할 수 있는 오류
이 정도 있다고 가정을 하자.
이제 6개의 Exception파일을 만들어준다
( 1.BadRequestException : 잘못된 요청 오류
2.BoardException : 게시글 오류
3.Login_RestException : restApi에서의 로그인 오류
4.LoginException : mvc에서의 로그인 오류
5.NotFindPage_RestException : restAPI에서의 페이지 오류
6.NotFindPageException: mvc에서의 페이지 오류)
BadRequestException
/**
* 잘못된 요청이 오류
*/
public class BadRequestException extends RuntimeException{
public BadRequestException() {
super();
}
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
public BadRequestException(Throwable cause) {
super(cause);
}
protected BadRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
BadRequestException는 RuntimeExcepton을 상속받아서 만든 클래시이며 , 기본적인 메서드를 오버라이드 했습니다. 나머지 5개도 동일한 방법으로 생성하면 됩니다.
Advice 작성
@RestControllerAdvice
RestApiControllerAdvice.class
@Slf4j
@RestControllerAdvice(basePackages = "messageboard.controller")
public class RestApiControllerAdvice {
@ExceptionHandler(NotFindPage_RestException.class)
public ResponseEntity<ErrorResult> notFindPageRest(NotFindPage_RestException e) {
log.error("[404에러]", e);
ErrorResult errorResult = new ErrorResult("Update-EX", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResult);
}
@ExceptionHandler
public ResponseEntity<ErrorResult> badRequest(BadRequestException e){
log.error("[401 에러]",e);
ErrorResult errorResult = new ErrorResult("BadRequest-EX", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResult);
}
@ExceptionHandler
public ResponseEntity<ErrorResult> login_restHandler(Login_RestException e){
log.error("[login 에러] ex",e);
ErrorResult errorResult = new ErrorResult("Login-rest-EX", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResult);
}
}
MVCControllerAdvice.class
@Slf4j
@ControllerAdvice(basePackages = "messageboard.controller")
public class MVCControllerAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResult loginHandler(LoginException e){
log.error("[exception] ex",e);
return new ErrorResult("login-EX", e.getMessage());
}
@ExceptionHandler(NotFindPageException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResult NotFindException(NotFindPageException e){
log.error("[exception] ex",e);
return new ErrorResult("NotFount-EX", e.getMessage());
}
}
basePackages를 통해서 RestApiControllerAdvice, MVCControllerAdivce 클래스가 적용될 패키지 경로를 지정해 준다.
@ExceptionHandlerd 에노테이션을 사용해 파라미터에 작성한 예외가 터지만 해당 메서드가 실행되도록 해주며, 상태를 각각 맞는 상태코드로 보내며, 해당 오류 메시지를 body에 넣어 보낸다
BoardController
이제 return값에 상태코드와 body에 직접 내용을 작성해서 보내줬다. 이렇게 되면 어떤 예외가 발생하는지 알 수 없었다. 하지만 만든 예외를 적용해서 처리하게 된다면 코드의 수정을 간편하게 할 수 있고, 어떤 예외가 발생하는지 알기 쉬워진다.
(모든 컨트롤러에 하지 않고 @GetMapping("/board/{boardId}") , @PostMapping("/password/verify") 두 개에만 적용해 보겠습니다. 나머지는 코드는 깃에 올려두었습니다.)
@GetMapping("/board/{boardId}")
@GetMapping("/board/update/{id}")
public String updateGetBoard(@PathVariable(name = "id") Long id,Model model,HttpSession session){
Board byBoardId = boardService.findByBoardId(id);
String board_write_user = byBoardId.getMember().getUsername();
Member loginMember = getSession(model, session);
if (loginMember.getUsername().equals(board_write_user)) {
model.addAttribute("board",byBoardId);
}else
throw new LoginException("작성자만 수정할수 있습니다.");
model.addAttribute("board",byBoardId);
return "board/updateBoard";
}
MVC 패턴을 사용하는 @Getmapping이므로 LoginException을 던진다.
@PostMapping("/password/verify")
@PostMapping("/password/verify")
@ResponseBody
public ResponseEntity<?> verifyPassword(@RequestBody BoardDto boardDto) {
try {
String password = boardDto.getPassword(); //입력한 비밀번호
String dtoUsername = boardDto.getMemberDto().getUsername(); //사용자 정보
Long boardId = boardDto.getId(); //게시글 번호
Integer integer = boardService.passwordVerify(boardId, password, dtoUsername);
;
if (integer == 2) {
throw new Login_RestException();
} else if (integer == 1) {
return ResponseEntity.ok(integer);
}
throw new BadRequestException();
} catch (Login_RestException e) {
throw new Login_RestException("작성자만 삭제 가능합니다.");
} catch (BadRequestException e) {
throw new BadRequestException("비밀번호가 일치하지 않습니다.");
} catch (NotFindPageException e) {
throw new NotFindPage_RestException("해당 게시물이 더이상 존재하지 않습니다.");
}
}
RestAPI 형태의 @POST매핑이므로 *_restException 예외를 던저주 었습니다. 이렇게 되면 @ExceptionHandler이 처리하며 JSON 형태로 반환해 줍니다.
View
이제 view의 BoardInfo.html 부분을 수정해 보겠습니다. 이전까지는 게시물 수정 시에 게시글의 비밀번호를 검증할 때 오류메시지를 직접 작성해줬지다. 이제는 JSON형태로 넘어온 오류 메시지값이 나오도록 해보겠습니다.
BoardInfo.html
function verifyPassword() {
... 동일
$.ajax({
... 동일
//수정
error : function (xhr){
var errorMap = JSON.parse(xhr.responseText);
console.log(errorMap)
$.each(errorMap, function (key, value){
if (key === "message") {
alert(value);
}
});
}
});
}
JSON형태를 파싱 해서 errorMap으로 초기화한다. 그리고 로그를 찍어서 확인해 보면
다음과 같이 나오게 된다. 이제 $. each로 값을 출력해주기만 하면 된다.
실행
비밀번호 틀릴 시
게시물이 삭제되었을 때
게시글 작성자가 아닐 시
다음으로
이번에는 @ExceptionHandler과 @ControllerAdvice로 예외를 처리하는 글을 작성해 봤습니다. 틀린 점이 있다면 댓글로 달아주시면 감사하겠습니다. 다음에는 댓글을 기능구현해 보겠습니다! 감사합니다.!!