티스토리 뷰

스프링 부트 REST API WEB 프로젝트

깃헙 링크

https://github.com/choiwoonsik/springboot_RestApi_App_Project/tree/main/restApiSpringBootApp

수행 목록

  1. 환경구성 및 helloworld 출력
  2. H2 DB 연동
  3. Swagger API 문서 연동
  4. REST API 설계
  5. RestControllerAdvice를 이용한 통합 예외 처리
  6. Entity - DTO 분리
  7. MessageSource를 이용해 예외 메시지 다국화
  8. JPA Aduting을 이용해 객체 생성시간/수정시간 적용
  9. 스프링 시큐리티 + Jwt를 이용해서 인증 및 권한 체크
  10. 스프링 시큐리티 AuthenticationEntryPoint, AccessDenied로 인증 및 인가 예외처리
  11. Jwt AccessToken + RefreshToken으로 보안성과 사용자 편의성 고도화하기
  12. JUnit Test (단위 테스트)
  13. JUnit Test (통합 테스트)
  14. OAuth 2.0 정리
  15. OAuth 2.0 카카오 로그인 part.1 Authorization code + Token 발급
  16. OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
  17. OAuth 2.0 카카오 로그인 테스트 검증
  18. 환경별 설정을 위해서profile 분리하기 

api 처리중 exceptoin이 발생한 경우 공통으로 처리하기 위한 방법을 정리한다.

 

스프링은 이와같은 처리를 위해 @ContrllerAdvice를 제공하고있다. 이걸 이용하면 Contrller에서 발생하는 Exception을 한군데서 처리가 가능하다.


스프링에서의 예외처리

프로그래밍에서 예외처리의 중요성은 모두가 아는 사실일 것이다. 예외처리를 꼼꼼하게 해주면 해줄수록 클라이언트나 서버나 더 안정적으로 서비스를 제공할 수 있을것이다. 하지만 그만큼 어렵고 놓치기 쉬운 부분도 많다.

 

일반적으로 예외처리를 하는 방법을 보면

  • 예외가 날 수 있을만한 구간을 try ~ catch문을 사용해서 감싸준다.
  • 요구조건을 통한 예외처리 (0 ~ 255 사이의 값만 valid하고 그 외는 예외처리)
  • 스프링 시큐리티를 통해 인터셉터로 잡아서 UnauthoizedException과 같은 예외처리
  • 등...

이런 방법들을 코드에 모두 적용하면 너무 복잡해지고 유지보수도 어려워진다. 정작 비지니스 로직 구현보다 예외처리 구현에 더 머리를 쓰고 고민해야 되는일이 생길 수 있다.

이러한 문제를 해결하기 위해 사용하는 것이 @ExceptionHandler@ContrllerAdvice인 것이다.

 

@ExceptionHandler

기능

  • @ExceptionHandler는 @Contrller, @RestController가 적용된 Bean내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능을 한다.

처리방법

@RestController
public class MyController {
    /*
    로직
     */
    @ExceptionHandler(NullPointerException.class)
    public Object nullEx(Exception e) {
        System.out.println(e.getClass());
        return "myServiceException";
    }
}

위와 같이 MyController내에 @ExceptionHandler를 선언하면 해당 컨트롤러 내에서 발생하는 매칭되는 예외는 모두 잡아서 해당 메소드가 처리해준다.

따라서 위 코드에서는 (널포인터) 예외가 발생하면 nullEx 메소드가 호출되어 처리할 것이다.

 

참고 및 주의사항

  • @ExceptionHandler(Exception.calss1, Exception.calss2 ..) 식으로 2개 이상 등록해서 사용할 수 있다.
  • @Controller, @RestController에만 사용가능하다. (@Service 등 다른 곳에서는 사용 X)
  • 리턴 타입은 자유롭게 설정 가능하다. Controller 내 선언된 메서드들은 다양한 타입의 반환값을 가지는데 그 타입과 관계없이 설정할 수 있다.
  • 현재 @Controller가 등록된 클래스 내에 있는 @ExceptionHandler만이 해당 클래스의 예외를 처리할 수 있다.
    • 즉, 예외처리를 하고싶은 클래스마다 따로 넣어줘야 한다.
  • 예외처리를 하나로 다 처리하고싶으면 상위 예외타입인 Exception.class를 사용하면 된다.

예시

@RestController
public class MyController {

    @GetMapping("/nullEx")
    @ResponseBody
    public String myController() {
        throw new NullPointerException();
    }

    @GetMapping("/indexEx")
    @ResponseBody
    public String myController2() {
        throw new IndexOutOfBoundsException();
    }

    @ExceptionHandler(NullPointerException.class)
    public Object nullEx(Exception e) {
        System.out.println(e.getClass());
        return "myServiceException";
    }
}

indexEx (IndexOutOfBoundsException)의 경우 예외가 그대로 발생한 반면, nullEx (NullPointerException)은 @ExceptionHandler에 의해 처리되는 것을 확인할 수 있다.


@ControllerAdvice

기능

  • @ExceptionHandler는 컨트롤러 하나의 클래스를 담당하는 반면, @ControllerAdvice는 모든 @Controller에 대한 예외를 잡아서 처리해주는 기능을 한다.

처리방법

@RestControllerAdvice
public class MyAdvice {
    @ExceptionHandler(Exception.class)
    public String custom() {
        return "hello Exception";
    }
}

위와 같은 코드를 새로운 클래스로 만들고 annotation을 달아주면 모든 Controller에서 발생하는 예외를 처리하게 된다. 그리고 원하는 예외별로 @ExceptionHandler를 이용해서 메서드를 연결시켜주면 된다.

 

