Spring
일정 관리 서버 Develop : 예외처리 및 검증, 비밀번호 암호화 구현
김예나
2025. 2. 13. 11:15
예외처리 및 검증
에러 응답 객체
@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());
}