Valitator vs Exception 처리

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());
    }

댓글

개발자  김철준

백엔드 개발자 김철준의 블로그입니다.

주요 프로젝트