자바에서 특정한 예외 상황을 더 명확하게 표현하고자 할 때는 커스텀 예외 클래스를 정의할 수 있습니다.
커스텀 예외 클래스를 사용하면 예외가 발생한 원인이나 상황에 대한 자세한 정보를 담을 수 있습니다.
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
컨트롤러에서는 예외를 구체화해서, 프론트엔드에게 던져줍니다
-
ex.body() : ApiResult(에러메시지, HttpStatus)
-
ex.status() : 상태 코드
-
- 프론트에서 상태 처리를 용이하게 하기 위함
@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 |