2016년 11월 27일 일요일

ModelAttribute에 대해 annotation 적용하기

이전에 [annotation을 사용한 controller aop 설정하기] 란 글을 쓴 적이 있다.

class 나 method 단위에서 aop를 적용하는 방법에 대한 내용인데 parameter 단위로 aop를 적용을 할 수도 있다.

하지만 개인적으로 느끼기엔 parameter 단위의 aop적용은 그리 효율적이지 못하다.

aop 문법이 특정 타입에 대해 적용되어 있는 annotation을 적용하는 것에 대해 효과적인 명시 방법이 없다.

method parameter의 첫번째가 특정 조건일때, 끝이 특정 족건일 때, 중간에 특정 조건일 때 처럼만 선언이 가능하여 어색하고 딱히 확실한 명시 방법이 없다.
(혹 내가 못찾은 것 일 수도 있겠지만..)


또 하나의 문제는 spring의 ModelAttribute처럼 request parameter나 path variable에 대해 바인딩을 하려면 aop를 통한 처리는 복잡하고 어렵다.


이에 대해 Spring은 HandlerMethodArgumentResolver 기능을 제공한다.

넘어오는 매개 변수에 대해 supportsParameter 인 경우 응답값을 resolveArgument 메소드를 통해 전달해 줄 수 있다.


하지만 해당 구현만 하면 2가지 단점이 있다.
  1. 외부에서 넘어온 request parmaeter나 path variable의 값이 object 에 맵핑되지 않는다. (modelAttribute 기능이 적용되지 않는다.)
  2. hibernate validation 체크가 무시 된다.


따라서 스프링이 modelAttribute에 대해 어떻게 동작하는지를 살펴보는 것이 좋다.

스프링에서는 HandlerMethodArgumentResolver을 modelAttribute에 대해서 구현한 ModelAttributeMethodProcessor를 제공하며 특히 servlet을 사용하는 경우 ModelAttributeMethodProcessor를 확장한 ServletModelAttributeMethodProcessor를 제공한다.

ModelAttributeMethodProcessor는 resolveArgument 메소드가 final 선언 되어 있어 커스텀한 확장을 하지 못한다.


따라서 다음처럼 custom ModelAttributeMethodProcessor를 만들어준다.

public abstract class BlueskyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
 
 /* (s) ServletModelAttributeMethodProcessor method copy */
 @Override
 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  String name = ModelFactory.getNameForParameter(parameter);
  Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
    createAttribute(name, parameter, binderFactory, webRequest));

  if (!mavContainer.isBindingDisabled(name)) {
   ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
   if (ann != null && !ann.binding()) {
    mavContainer.setBindingDisabled(name);
   }
  }

  WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
  if (binder.getTarget() != null) {
   if (!mavContainer.isBindingDisabled(name)) {
    bindRequestParameters(binder, webRequest);
   }
   validateIfApplicable(binder, parameter);
   if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    throw new BindException(binder.getBindingResult());
   }
  }

  // Add resolved attribute and BindingResult at the end of the model
  Map<string, object> bindingResultModel = binder.getBindingResult().getModel();
  mavContainer.removeAttributes(bindingResultModel);
  mavContainer.addAllAttributes(bindingResultModel);

  return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
 }

 protected Object createAttribute(String attributeName, MethodParameter methodParam,
   WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {

  return BeanUtils.instantiateClass(methodParam.getParameterType());
 }
 
 protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
  ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
  ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
  servletBinder.bind(servletRequest);
 }
 
 protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
  Annotation[] annotations = methodParam.getParameterAnnotations();
  for (Annotation ann : annotations) {
   Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
   if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
    Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
    Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
    binder.validate(validationHints);
    break;
   }
  }
 }
 
 protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
  int i = methodParam.getParameterIndex();
  Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
  boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
  return !hasBindingResult;
 }
 /* (e) ServletModelAttributeMethodProcessor method copy */
}


이제 custom한 annotation을 만들어 본다.


/**
 * 로그인한 유저의 Blog 정보를 주입하기 위해 사용하는 aop
 * @author bluesky
 *
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface UserBlog {

 public boolean checkBlog() default false; // 외부 접근 파라메터에 대해 유저의 blog 인지 확인 여부
}

해당 어노테이션이 적용된 modelAttribute에 대한 methodArgumentResolver를 만들어준다.

/**
 * 유저의 Blog 정보를 반환하는 Resolver
 * @author bluesky
 *
 */
public class UserBlogHandlerMethodArgumentResolver extends BlueskyHandlerMethodArgumentResolver {

 @Override
 public boolean supportsParameter(MethodParameter parameter) {
  return Blog.class.isAssignableFrom(parameter.getParameterType()) && Arrays.asList(parameter.getParameterAnnotations()).stream().anyMatch(annotation -> annotation.annotationType().isAssignableFrom(UserBlog.class));
 }

 @Override
 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  List<Blog> userBlogList = BlogRequestAttributeUtil.getUserBlogList();
  if (userBlogList.isEmpty()) {
   throw new BlueskyException(BlogErrorCode.NOT_EXIST_BLOG);
  }
  
  Blog requestBlog = (Blog) super.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  
  UserBlog userBlog = (UserBlog) Arrays.asList(parameter.getParameterAnnotations()).stream().filter(annotation -> annotation.annotationType().isAssignableFrom(UserBlog.class)).findAny().get();
  if (userBlog.checkBlog()) {
   
   if (requestBlog.getId() == 0) {
    throw new BlueskyException(BlogErrorCode.NOT_EXIST_PARAMETER_BLOG_ID);
   }
   return userBlogList.stream().filter(blog -> blog.getId() == requestBlog.getId()).findAny().orElseThrow(() -> new BlueskyException(BlogErrorCode.INVALID_PARAMETER_BLOG_ID)); // 유저의 블로그가 아닌 접근인 경우 에러
  }
  
  return userBlogList.get(0);
 }
 
}


이제 해당 annotation 을 사용하면 modelAttribute가 동작하고 hibernate validation도 처리가 되는 custom methodArgumentResolver가 동작하는 것을 확인 할 수 있다.
 @PreAuthorize(AuthorizeRole.PRE_AUTHORIZE_ROLE)
 @GetMapping(value = "/{id}/article/write")
 public String writePage(@UserBlog(checkBlog = true) Blog blog, ModelMap modelMap) {
  modelMap.addAttribute(articleCategoryService.findByBlog(blog));
  return "blog/write";
 }

댓글 없음:

댓글 쓰기