Validation 적용 회고에서 다양하게 Validation을 적용했었는데, 주요한 포인트는, 간단한 것은 BeanValidation으로, 복잡한 것은 Validator 구현으로 하자였다.
그런데, 작업하다보니 복잡한 경우, 컨트롤러나 서비스 계층에서 Exception을 던지고 Exception핸들러로 처리하는 게 낫다는 생각이 들었고, gpt에게 물어봐도 그 방향이 맞다는 답변이 나와서 리팩토링했다.
불필요한 Connection 요청
또, DB 연결시 문제가 발생하기도 하는데, Validator에서 불필요하게 connection을 요청하게 된다는 문제가 있다. service계층에서 가져온 값으로 검증하면 되는데, 굳이 Validator에서 또 쿼리를 요청하면 불필요한 connection이 발생하게 된다.
public class LoginValidator implements Validator {
private final UserService userService;
public static final String LOGIN_ERROR = "loginGlobal";
@Override
public boolean supports(Class<?> clazz) {
return LoginUserDto.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
LoginUserDto user = (LoginUserDto) target;
String password = user.getPassword();
// 로그인 검증
getFoundUser(user)
.ifPresent(foundUser -> {
if (isInvalidPassword(foundUser, password)) {
errors.reject(LOGIN_ERROR, null, null);
}
});
}
private Optional<User> getFoundUser(LoginUserDto user) {
return userService.findByUsername(user.getUsername()); // 불필요한 Connection 요청
}
/**
* 받은 User 객체와 password 파라미터가 일치하는지 확인하고 실패할 시 결과 반환
*
* @param user : User 객체
* @param password : password
* @return : 일치하다면 true, 일치하지 않으면 false
*/
private boolean isInvalidPassword(User user, String password) {
if (user == null) {
return true;
}
return !user.getPassword().equals(password);
}
기존 컨트롤러 코드
Validator이 들어있으면서도, Exception 처리가 중복되어 있고, 코드가 매우 길다.
public class UserBookController {
private final BookService bookService;
private final BookRentValidator bookRentValidator;
private final BookUnRentValidator bookUnRentValidator;
private final ErrorResponseUtils errorResponseUtils;
@PostMapping("/{bookId}/rent")
public ResponseEntity<JsonResponse> rent(@Login User user, @PathVariable("bookId") Long bookId) {
// Optional로 Book을 안전하게 처리
Book findBook = bookService.findBookById(bookId)
.orElseThrow(NotFoundBookException::new);
log.debug("rent by user={}", user);
log.debug("rent findBook={}", findBook);
BookRentDto bookRentDto = new BookRentDto(user, findBook);
BindingResult bindingResult = new BeanPropertyBindingResult(bookRentDto, "rentBookDto");
log.debug("bindingResult.getObjectName()={}", bindingResult.getObjectName());
log.debug("bindingResult.getTarget()={}", bindingResult.getTarget());
bookRentValidator.validate(bookRentDto, bindingResult);
if (bindingResult.hasErrors()) {
log.debug("errors={}", bindingResult);
return errorResponseUtils.handleValidationErrors(bindingResult);
}
user.rent(findBook);
return ResponseEntity.ok().body(JsonResponse.builder()
.message("정상 대출되었습니다.")
.build());
}
@PostMapping("/{bookId}/unrent")
public ResponseEntity<JsonResponse> unRent(@Login User user, @PathVariable("bookId") Long bookId) {
Book findBook = bookService.findBookById(bookId)
.orElseThrow(NotFoundBookException::new);
log.debug("unRent by user={}", user);
log.debug("unRent findBook={}", findBook);
BookRentDto bookRentDto = new BookRentDto(user, findBook);
BindingResult bindingResult = new BeanPropertyBindingResult(bookRentDto, "rentBookDto");
log.debug("bindingResult.getObjectName()={}", bindingResult.getObjectName());
log.debug("bindingResult.getTarget()={}", bindingResult.getTarget());
bookUnRentValidator.validate(bookRentDto, bindingResult);
if (bindingResult.hasErrors()) {
log.debug("errors={}", bindingResult);
return errorResponseUtils.handleValidationErrors(bindingResult);
}
user.unRent(findBook);
return ResponseEntity.ok().body(JsonResponse.builder()
.message("정상 반납되었습니다.")
.build());
}
}
리팩토링된 코드
Rental 도메인을 분리해서 user 패키지에서 벗어났고, 예외는 서비스 계층에서 주로 던진다.
ExceptionHandler를 사용해서 한번에 예외를 처리한다.
BookRentalController
public class BookRentalController {
private final BookService bookService;
private final BookRentalService bookRentalService;
private final ErrorResponseUtils errorResponseUtils;
@PostMapping("/{bookId}/rent")
public ResponseEntity<JsonResponse> rent(@Login User user, @PathVariable("bookId") Long bookId) {
// Optional로 Book을 안전하게 처리
Book findBook = bookService.findBookById(bookId)
.orElseThrow(NotFoundBookException::new);
log.debug("rent by user={}", user);
log.debug("rent findBook={}", findBook);
bookRentalService.rentBook(user, findBook);
return ResponseEntity.ok().body(JsonResponse.builder()
.message("정상 대출되었습니다.")
.build());
}
@PostMapping("/{bookId}/unrent")
public ResponseEntity<JsonResponse> unRent(@Login User user, @PathVariable("bookId") Long bookId) {
Book findBook = bookService.findBookById(bookId)
.orElseThrow(NotFoundBookException::new);
log.debug("unRent by user={}", user);
log.debug("unRent findBook={}", findBook);
bookRentalService.unRentBook(user, findBook);
return ResponseEntity.ok().body(JsonResponse.builder()
.message("정상 반납되었습니다.")
.build());
}
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleNotFoundBookError(RentalException e) {
return errorResponseUtils.handleExceptionError(e);
}
}
BookRentalService
public class BookRentalService {
private final BookRentalRepository bookRentalRepository;
private final BookService bookService;
private final UserService userService;
public Rental rentBook(User user, Book book) {
// remailrents가 없거나, 현재 누가 대출중인 경우 예외 발생함
User rendtedUser = findUserByBookId(book.getId());
if (user.equals(rendtedUser)) {
throw new RentalException("이미 대출중입니다.");
} else if (user.getRemainingRents() < 1) {
throw new RentalException("더 이상 빌릴 수 없습니다.");
} else if (book.isRented()) {
throw new RentalException("이미 다른 유저가 대출중입니다.");
}
book.rent();
user.decrementRemainingRents();
return bookRentalRepository.save(new Rental(book.getId(), user.getId()));
}
public void unRentBook(User user, Book book) {
User rendtedUser = findUserByBookId(book.getId());
if (!user.equals(rendtedUser)) {
throw new RentalException("빌리지 않은 도서입니다.");
}
Rental rental = bookRentalRepository.findActiveRentalByBookId(book.getId())
.orElseThrow(() -> new RentalException("이미 대출중인 도서입니다."));
bookService.findBookById(rental.getBookId())
.ifPresent(Book::unRent);
userService.findById(rental.getUserId())
.ifPresent(User::incrementRemainingRents);
rental.returnBook();
}
public User findUserByBookId(Long bookId) {
return bookRentalRepository.findActiveRentalByBookId(bookId).flatMap(rental -> userService.findById(rental.getUserId()))
.orElse(null);
}
}
ErrorResponseUtils
public ResponseEntity<ErrorResponse> handleExceptionError(RentalException e) {
Map<String, String> errors = new HashMap<>();
errors.put(e.objectName, e.getMessage());
return ResponseEntity.badRequest().body(ErrorResponse.builder()
.code("global")
.errors(errors).build());
}