종우의 삶 (전체 공개)

Market Project -5 // 예외 처리 본문

개발/Spring

Market Project -5 // 예외 처리

jonggae 2024. 2. 27. 14:37

마켓 프로젝트에서 이제 남은 것은 장바구니와 주문 관련된 내용이다.

 

진행하기에 앞서 필요한 코드 리팩토링과 수정사항을 처리하였다.

 

지난 포스트에 적었던 JWT 검증이 두번 일어나는 것과 관련하여 무엇이 문제인지 살펴보았는데

 

아무래도 다양한 곳에서 예외와 에러들을 처리하는데 중복이 되거나 산만한 코드가 있는 듯 했다.

 

정리를 진행하고, 적당한 메시지들을 넣으니 중복처리되는 부분도 사라지고 클라이언트에게도 깔끔한 오류메시지를 전달할 수 있게 되었다.

 

우선 지난번에 만든 Product 관련 컨트롤러에서 이러한 부분이 있다.

 

 // 단일 상품 조회
 @GetMapping("/{id}")
    public ResponseEntity<?> getProduct(@PathVariable Long id) {
        try {
            ProductDto product = productService.showProductInfo(id);
            return ResponseEntity.ok(product);
        } catch (NotFoundProductException e) {
            return ResponseEntity.ok(new ApiResponseDto("상품이 존재하지 않습니다."));
        }
    }

 

지금은 수정된 내용이지만, 클라이언트가 등록되지 않은 상품의 id를 조회하고자 하면 보통

ResponseEntity로 NotFound, 즉 HTTP 404 에러를 보여준다고 한다.

 

하지만 생각해보니 이것이 404에러보다는 그저 존재하지 않는 상품이다 라는 내용을 알려주는것이 더 좋은 방향일 것 같아 접근이 실패하는 것이아니라 200 으로 OK를 코드를 보내주고, 메시지로 상품이 존재하지 않는다는 것을 출력하였다.

 

여기서 다양한 예외 처리와 마주하게 되었는데,

참고자료를 보며 만들었을 때에는 몰랐던 (크게 신경쓰지 않았던) 것들을 이제 이해할 수 있게 되었다.

아래는 중구난방으로 퍼져있던 클래스들을 다시 정리한 모습이다.

 

 

JWT검증, Login 요청, 토큰의 유효성 검증 등 예외가 발생할 곳은 무척이나 많았다.

 

현재까지 발생할 수 있는 예외는 SpringSecurity와 관련된 인증, 권한 부분과 다른 도메인에서 발생하는 접근들이 있었다.


 

1. 회원 가입 시 입력한 username, password, email 값이 이미 DB에 존재할 때 (중복 데이터)

→ DuplicateMemberException ; 409 CONFLICT 오류 메시지 전송

아래 메시지 값이ResponseEntityExceptionHandler 에서 처리됨

  private void checkUserInfo(String customerName, String email, String phoneNumber) {
        if (customerRepository.findByCustomerName(customerName).isPresent()) {
            throw new DuplicateMemberException("이미 사용중인 ID 입니다");
        }
        // 이메일 중복 체크
        if (customerRepository.findByEmail(email).isPresent()) {
            throw new DuplicateMemberException("이미 사용중인 이메일 주소입니다");
        }

        // 전화번호 중복 체크
        if (customerRepository.findByPhoneNumber(phoneNumber).isPresent()) {
            throw new DuplicateMemberException("이미 사용중인 전화번호입니다");
        }
    }
@ControllerAdvice
public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {

    //회원 가입 시 중복 회원 정보가 있을 때
    @ResponseStatus(CONFLICT)
    @ExceptionHandler(value = {DuplicateMemberException.class})
    @ResponseBody
    protected ErrorDto conflict(RuntimeException ex, WebRequest request) {
        return new ErrorDto(CONFLICT.value(), ex.getMessage());
    }

이미 존재하는 데이터 값으로 회원가입 시도

2. 로그인을 시도할 때 올바르지 않은 값을 입력 하였을 때 (Security)

AuthenticationException ; 401 UNAUTHORIZED

AuthenticationFailureHandler 에 의해 관리된다.

//ID , Password 를 입력하는 로그인 시도가 실패하였을 때 -> id, 비밀번호가 db와 일치하지 않을 때
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        String jsonPayload = "{\"message\": \"" + exception.getMessage() + "\", \"error\": \"Id, 비밀번호를 확인해 주세요\"}";
        response.getWriter().write(jsonPayload);
    }
}

 

