본문 바로가기

Spring

스프링 MVC에서 커스텀 ArgumentResolver로 코드 중복 줄이기: @UserCache 구현 예제

소스코드:

 

GitHub - youseonghyeon/argument-resolver

Contribute to youseonghyeon/argument-resolver development by creating an account on GitHub.

github.com

 

ArgumentResolver를 이용해서 Controller 파라미터 어노테이션을 만들어보겠습니다 !!

 

1. 사용 목적

컨트롤러(Controller)에서 인증 정보나 헤더 정보를 활용해야 할 때가 종종 있습니다. 예를 들어, 쿠키(cookie)에 저장된 세션 ID를 이용하여 사용자 정보를 가져오는 경우를 생각해보겠습니다.

기존 방식으로는 다음과 같이 세션 ID를 이용하여 사용자 정보를 가져오는 코드를 작성할 수 있습니다.

    @GetMapping("/")
    public String getUser(@CookieValue("my-session") String sessionId) {
        User user = userCacheStore.get(sessionId);
        ...
    }

 

위와 같이 하나의 컨트롤러에서 이러한 로직을 사용한다면 큰 문제가 없겠지만, 여러 컨트롤러에서 같은 로직을 반복 사용해야 한다면 코드 중복과 유지보수 어려움이 발생할 수 있습니다.

이러한 문제를 해결하기 위해 ArgumentResolver를 활용한 아래 코드로 개선해보겠습니다.

    @GetMapping("/")
    public String getUser(@UserCache("my-session") User user) {
        ...
    }

만약. 한줄밖에 안줄었는데? 라고 생각하신다면

인프런 강사님 김영한님의 말을 빌리겠습니다.

"이런 코드가 100개, 아니 수백개 수천개라면 어떨까요?"

또한, userCacheStore와 같은 구체적인 클래스를 감추고, @UserCache 어노테이션을 통해 로직을 추상화함으로써 OCP(Open-Closed Principle)를 준수할 수 있습니다.

 

기능을 사용하기 위해서는 아래와 같은 단계를 거쳐야 합니다.

1. 파라미터 어노테이션 생성 (권장)

파라미터 어노테이션인 @UserCache를 생성합니다. 이 어노테이션은 컨트롤러 메서드의 파라미터에 붙여서 사용됩니다. @UserCache 어노테이션은 다음과 같이 구현할 수 있습니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface UserCache {
    String value();
}

 

어노테이션은 필수가 아니나 @ModelAttribute와 기능이 충돌할 수 있으므로 어노테이션을 사용하는 것을 권장합니다.

@ModelAttribute는 생략이 가능하며, 이러한 이유로 충돌 가능성이 있습니다.

 

 

2. 파라미터 처리 로직 작성

HandlerMethodArgumentResolver를 상속받아 UserCacheResolver를 생성합니다.

 

supportsParameter는 어떤 파라미터가 존재할 때 해당 로직을 적용할지 판단하는 역할을 합니다.

resolveArgument는 supportsParameter에서 true를 리턴한 경우에 실행되며, 실제로 해당 로직을 처리하는 역할을 합니다. 

@Component
public class UserCacheResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UserCache.class)
                && parameter.getGenericParameterType().equals(User.class);
    }

    @Override
    public User resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        return new User();
    }
}

 

위 내용을 보면 supportsParameter는 파라미터가 @UserCache를 붙이고 있고 타입이 User인 파라미터인지 확인합니다.

resolveArgument는 단순히 User 객체를 생성하여 리턴(파라미터 바인딩) 합니다.

 

CacheUser를 가져오는 로직은 아래 4번을 참고해주세요.

 

 

3. WebMvcConfig에 작성한 처리 로직 등록 

앞서 생성한 UserCacheArgumentResolver를 WebMvcConfig에 등록합니다. WebMvcConfig는 WebMvcConfigurer 인터페이스를 구현하여 웹 MVC 설정을 위한 메서드들을 Override할 수 있습니다.

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final UserCacheResolver userCacheResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userCacheResolver);
    }
}

WebMvcConfig 클래스에서 addArgumentResolvers() 메서드를 Override하여, 앞서 생성한 UserCacheArgumentResolver를 resolvers 리스트에 추가해줍니다.

이로써 우리가 직접 만든 ArgumentResolver인 UserCacheArgumentResolver가 Spring Web MVC 설정에 추가되었습니다. 이제 @UserCache 어노테이션을 사용하는 컨트롤러 메서드의 파라미터에 ArgumentResolver가 적용되었습니다.

 

 

4. Cache 저장소 생성 및 호출 로직 작성

이제 Cookie에 있는 데이터를 이용하여 User를 가져오는 로직을 작성해보겠습니다.

위에서 보았던 resolveArgument 메서드를 수정하겠습니다.

    private final UserCacheStore userCacheStore;

    @Override
    public User resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        HttpServletRequest req = webRequest.getNativeRequest(HttpServletRequest.class);
        String sessionName = parameter.getParameterAnnotation(UserCache.class).value();

        Optional<Cookie> cacheCookie = findCookieByName(req.getCookies(), sessionName);
        String cacheKey = cacheCookie.map(Cookie::getValue).orElse("");

        return userCacheStore.get(cacheKey);
    }

    private Optional<Cookie> findCookieByName(@Nullable Cookie[] cookies, String sessionName) {
        if (cookies == null) {
            return Optional.empty();
        }
        
        return Arrays.stream(cookies)
                .filter(cookie -> cookie.getName().equals(sessionName))
                .findFirst();

    }

 

 

resolveArgument 메서드에서는 다음과 같은 동작을 합니다
1. HttpServletRequest를 획득하여 Cookie를 가져옵니다.
2. @UserCache 어노테이션에서 지정한 sessionName 값을 얻습니다.
3. 해당 sessionName을 가진 Cookie를 찾습니다.
4. Cache로 사용할 cacheKey를 얻어옵니다.
5. userCacheStore에서 cacheKey를 이용하여 User를 가져옵니다.


이렇게 하면 `@UserCache` 어노테이션을 사용하는 컨트롤러 메서드의 파라미터에 ArgumentResolver가 적용되어 세션 ID를 이용하여 사용자 정보를 가져오는 로직이 완성됩니다.

자세한 코드는 상단에 언급한 GitHub 링크를 확인하실 수 있습니다.

 

만약 ArgumentResolver의 작동 원리를 알고 싶으시면 아래 링크에서 확인 가능합니다.

https://youseong.tistory.com/31

 

감사합니다.