맨땅에 헤딩하는 개바른자

Request DTO Validate Aspect 적용기 본문

개발 TIP

Request DTO Validate Aspect 적용기

앵낄낄 2022. 5. 10. 19:28
반응형

Contoller에서 특정 DTO를 받는 Post메소드가 있다고 하였을 때 DTO의 내용이 잘 들어왔는가를 체크해야할 때가 있습니다.

이런경우 다양한 방식으로 검증은 가능하겠지만 여러사람이 코드를 작성하는 경우 통일성이 없게 될 수 있습니다.
이렇게되면 코드 분석 시 서로 다른 방식의 검증로직 확인해야하고 이해하기 어려운 부분들이 있을 것이다.

그래서 공통적이고 뭔가 자동적으로 검증을 하려면 어떻게 해야하는가를 알아보겠습니다.

이번 포스팅에서는 Controller단계에서 SRP 단일책임분리원칙을 준수하는 코드기반에서 DTO Vaildation을 Aspect를 적용한 내용을 소개해보려고 합니다.

갑자기 왠 단일책임?
이번글과 연관없을 수도 있지만 중요한 항목이기 때문에 한번 적어보았습니다.

Controller에서는 어떤 역활이 단일책임원칙에 어울릴까요?

  • 요청 URL 맵핑
  • 요청 파라미터를 수신
  • 요청 파라미터 값 Validation
  • 응답 처리

단일책임원칙을 지키면 어떤 이점이 있는가?

  • 테스트코드가 간략화 된다.
  • 가독성이 높아진다.
    • 지저분한 상태를 최소화 할 수 있습니다.

자 그럼 본론으로 들어와 Aspect를 적용한 코드로 설명을 드려보겠습니다.

[DTO준비]

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class SampleDto implements AbstractRequest {

    private Long id;

    @Size(min = 2, max = 3, message = "자리수 오류입니다.")
    private String code;

    private String name;

    @Override
    public void validate() {
				this.name = name.replace(' ','');
    }
}

vaidate에서는 javax.validation에서 하기 어려운 형태를 사용합니다.

[DTO 상속객체]

public interface AbstractRequest {
    default void validate() {
    }

    static void initAndValidate(final Object o) {
        if (o != null) {
            if (o instanceof Collection) {
                Collection dtos = (Collection)o;
                dtos.forEach(AbstractRequest::initAndValidate);
            } else if (o instanceof AbstractRequest) {
                AbstractRequest request = (AbstractRequest)o;
                request.validate();
                Map<String, Field> fieldsMap 
								Map<String, Field> fieldMap = new HashMap();
				        ReflectionUtils.doWithFields(type, (field) -> {
				            if (!fieldMap.containsKey(field.getName())) {
				                fieldMap.put(field.getName(), field);
				            }
				
				        }, (field) -> {
				            return !ignoreFieldPattern.matcher(field.getName()).matches();
				        });
                BeanWrapper dtoWrapper = PropertyAccessorFactory.forBeanPropertyAccess(request);

                Object fieldValue;
                try {
                    for(Iterator var4 = fieldsMap.entrySet().iterator(); var4.hasNext(); initAndValidate(fieldValue)) {
                        Entry<String, Field> entry = (Entry)var4.next();
                        String fieldName = (String)entry.getKey();
                        if (dtoWrapper.isReadableProperty(fieldName)) {
                            fieldValue = dtoWrapper.getPropertyValue(fieldName);
                        } else {
                            Field field = (Field)entry.getValue();
                            field.setAccessible(true);
                            fieldValue = field.get(request);
                        }
                    }
                } catch (IllegalAccessException var9) {
                    throw new RuntimeException(var9);
                }
            }

        }
    }
}

내용을 간략히 설명드리면 AbstractRequest 을 상속한 DTO인경우 @validate 오버라이드한 메소드를 실행시키는 과정이 있으며 DTO내에 하위 DTO까지 체크하는 로직입니다.

뭔가굉장이 복잡해 보이죠.. 리플렉션이 어쩔수 없이 사용되는 구간이라 복잡해 보일 수 있습니다. class안에 선언된 변수를 찾고 inner클래스까지 체크하기위한 로직 입니다.

[Controller 생성]

Controller class

@PostMapping("/sample")
public SampleDto sample(@Valid @RequestBody SampleDto sampleDto) {
    return someService.some(countrySafetyDto);
}

컨트롤러에 /sample이라는 URL을 가진 post 호출 메소드 입니다.

SampleDto를 요청받는 객체가 있습니다.

Dto내에 javax.validation 관련 어노테이션이 사용되고 있다면 메소드 진입과정에서 validate 처리가 발생 될 것입니다.

하지만 아직 Aspect로 실제 로직에서 검증단계를 실행되지 않습니다.

해당 부분은 아래 로직을 통해서 수행됩니다.

[RequestVaidationAspect]

@RequiredArgsConstructor
@Aspect
public class RequestValidationAspect {

    private final Validator validator;

    /**
     * Controller 클래스 && 인자가 AbstractRequest 구현체이거나 AbstractRequest 구현체의 List 인 경우 동작
     */
    @Before("bean(*Controller) && " +
            "(execution(* *(.., com.test.AbstractRequest+, ..)) || " +
            "execution(* *(.., java.util.List<com.test.AbstractRequest+>, ..)))")
    public void before(final JoinPoint joinPoint) {
        for (final Object arg : joinPoint.getArgs()) {
            if (arg instanceof Collection) {
                final Collection c = (Collection) arg;

                for (Object e : c) {
                    // 스프링 Bean Validation 직접 수행
                    final Set<ConstraintViolation<Object>> errors = this.validator.validate(e);

                    if (!errors.isEmpty()) {
                        throw new ConstraintViolationException(errors);
                    }

                    if (e instanceof AbstractRequest) {
                        // 구현 객체 초기화 및 검증
                        AbstractRequest.initAndValidate(arg);
                    }
                }
            } else {
                // 구현 객체 초기화 및 검증
                if (arg instanceof AbstractRequest) {
                    AbstractRequest.initAndValidate(arg);
                }
            }
        }
    }
}

@Aspect 어노테이션이 선언 된 class 입니다.

우리가 상속으로 구현해놓은 validate 메소드가 요부분에서 처리가된다고 보시면 되겠습니다.

@Before 에서는 적용 대상의 class를 지정하였습니다. (bean(), execution() 부분은 다른 포스팅에서 다루겠습니다.)

AOP는 Filter단계에서 선처리 되기때문에 컨트롤러 진입 전 해당 로직이 타게됩니다.

그래서 컨트롤러 진입 전 상속객체를 사용한 DTO를 검열하고 그안에 validate메소드를 수행하여 검증체크를 진행 할 수 있습니다.

반응형

'개발 TIP' 카테고리의 다른 글

Lombok > 생성자 AccessLevel.PROTECTED 를 알고 사용하자  (0) 2023.01.19
CRUD 공통로직 만들기  (0) 2022.05.10
restTemplate LogInterceptor  (0) 2022.05.03
MDC LogFilter 사용하기  (0) 2022.05.02
restTemple 제네릭Type 사용  (0) 2022.04.28