잘못된 비밀번호로 로그인을 한다.

 

3. 로그인 후 발급된 토큰이 유효하지 않을 때 (Security)

(잘못된 JWT 서명, 만료된 JWT 토큰, 지원되지 않는 JWT 토큰, 기타 잘못된 JWT토큰) 

→ AuthenticationException ; 401 UNAUTHORIZED

AuthenticationEntryPoint 를 통해 관리된다.

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    /*
    인증되지 않은 접근 401 (JWT 토큰이 유효하지 않을 때)
    * 서명, 잘못된 토큰 등등 */

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        ErrorDto errorDTO = new ErrorDto(HttpStatus.UNAUTHORIZED.value(), "잘못된 인증 정보입니다.");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(new ObjectMapper().writeValueAsString(errorDTO));
    }
}

토큰 값을 변조함

 

4-1. 권한이 없는 계정으로 접근하였을 때 (Security) 

(eg. 일반 유저로 ADMIN 권한이 필요한 접근을 시도할 때)

AccessDeniedException ; 403 FORBIDDEN

AccessDeniedHandler 를 통해 관리된다.

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    // JWT 인증 값에 [권한]이 없는 접근을 할 때 403
    // eg) admin 권한을 customer 가 접근 하였을 때 Security 레벨

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ErrorDto errorDTO = new ErrorDto(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다.", "해당 작업은 관리자만 가능합니다.");
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(new ObjectMapper().writeValueAsString(errorDTO));
    }
}

 

일반 customer로 데이터 수정을 진행하는 것은 불가능하다. (4-1 예외 발생)

4-2. 권한이 없는 계정으로 특정 API를 접근하였을 때 (Security가 아닌 MVC 레벨)

(eg2. 상품 등록, 수정, 제거를 일반 회원이 시도할 때 )

 AccessDeniedException ; 403 FORBIDDEN

@ControllerAdvice 로 표현되는 GlobalExceptionHandler, ResponseEntityExceoptionHandler 에서 처리

보통 이러한 경우는 이미 4-1에서 처리가 되지만, 그렇지 않은 경우도 있다. 

@ControllerAdvice
public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {
   
    // 권한이 없는 계정으로 접근하였을 때 mvc 레벨
    @ResponseStatus(FORBIDDEN)
    @ExceptionHandler(value = {NotFoundMemberException.class, AccessDeniedException.class})
    @ResponseBody
    protected ErrorDto forbidden(RuntimeException ex, WebRequest request) {
        String additionalMsg = "접근 권한이 없습니다. 계정을 확인해주세요.";
        return new ErrorDto(FORBIDDEN.value(), ex.getMessage(), additionalMsg);
    }
}
이 4-1, 4-2는 정확한 구분이 필요함.
4-1의 Security 레벨에서의 상황과  4-2의 MVC 레벨에서의 상황을 잘 구분하자.. 

MVC에서 직접 요청을 처리하기 에 Security 에서 발생하는 예외와 
API접근 등 Security 필터 체인 이후 이후에 발생하는 예외의 차이

Security : 인증과 전역적인 권한 부여 검사. 예를들어 JWT가 유효한지, 사용자가 '인증'되었는지 검사
MVC : 인증된 사용자가 특정 동작을 수행할 수 있는지의 여부를 보다 세밀하게 제어. 애플리케이션의 비즈니스 로직과 데이터 처리에 더 관련됨

이후 생기는 예외들도 로직의 작동 과정을 잘 살피고 처리하도록 한다. 위 권한 관련된 것은 주로 Security 에서 먼저 처리되는 경우가 많다.

 

5. 존재하지 않는 상품을 조회하였을 때

ProductNotFoundException ; 200 OK , "상품이 존재하지 않음" 오류 메시지 전송

ProductController에서 직접 예외 던짐

@GetMapping("/{id}")
    public ResponseEntity<?> getProduct(@PathVariable Long id) {
        try {
            ProductDto product = productService.showProductInfo(id);
            return ResponseEntity.ok(product);
        } catch (NotFoundProductException e) {
            return ResponseEntity.ok(new ApiResponseDto("상품이 존재하지 않습니다."));
        }
    }

존재하지 않는 상품 id 접근

이것도 404, 200 과 관련하여 많은 이야기들이 있음. 


 

이런저런 예외들이 앞으로 더 생겨날 것이다.

4번의 경우처럼 헷갈리지 않게 잘 관리해보자.

 

Comments