에러처리 - 9min9/GiftHub GitHub Wiki

사용자 정의 Exception

  • 에러 메세지의 형식을 맞추기 위해 사용자 정의 Exception을 작성
  • 사용자 정의 Exception은 전체적으로 BaseException을 상속
  • field, code, message 값을 가지고 에러를 처리
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class BaseException extends RuntimeException {
    private String field;
    private String code;
    private String message;

}
public class RequiredFieldException extends BaseException {

    public RequiredFieldException(String field, String message) {
        super(field, "Required", message);
    }
}

public class NotEnoughPointException extends BaseException {

    public NotEnoughPointException() {
        super.setField("point");
        super.setCode("NotEnough");
        super.setMessage("포인트가 부족합니다. 포인트를 충전하고 다시 시도해주세요.");
    }
}

public class NotValidFileExtensionException extends BaseException {

    public NotValidFileExtensionException() {
        super.setCode("NotValid");
        super.setField("fileExtension");
        super.setMessage("지원하지 않는 파일확장자 입니다");
    }
}

...

error.properties, MessageSource

  • error.properties를 MessageBean으로 등록하여 에러 메세지를 관리
  • errors의 field와 code를 통해 메세지를 등록
    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:/errors");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
#회원가입
NotBlank.email = 이메일을 입력해주세요
Duplicate.email = 중복된 이메일 입니다
...

#기프티콘 및 관리자
NotBlank.productName = 상품 이름을 확인해주세요
NotBlank.brandName = 브랜드 이름을 확인해주세요
NotBlank.due = 유효기간을 확인해주세요
NotNull.price 가격을 확인해주세요
NotSelect.category = 카테고리를 선택해주세요
...

#포인트 구매 및 결제
NotEnough.point = 포인트가 부족합니다 포인트를 충전하고 다시 시도해주세요
Empty.totalAmount = 총 가격이 0입니다 상품 추가 후 다시 시도해주세요
Mismatch.paidId = 결제 시도 아이디와 로그인 아이디가 다릅니다 재로그인 후 다시 시도해주세요
NotFound.gifticon = 없는 기프티콘입니다 존재하는 기프티콘을 구매해주세요
...

NotFound.user = 존재하지 않는 유저입니다
...

errorResponse, ExceptionResponse

  • Client에서 일관된 처리를 위해 Exception 메세지 또한 Errors와 동일한 형식으로 처리
  • 항상 field, code, message 형식으로 Exception과 Errors를 Client에 전달하여 오류를 처리
    @Bean
    public ErrorResponse errorResponse(MessageSource messageSource) {
        return ErrorResponse.setMessageSource(messageSource);
    }

    @Bean
    public ExceptionResponse exceptionResponse() {
        ExceptionResponse exceptionResponse = new ExceptionResponse();
        return exceptionResponse;
    }
public class ExceptionResponse {
    private Map<String, String> response = new HashMap<>();

    public Map<String, String> getException(String field, String code, String message) {
        this.response.put("field", field);

        if (field == null || field.isEmpty()) {
            this.response.put("code", code);
        } else {
            this.response.put("code", code + "." +field);
        }
        this.response.put("message", message);

        return response;
    }
}
     public ResponseEntity<Object> registerGifticon(@Valid @RequestBody GifticonRegisterRequest request,
                                                   BindingResult bindingResult, @RequestHeader HttpHeaders headers) {
        try {
            ...
            if (userId == null) {
                throw new NotLoginedException();
            }
            ...

        } catch (NotLoginedException e) {
            return ResponseEntity.badRequest().body(exceptionResponse.getException(null, e.getCode(), e.getMessage()));

        } catch (NotFoundStorageException e) {
            return ResponseEntity.badRequest().body(exceptionResponse.getException(e.getField(), e.getCode(), e.getMessage()));
        } 

        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest().body(errorResponse.getErrors(bindingResult));

        } else {
          ...
        }
    }

Validator 사용

  • 사용자 입력 값을 조건에 따라 검증하기 위해 Validator를 구현하여 사용
@Component
public class GifticonAppovalValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return GifticonAppovalRequest.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        GifticonAppovalRequest request = (GifticonAppovalRequest) target;

        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "storageId", "NotBlank");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "productName", "NotBlank");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "brandName", "NotBlank");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "due", "NotBlank");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "barcode", "NotBlank");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "price", "NotNull");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "category", "NotSelect");
    }
}
    private final GifticonAppovalValidator gifticonConfirmRegister;

    @PostMapping("/gifticon/confirm/register")
    public ResponseEntity<Object> registerGifticon(@RequestBody GifticonAppovalRequest request, BindingResult bindingResult) {
        ...
        Boolean isConfirm = request.getIsConfirm();   //관리자 검수 Form의 승인, 거절 선택 여부

        try {
            if (isConfirm == null) {
                throw new NotSelectConfirmFlagException();
            }

            GifticonStorageDto storageDto = gifticonStorageService.getStorageById(storageId);

            if (isConfirm) {  //관리자 검수가 승인일 때
                gifticonConfirmRegister.validate(request, bindingResult);  //사용자 입력 값을 검증
                
                //기프티콘을 등록
                ...
            }

            if (!isConfirm) {  //관리자 검수가 거절일 때
                //기프티콘 거절 사유를 반환
                String rejectReason = request.getRejectReason();
                storageDto.setApprovalFailReason(RegistrationFailureReason.valueOf(rejectReason));
                gifticonStorageService.adminToStorage(storageDto);
            }

        } catch (NotSelectConfirmFlagException e) {
            bindingResult.rejectValue("isConfirm", e.getCode(), e.getMessage());
        } catch (NotFoundStorageException e) {
            return ResponseEntity.badRequest().body(exceptionResponse.getException(e.getField(), e.getCode(), e.getMessage()));
        } 

        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest().body(errorResponse.getErrors(bindingResult));
        }

        return ResponseEntity.ok(Collections.singletonMap("status", "200"));
    }

⚠️ **GitHub.com Fallback** ⚠️