참고사항

  • ControlloerAdvice애노테이션은 @RestControllerAdvice와 @ControllerAdvice 두 종류가 있는데 @RestControllerAdvice 내부에 @ControllerAdvice와 @ResponseBody가 등록되어 있다

    즉, API 서버로써 에러응답으로 객체를 리턴해야된다면 Json 형식으로 반환하기 위해서 @RestControllerAdvie를 사용해야 하고, viewResover를 통해서 바로 예외처리 페이지를 보여준다 하면 @ControllerAdvice만 사용해도 되는 것이다.
  • 패키지별 처리
@RestControllerAdvice("com/restApi/restApiSpringBootApp/controller/v1)

과 같이 패키지를 적어주면 전역으로 에러를 처리하면서 패키지별로 에러를 담당하게 할 수도 있다.


결론

@ControllerAdvice를 이용해서 통합으로 에러 컨트롤을 하기 위해서 주의해야할 점이 있다. 무엇보다 에러 메시지가 잘 정의되어 있어야 한다는 점이다.

만약, 로그인 모듈에서 발생한 에러메시지 반환 모델과 주문 모듈에서 발생한 에러메시지 반환모델이 다르다면 통합으로 관리할 수가 없을 것이다.

 

따라서 하나의 에러메시지를 관리하는 클래스를 만들고 동일한 에러 포맷을 갖도록 하는 것이 중요하다

 

통합으로 에러 관리

@Getter
@AllArgsConstructor
public enum ErrorCode {
    OperationNotAuthorized(6000, "Operation not authorized"),
    DuplicatedIdFound(6001, "Duplicate Id"),
    DuplicatedEmailFound(6002, "Duplicate Email"),
    //...
    UnrecognizedRole(6010, "Unrecognized Role");
    
    private int code;
    private String description;
}

위와 같이 한곳에 ErrorCode 클래스를 정리해놓고 @ControllerAdvice, @ExceptionHandler를 사용해서 에러를 처리하면 한곳에서 에러 케이스별로 처리가 가능하다.

 

또한, 코드를 작성할 때도 조건문을 통해 잘못된 케이스는 케이스에 맞춰서 throw new XXXException();을 호출해 버리면 되므로 유지보수 관점에서도 매우 좋다.


이제 정리한 내용을 바탕으로 실제 프로젝트에 적용해보자.

advice 패키지에 ExceptionAdvice 클래스를 생성한다.
@RequiredArgsConstructor
@RestControllerAdvice
public class ExceptionAdvice {

    private final ResponseService responseService;

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult defaultException(HttpServletRequest request, Exception e) {
        return responseService.getFailResult();
    }
}

위에서 설명한 부분은 제외하고 설명하면

 

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

  • Http Response Code를 500으로 설정해준다.

responseService.getFailResult();

  • 이전에 공통 응답 포맷으로 만들었던 CommonResult를 "실패 시 응답 데이터"를 반환하도록 한다.

 

UserController 또한 에러를호출하도록 수정

회원 조회시 해당 Id에 맞는 회원이 없으면 null이 아니라 예외가 터지도록 수정
    @ApiOperation(value = "회원 단건 검색", notes = "userId로 회원을 조회합니다.")
    @GetMapping("/user/{userId}")
    public SingleResult<User> findUserByKey
            (@ApiParam(value = "회원 ID", required = true) @PathVariable Long userId) throws Exception {
        return responseService
                .getSingleResult(userJpaRepo.findById(userId).orElseThrow(Exception::new));
    }
에러가 나면 CommonResult포맷의 FAIL 데이터로 응답이 오는 모습

 

예외처리 고도화 : CustomException 구현

모든 예외를 Exception으로 처리하는 것은 유지보수 측면에서 좋지 않을 것이므로, 예외 상황에 맞는 예외를 내도록 CustomException을 만들어보자.


1. advice/exception 패키지내에 User를 찾지 못했을 때 발생하는 CustomException을 만든다.

public class UserNotFoundCException extends RuntimeException {

    public UserNotFoundCException() {
        super();
    }
}

2. @RestControllerAdvice가 등록되어있는 ExceptionAdvice에 해당 에러를 처리하기 위한 Handler 만들어준다.

    /***
     * 유저를 찾지 못했을 때 발생시키는 예외
     */
    @ExceptionHandler(UserNotFoundCException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult userNotFoundException(HttpServletRequest request, UserNotFoundCException e) {
        return responseService.getFailResult();
    }

3. UserController에서 회원 조회를 담당하는 메서드에 적용시킨다.

    @ApiOperation(value = "회원 단건 검색", notes = "userId로 회원을 조회합니다.")
    @GetMapping("/user/id/{userId}")
    public SingleResult<User> findUserById
            (@ApiParam(value = "회원 ID", required = true) @PathVariable Long userId) {
        return responseService
                .getSingleResult(userJpaRepo.findById(userId).orElseThrow(UserNotFoundCException::new));
    }

    @ApiOperation(value = "회원 단건 검색 (이메일)", notes = "이메일로 회원을 조회합니다.")
    @GetMapping("/user/email/{email}")
    public SingleResult<User> findUserByEmail
            (@ApiParam(value = "회원 이메일", required = true) @PathVariable String email) {
        User user = userJpaRepo.findByEmail(email);
        if (user == null) throw new UserNotFoundCException();
        else return responseService
                .getSingleResult(user);
    }

 

없는 회원 조회시 정상적으로 에러 메시지가 출력되는 모습이다.


Reference : https://jeong-pro.tistory.com/195

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday