전체 소스코드
https://github.com/GHGHGHKO/Springboot/tree/main/pepega_chapter_6
이전 포스팅에서는
본격적으로 API 서버 개발을 하기 전
API 인터페이스 및 결과 데이터 구조를 살펴보고
확장 가능한 형태로 설계해보았다.
API 성공에 대한 내용만 포스팅했지만
이번 포스팅에는 실패 시 ExceptionHandling과
결과 Message 처리에 대한 내용을 살펴보도록 하겠다.
API 처리 중 특정한 Exception이 발생할 경우
공통으로 처리하는 방법에 대해 알아보도록 하겠다.
Spring에서는 이와 같은 처리를 위해 ControllerAdvice annotation을 제공하며
이 annotation을 이용하면 Controller에서 발생하는
Exception을 한 군데에서 처리할 수 있다.
@ControllerAdvice의 사용
ControllerAdvice는 Spring에서 제공하는 annotation으로
Controller 전역에 적용되는 코드를 작성 할 수 있게 해준다.
설정 시 특정 패키지를 명시하면 적용되는 Controller의 범위도 제한 할 수 있다.
이러한 특성을 이용하면
@ControllerAdvice와 @ExceptionHandler를 조합하여
예외 처리를 공통 코드로 분리하여 작성 할 수 있다.
com.example.pepega 하위에
advice package를 추가한다.
그리고 ExceptionAdvice 클래스를 생성한다.
package com.example.pepega.advice;
import com.example.pepega.model.response.CommonResult;
import com.example.pepega.service.ResponseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@RequiredArgsConstructor
@RestControllerAdvice // 예외 발생 시 json 형태로 결과 반환 | 프로젝트의 모든 Controller에 로직 적용
// @RestControllerAdvice(basePackages = "com.example.pepega") : pepega 하위의 Controller 에만 로직 적용
public class ExceptionAdvice {
private final ResponseService responseService; // 결과에 대한 정보를 도출하는 클래스 명시
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected CommonResult defaultException(HttpServletRequest request, Exception e) {
// CommonResult : 응답 결과에 대한 정보
return responseService.getFailResult();
// getFailResult : setSuccess, setCode, setMsg
}
}
@RestControllerAdvice
ControllerAdvice의 annotation은
@ControllerAdvice, @RestControllerAdvice 두 가지가 있다.
예외 발생 시 json 형태로 결과를 반환하기 위해서
@RestControllerAdvice를 클래스에 선언하면 된다.
annotation에 추가로 패키지를 적용하면
특정 패키지 하위에 Controller에만 로직이 적용되게 할 수 있다.
ex) @RestControllerAdvice(basePackages = "com.example.pepega")
본 포스팅에서는 아무것도 적용하지 않아
프로젝트의 모든 Controller에 로직이 적용된다.
@ExceptionHandler(Exception.class)
Exception이 발생하면 해당 Handler로 처리하겠다고 명시하는 annotation 이다.
괄호 안에는 어떤 Exception이 발생할 때 handler를 적용할 것인지
Exception 클래스를 인자로 넣는다.
예제에서는 Exception.class를 지정하였는데
Exception.class는 최상위 예외처리 객체이므로
다른 ExceptionHandler에서 걸러지지 않은 예외가 있으면
최종으로 이 handler를 거쳐 처리된다.
그래서 메서드 명을 defaultException으로 명명하였다.
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
해당 Exception이 발생하면 Resource에 출력되는 HttpStatus Code가 500으로 내려가도록 설정한다.
성공 시에는 HttpStatus Code가 200으로 내려간다.
본 포스팅에서는 HttpStatus Code의 역할은 성공이냐(200) 아니냐 정도의 의미만 있고
실제 사용하는 성공 실패 여부는 json으로 출력되는 정보를 이용한다.
responseService.getFailResult()
Exception 발생 시 이미 만들어준 CommonResult의 실패 결과를 json 형태로 출력하도록 설정한다.
위에서 세팅한 HttpStatus Code외에 추가로 api 성공 실패 여부를 다시 세팅하는 이유는
상황에 따라 다양한 메시지를 전달하기 위해서 이다.
HttpStatus Code는 이미 고정된 스펙이기 때문에
(예 200 == OK, 404 == Not Found 등등..)
상세한 예외 메시지 전달에 한계가 있다.
예를 들자면 "회원 정보가 없음" 이라는 에러 메시지는
HttpStatus Code 상에 존재하지 않아 표현할 수 없다.
따라서 커스텀된 Exception을 정의하고
해당 Exception이 발생하면 적절한 형태의 오류 메시지를 담은 Json을 결과에 내리도록 처리하는 것이다.
Exception Test를 위한 Controller 수정
UserController의 findUserById를 수정한다.
기존에는 회원 조회 시 데이터가 없는 경우 null을 반환하였지만
이제 Exception을 발생시키도록 수정 할 것이다.
@ApiOperation(value = "회원 단건 조회", notes = "msrl로 회원을 조회한다.")
@GetMapping(value = "/user/{msrl}")
public SingleResult<User> findUserById(@ApiParam(value = "회원ID", required = true) @PathVariable long msrl) throws Exception {
return responseService.getSingleResult(userJpaRepo.findById(msrl).orElseThrow(Exception::new));
// 결과 데이터가 단일건인 경우 getSingleResult를 이용하여 결과를 출력
}
수정한 내용을 swagger에서 확인한다.
존재하지 않는 회원을 조회할 경우 결과 메시지는 정의된 실패 메시지로 출력되며
Response Code도 설정된 500이 출력됨을 확인할 수 있다.
Exception 고도화 - Custom Exception 정의
위에서 처리한 Exception은 Java에 정의되어있는 Exception이다. (Exception.class)
예회 발생 시 이미 구현되어있는 Exception Class를 사용할 수 있지만
매번 정의된 Exception을 사용하는 것은 여러가지 예외 상황을 구분 하는데 적합하지 않을 수 있다.
그래서 이번에는 Custom Exception을 정의하여 사용해보도록 하겠다.
com.example.pepega.advice 아래에 exception package를 생성한다.
그 아래 CUserNotFound 클래스를 생성한다.
Class 명의 prefix C는 Custom을 의미한다.
Exception 이름은 알아보기 쉽고 의미가 명확하게 전달될 수 있는 한 자유롭게 지으면 된다.
package com.example.pepega.advice.exception;
public class CUserNotFoundException extends RuntimeException {
public CUserNotFoundException(String msg, Throwable t) {
super(msg, t);
}
public CUserNotFoundException(String msg) {
super(msg);
}
public CUserNotFoundException() {
super();
}
}
CUserNotFoundException은 RuntimeException을 상속받아 작성한다.
총 3개의 메서드가 제공되는데
메서드 중 CUserNotFoundException()을 사용할 것이다.
혹시 Controller에서 메시지를 받아 예외처리 시 사용이 필요하면
CUserNotFoundException(String msg)을 사용하면 된다.
위에서 작성한 CUserNotFoundException을 적용하기 위해 ExceptionAdvice를 다시 열고
아래와 같이 작성한다.
이제 Controller에서 CUserNotFoundException이 발생하면
해당 ExceptionHandler에서 받아 처리하게 된다.
기존의 ExceptionHandler는 새로운 ExceptionHandler가 제대로 작동하는지 테스트하기 위해
일단 주석 처리할 것이다.
package com.example.pepega.advice;
import com.example.pepega.advice.exception.CUserNotFoundException;
import com.example.pepega.model.response.CommonResult;
import com.example.pepega.service.ResponseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@RequiredArgsConstructor
@RestControllerAdvice // 예외 발생 시 json 형태로 결과 반환 | 프로젝트의 모든 Controller에 로직 적용
// @RestControllerAdvice(basePackages = "com.example.pepega") : pepega 하위의 Controller 에만 로직 적용
public class ExceptionAdvice {
private final ResponseService responseService; // 결과에 대한 정보를 도출하는 클래스 명시
//@ExceptionHandler(Exception.class)
//@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
//protected CommonResult defaultException(HttpServletRequest request, Exception e) {
// // CommonResult : 응답 결과에 대한 정보
// return responseService.getFailResult();
// // getFailResult : setSuccess, setCode, setMsg
//}
@ExceptionHandler(CUserNotFoundException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected CommonResult userNotFoundException(HttpServletRequest request, CUserNotFoundException e) {
// CommonResult : 응답 결과에 대한 정보
return responseService.getFailResult();
// getFailResult : setSuccess, setCode, setMsg
}
}
이제 UserController를 열고 다음과 같이 orElseThrow 부분의 Exception을
CUserNotFoundException으로 변경한다.
기존의 throws Exception 부분도 더 이상 필요 없으니 삭제한다.
@ApiOperation(value = "회원 단건 조회", notes = "msrl로 회원을 조회한다.")
@GetMapping(value = "/user/{msrl}")
public SingleResult<User> findUserById(@ApiParam(value = "회원ID", required = true) @PathVariable long msrl) {
return responseService.getSingleResult(userJpaRepo.findById(msrl).orElseThrow(CUserNotFoundException::new));
// 결과 데이터가 단일건인 경우 getSingleResult를 이용하여 결과를 출력
}
이제 swagger로 테스트하여 새로운 Exception이 잘 처리되는지 확인한다.
Exception -> CUserNotFoundException으로 변경 시에도 예외 처리가 동일하게 처리되는 것을 확인했다.
다음 포스팅에서는 Exception의 형태마다 다른 에러 메시지가 출력되도록 고도화 할 것이다.
출처
https://daddyprogrammer.org/post/446/spring-boot2-5-exception-handling-controlleradvice/
https://github.com/codej99/SpringRestApi
'SpringBoot' 카테고리의 다른 글
springboot로 Rest api 만들기(8) SpringSecurity를 이용한 인증 및 권한부여 (0) | 2021.10.22 |
---|---|
springboot로 Rest api 만들기(7) MessageSource를 이용한 Exception 처리 (0) | 2021.10.22 |
springboot로 Rest api 만들기(5) API 인터페이스 및 결과 데이터 구조 설계 (0) | 2021.10.22 |
springboot로 Rest api 만들기(4) Swagger API 문서 자동화 (0) | 2021.10.21 |
springboot로 Rest api 만들기(3) H2 Database 연동 (0) | 2021.10.21 |