본문 바로가기

Spring

일정 관리 서버 Develop : 예외처리 및 검증, 비밀번호 암호화 구현

예외처리 및 검증

에러 응답 객체

@Getter
public class CommonErrorResponse {

    private String status;
    private String message;
    private int code;


    public CommonErrorResponse(HttpStatus status, String message){
        this.status = status.name();
        this.code = status.value();
        this.message = message;
    }

}

 

예외 처리 클래스

  • RuntimeException을 상속한 ApplicationException을 생성
@Getter
public class ApplicationException extends RuntimeException {
    private final HttpStatus status;

    public ApplicationException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}
  • 나머지 커스텀 예외 처리 클래스는 모두 ApplicationException를 상속
public class InvalidPasswordException extends ApplicationException{
    public InvalidPasswordException() {
        super("비밀번호가 일치하지 않습니다.", HttpStatus.UNAUTHORIZED);
    }
}

 

예외처리 클래스들

 

ExceptionHandler

총 3가지 경우의 ExceptionHandler를 만듦

 

런타임 에러 발생 핸들러 (ApplicationException.class)

  • RuntimeException을 상속한 ApplicationException을 상속한 예외가 발생했을 때 처리해주는 핸들러
  • CommentNotFoundException(존재하지 않는 댓글), InvalidPasswordException(비밀번호가 틀린 경우) 등등의 예외처리
@ExceptionHandler(ApplicationException.class)
    public ResponseEntity<CommonErrorResponse> handleApplicationException(ApplicationException e){
        CommonErrorResponse errorResponse = new CommonErrorResponse(e.getStatus(), e.getMessage());
        log.error("[ApplicationExceptionHandler] ApplicationException = {}, class = {}", e.getMessage(), e.getClass());
        return new ResponseEntity<>(errorResponse, e.getStatus());
    }

 

Dto 검증이 잘못된 경우에 발생하는 에러를 관리하는 핸들러 (handleValidationException)

  • 요청값이 객체로 반환된 후에 @Valid를 통한 검증에 걸리는 경우의 에러를 처리하는 핸들러
  • @NotBlank, @Email 등을 안 지킨 경우
@ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<CommonErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
        String firstErrorMessage = null;
        for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
            firstErrorMessage = fieldError.getDefaultMessage();
            break;
        }

 

바인딩 실패한 경우 발생하는 에러를 관리하는 핸들러 (handleHttpMessageNotReadableException)

  • @RequestBody는 HTTP Body Data를 Object로 변환하므로 변환이 실패된다면 Controller를 호출하지 않음
  • 객체 바인딩이 실패한 경우 (Long타입에 String을 넣어서 전송한 경우)
@ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<CommonErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException e){
        CommonErrorResponse errorResponse = new CommonErrorResponse(HttpStatus.BAD_REQUEST, "객체 바인딩이 실패하였습니다. 타입을 다시 확인해주세요.");
        log.error("[HttpMessageNotReadableExceptionHandler] HttpMessageNotReadableException = {}, class = {}", e.getMessage(), e.getClass());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

 

구현하면서 알게 된 점

아래 두개 때문에 시간을 좀 잡아먹었다 🙄

  • @NotBlank : String에만 사용해야 함, Long타입에는 @NotNull을 사용해야 함!
  • @Size : 문자열 길이에 사용하는 것, @Range는 숫자에 사용하는 것!

 

비밀번호 암호화

  • BCrypt 라이브러리 설치 후 스프링 빈에 등록
  • BCrypt.MIN_COST : 암호화를 할 때 사용하는 비용, 적을수록 성능이 높아지지만 암호화의 수준은 낮아짐
@Component
public class PasswordEncoder {

    public String encode(String rowPassword){
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rowPassword.toCharArray());
    }

    public boolean matches(String rawPassword, String encodedPassword) {
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified;
    }
}

 

  • encode : 암호화를 수행하는 함수
  • 암호화 수행 후에 데이터베이스에 비밀번호 저장
@Transactional
    public SignupResponseDto signUp(SignupRequestDto requestDto) {
        String encodedPassword = passwordEncoder.encode(requestDto.getPassword());

        Member member = Member.builder()
                        .name(requestDto.getName())
                        .email(requestDto.getEmail())
                        .password(encodedPassword)
                        .build();

        Member savedMember = memberRepository.save(member);

        return SignupResponseDto.buildDto(savedMember);
    }

 

  • matches : 암호화하기 전 값과 암호화한 후의 값을 비교하는 함수
  • 사용자가 입력한 비밀번호와 데이터베이스에 암호화된 비밀번호를 비교
if (passwordEncoder.matches(rawPassword, member.getPassword())){
            return new LoginResponseDto(member.getId());
        }