봄수의 연구실

Custom Exception 본문

DEV/Java

Custom Exception

berom 2023. 7. 18. 14:21

자바에서 특정한 예외 상황을 더 명확하게 표현하고자 할 때는 커스텀 예외 클래스를 정의할 수 있습니다.

커스텀 예외 클래스를 사용하면 예외가 발생한 원인이나 상황에 대한 자세한 정보를 담을 수 있습니다.

Exception 400

유효성 검사 실패나 잘못된 요청을 의미합니다

@Getter
public class Exception400 extends RuntimeException {
    public Exception400(String message) {
        super(message);
    }

    public ApiUtils.ApiResult<?> body() {
        return ApiUtils.error(getMessage(), HttpStatus.BAD_REQUEST);
    }

    public HttpStatus status() {
        return HttpStatus.BAD_REQUEST;
    }
}

Controller 측 Code

컨트롤러에서는 예외를 구체화해서, 프론트엔드에게 던져줍니다

@PostMapping("/join")
public ResponseEntity<?> join(
        @RequestBody @Valid UserRequest.JoinDTO joinDTO,
        Errors errors,
        HttpServletRequest request
) {
    if (errors.hasErrors()) {
        List<FieldError> fieldErrors = errors.getFieldErrors();
        Exception400 ex = new Exception400(fieldErrors.get(0).getDefaultMessage() + ":" + fieldErrors.get(0).getField());
        return new ResponseEntity<>(
                ex.body(),
                ex.status()
        );
    }
    // …
}

Exception 401

Spring Security Setting

먼저 실패에 대한 예외처리를 해줍니다

// 8. 인증 실패 처리  
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {  
	log.warn("인증되지 않은 사용자가 자원에 접근하려 합니다 : "+authException.getMessage());  
	FilterResponseUtils.unAuthorized(response, new Exception401("인증되지 않았습니다"));  
});  

8번의 인증 실패 처리를 위해 unAuthorized에서 상태와 에러 메시지를 설정합니다

public static void unAuthorized(HttpServletResponse resp, Exception401 e) throws IOException {  
	resp.setStatus(e.status().value());  
	resp.setContentType("application/json; charset=utf-8");  
	ObjectMapper om = new ObjectMapper();  
	String responseBody = om.writeValueAsString(e.body());  
	resp.getWriter().println(responseBody);  
}

예외 처리 코드

@Getter
public class Exception401 extends RuntimeException {
    public Exception401(String message) {
        super(message);
    }

    public ApiUtils.ApiResult<?> body() {
        return ApiUtils.error(getMessage(), HttpStatus.UNAUTHORIZED);
    }

    public HttpStatus status() {
        return HttpStatus.UNAUTHORIZED;
    }
}

Exception 403

403 예외 처리를 위해 Spring Security에서 사전 설정을 합니다

// 9. 권한 실패 처리  
http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {  
	log.warn("권한이 없는 사용자가 자원에 접근하려 합니다 : "+accessDeniedException.getMessage());  
	FilterResponseUtils.forbidden(response, new Exception403("권한이 없습니다"));  
});
@Getter
public class Exception403 extends RuntimeException {
    public Exception403(String message) {
        super(message);
    }

    public ApiUtils.ApiResult<?> body() {
        return ApiUtils.error(getMessage(), HttpStatus.FORBIDDEN);
    }

    public HttpStatus status() {
        return HttpStatus.FORBIDDEN;
    }
}

Exception 404

404 Notfound 에러는 상품 조회를 했는데, 자원을 찾지 못했을 때의 오류이다
Controller 단에서 에러를 처리한다

// (기능5) 개별 상품 상세 조회
@GetMapping("/products/{id}")
public ResponseEntity<?> findById(@PathVariable int id) {
    // 1. 더미데이터 가져와서 상품 찾기
    Product product = fakeStore.getProductList().stream()
            .filter(p -> p.getId() == id)
            .findFirst()
            .orElse(null);
    if (product == null) {
        Exception404 ex = new Exception404("해당 상품을 찾을 수 없습니다: " + id);
        return new ResponseEntity<>(ex.body(), ex.status());
    }
    // 2. 더미데이터 가져와서 해당 상품에 옵션 찾기
    List<Option> optionList = fakeStore.getOptionList().stream()
            .filter(option -> product.getId() == option.getProduct().getId())
            .collect(Collectors.toList());
    // 3. DTO 변환
    ProductResponse.FindByIdDTO responseDTO = new ProductResponse.FindByIdDTO(product, optionList);
    // 4. 공통 응답 DTO 만들기
    return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
@Getter
public class Exception404 extends RuntimeException {
    public Exception404(String message) {
        super(message);
    }

    public ApiUtils.ApiResult<?> body() {
        return ApiUtils.error(getMessage(), HttpStatus.NOT_FOUND);
    }

    public HttpStatus status() {
        return HttpStatus.NOT_FOUND;
    }
}

Exception 500

@Getter
public class Exception500 extends RuntimeException {
    public Exception500(String message) {
        super(message);
    }

    public ApiUtils.ApiResult<?> body() {
        return ApiUtils.error(getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    public HttpStatus status() {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

상태 코드 500은 서버 문제인데, DB 정보 등이 외부에 유출 될 수 있기 때문에 조심해야 한다.

try-catch 문으로 감싸서 예외처리를 해주자, 그냥 내부적으로 터져버리면 프론트 단에서 처리를 하지 못한다.

try{  
	userRepository.save(user);  
}catch(Exception e){  
		Exception500 ex = new Exception500(e.getMessage()) ;  
		return new ResponseEntity<>(  
			ex.body(),  
			ex.status()  
	);  
}

하지만 위의 코드도 아직 부족하다. 최대한 Write 작업을 하다가 터지지 않도록하자

Optional<User> userOP = userRepository.findByEmail(joinDTO.getEmail());
if (userOP.isEmpty()) {
    try {
        userRepository.save(user);
    } catch (Exception e) {
        ErrorLog errorLog = ErrorLog.builder()
                .message(e.getMessage())
                .userAgent(request.getHeader("User-Agent"))
                .userIp(request.getRemoteAddr())
                .build();
        errorLogJPARepository.save(errorLog);

        Exception500 ex = new Exception500("unknown server error");
        return new ResponseEntity<>(ex.body(), ex.status());
    }
} else {
	 // 서비스 계층 로직임
	 // Exception400 ex = new Exception400("동일한 이메일이 존재합니다: email");
    // return new ResponseEntity<>(ex.body(), ex.status());
}

부족한 점이나 잘못 된 점을 알려주시면 시정하겠습니다 :>

728x90

'DEV > Java' 카테고리의 다른 글

Service의 책임  (0) 2023.07.18
Controller의 책임  (0) 2023.07.18
Body에 상태 코드를 넣는 이유  (0) 2023.07.18
요청 DTO에 정규 표현식 적용하는 방법  (0) 2023.07.18
H2 DB  (0) 2023.07.11