티스토리 뷰

728x90
반응형

처음 써보는 웹플럭스에 처음 써보는 리액티브 몽고+레디스를 사용해 마이그레이션을 시도할 때

 

가장 먼저 당황한 부분이 바로 이 부분이다.

 

MVC패턴에선 당연히, 아주 손쉽게 이루어졌던 <애너테이션을 이용한 유효성 검증>이

 

함수형 엔드포인트를 사용하는 웹플럭스에서는 작동하지 않았던 것.

 

물론 내 경험의 산물이라 100% 확신할 수는 없지만,

 

여태까지 내가 알기론 함수형 엔드포인트에서 기존의 방식대로 간편하게 유효성 검증을 하는 방법은 없다, 혹은

 

다른 라이브러리를 불러서 사용해야 한다.

 

아이 그럼 어쩌라고?

 

에 대해 헤매면서 얻은 답을 정리한다.

 

Validator

 

결론부터 말하자면, 웹플럭스에서는 검증이 필요한 클래스(DTO와 같은)마다 개별 검증 클래스를 구현해야 한다.

 

따라서 CRUD에 따라 검증 로직이 다르고 복잡하다면, 패키지 안에 유효성 검증 클래스만 몇 개씩 만들어야 한다.

 

내가 개발 중인 패키지를 예로 들면 아래와 같다.

 

사용자 패키지 안에 회원가입, 최초 로그인, 업데이트 등에 사용하는 검증 클래스가 따로 존재한다.

 

이 클래스는 전부 Validator라는 인터페이스를 구현해서 만드는데, 그 과정에서 validate() 메서드를 구현해야 하고

 

이를 핸들러 클래스에서 불러다 사용하는 것이다.

 

해서 위 클래스 중 가장 복잡한 아이를 import문을 포함해 적어보면 아래와 같이 생겼다.

package backend.domain.user.validator;

import static backend.domain.constant.Constant.*;

import java.util.regex.Pattern;

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import backend.domain.user.dto.UserRequestDto;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class UserUpdateInfoValidator implements Validator {


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

	@Override
	public void validate(Object target, Errors errors) {

		ValidationUtils.rejectIfEmpty(errors, "nickname", "NICKNAME_IS_EMPTY", "Nickname Is Empty");
		ValidationUtils.rejectIfEmpty(errors, "genderType", "GENDER_TYPE_IS_EMPTY", "Gender Type Is Empty");
		ValidationUtils.rejectIfEmpty(errors, "ageType", "AGE_TYPE_IS_EMPTY", "Age Type Is Empty");
		ValidationUtils.rejectIfEmpty(errors, "favoriteBeerCategoryList", "FAVORITE_BEER_CATEGORY_IS_EMPTY", "Favorite Beer Category Is Empty");
		ValidationUtils.rejectIfEmpty(errors, "favoriteBeerTagList", "FAVORITE_BEER_TAG_IS_EMPTY", "Favorite Beer Tag Is Empty");

		UserRequestDto.UpdateInfoRequest targetUserDto = (UserRequestDto.UpdateInfoRequest)target;
		String nicknamePattern = "^[가-힣ㄱ-ㅎa-zA-Z0-9._-]{2,8}$";

		if (!Pattern.matches(nicknamePattern, targetUserDto.getNickname())) {
			errors.rejectValue("nickname", "NICKNAME_NOT_VALID", "닉네임은 2-8 자리의 숫자, 영어, 한국어와 언더스코어만 허용됩니다.");
		}
		if (!GENDER_TYPE_SET.contains(targetUserDto.getGenderType())) {
			errors.rejectValue("genderType", "GENDER_TYPE_NOT_VALID", "Gender Type Is Not Valid");
		}
		if (!AGE_TYPE_SET.contains(targetUserDto.getAgeType())) {
			errors.rejectValue("ageType", "AGE_TYPE_NOT_VALID", "Age Type Is Not Valid");
		}
		if (!BEER_CATEGORY_SET.containsAll(targetUserDto.getFavoriteBeerCategoryList())) {
			errors.rejectValue("favoriteBeerCategoryList", "FAVORITE_BEER_CATEGORY_NOT_VALID", "Favorite Beer Category Is Not Valid");
		}
		if (targetUserDto.getFavoriteBeerCategoryList().size() > 2) {
			errors.rejectValue("favoriteBeerCategoryList", "TOO_MANY_FAVORITE_BEER_CATEGORY", "선호 맥주 카테고리는 최대 두 개까지 선택할 수 있습니다.");
		}
		if (!BEER_TAG_SET.containsAll(targetUserDto.getFavoriteBeerTagList())) {
			errors.rejectValue("favoriteBeerTagList", "FAVORITE_BEER_TAG_NOT_VALID", "Favorite Beer Tag Is Not Valid");
		}
		if (targetUserDto.getFavoriteBeerTagList().size() > 4) {
			errors.rejectValue("favoriteBeerTagList", "TOO_MANY_FAVORITE_BEER_TAG", "선호 맥주 태그는 최대 네 개까지 선택할 수 있습니다.");
		}
	}
}

유효성 검증을 손으로 하나씩(...) 적어준 것을 확인할 수 있겠다.

 

글을 올리면서도 분명 이거보다 단순하면서도 우아한 방법이 어디엔가 있으리라 믿음이 생긴다.

 

하여간에 DTO 클래스에 애너테이션을 추가하는 것은 먹히지 않고, 위 클래스처럼 구현하면

 

일단은 유효성 검증이 가능해진다.

 

위에도 적었지만 이렇게 구현한 코드를 핸들러메서드에서 아래와 같이 불러온다.

private void validate(UserRequestDto.UpdateInfoRequest updateInfoRequest) {
	Errors errors = new BeanPropertyBindingResult(updateInfoRequest, UserRequestDto.UpdateInfoRequest.class.getName());

	userUpdateInfoValidator.validate(updateInfoRequest, errors);
	if (errors.hasErrors()) {
		throw new ServerWebInputException(errors.toString());
	}
}

이렇게 적은 뒤에 핸들러메서드를 구현하면서 유효성 검증을 진행하면 된다.

 

그 글은 핸들러메서드를 구현하는 글에서 다시 보기로 하고,

 

이런 방법을 이용해서라도, 나처럼 헤매는 사람이 줄어들면 좋겠다...

 

Addition: Sinks.Many<User> usersSink = Sinks.many().replay().latest();

 

레퍼런스 코드에서만 보고 뭔 뜻인지도 모르고 사용하다가 이 글을 적는 김에 공부했다.

 

한 마디로 말하자면 구독자에게 최신 데이터만 전달하기 위한 메서드 체인이라고 한다.

 

구체적으로는

 

  • Sinks - 다수의 객체를 다룰 수 있는 인터페이스
  • many() - 다수의 이벤트를 위한 Hot Source(Sinkk.Many) 생성
  • replay() - 새로운 구독자가 구독을 시작하면 Sink.Many가 이후의 이벤트를 전달할 수 있도록 이벤트 재생성
  • latest() - 구독자에게 최신 값만을 전달, 이전 값은 무시

가 되며, 구독자는 구독 시점 이후에 발생하는 User 객체만을 수신하게 된다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함