AOP를 사용한 코드 리팩토링 - 스프링 시큐리티 로그인 성능개선

Wintering·2022년 8월 8일
0

PROJECT

목록 보기
4/5
  • 개선이 필요한 나쁜코드 ? 반복되는 코드
  • 프로젝트에서 개선할 만한 부분? 로그인한 유저의 정보를 가져오는 부분

Before

원래 프로젝트에서 로그인 한 유저에 대한 정보를 가져오는 코드는 아래와 같다.

   public String main(@CookieValue(required = false, name = "access_token") 
   					String token, Model model) {
        if (token != null) {
            String userEmail = tokenProvider.getUserEmailByToken(token);
            User user = userService.getUserByEmail(userEmail);
            model.addAttribute("user", user);

token에서 userEmail정보를 빼온 이후, userEmail 정보를 이용하여 해당하는 유저의 정보를 넘겨받는 형식인데, 우리 프로젝트의 특성상 유저의 정보가 어디서든 필요하고 + 프로젝트의 볼륨이 작지 않다보니 이 대략 5줄의 코드가 모든 컨트롤러에 존재하는 게 상당히 보기 좋지 않은 요인이라고 생각했고 모듈화 시킬 수 있는 방법을 고민했다.

튜터님의 도움을 받아 userArgumentResolver라는 개념에 대해 알게되었다. 컨트롤러 메서드에서 특정 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩해주는 인터페이스인 HandlerMethodArgumentResolver를 상속받아 만드는, 로그인 한 유저를 판별하는 Resolver이다.

HandlerMethodArgumentResolver

After

1. common 패키지에 UserArgumentResolver 클래스 생성

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return User.class.isAssignableFrom(methodParameter.getParameterType());
    }
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!(authentication instanceof AnonymousAuthenticationToken)) {
            return authentication.getPrincipal();
        }
        return null;
    }
}
  • supportsParameter()
    • 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단합니다.
    • 해당 코드에서는 파라미터 클래스 타입이 User.class인 경우 true를 반환합니다.
  • resolveArgument()
    • 파라미터에 전달할 객체를 생성합니다.
    • 해당코드에서는 SpringSecurity의 Principal 객체를 가져옵니다.

return User.class.isAssignableFrom(methodParameter.getParameterType());에서
User는 SpringSecurity에 principal로 등록한 객체를 의미한다.
원래 코드에서는 Java에서 제공하는 User라이브러리를 사용해 유저아이디와 비밀번호, 권한만 넘겨서 인증받는 객체를 principal에 등록했었는데, 로그인 한 유저의 정보 전체를 받아오고 싶었기 때문에 코드를 수정해주었다.

if (authToken.validate()) {

            Claims claims = authToken.getTokenClaims();
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()})
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());

            log.debug("claims subject := [{}]", claims.getSubject());
            //수정전
            User principal = new User(claims.getSubject(), "", authorities);
            return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);

			//수정후
            User principal = userRepository.findByUserEmail(claims.getSubject()).orElseThrow();
            return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);

로그인 된 이후, 로그인 된 유저 전체의 정보가 넘어가는 경우가 빈번했기 때문에 토큰이 유효하다면 토큰의 subject값으로 등록해 준 user의 email값을 parameter로 받아서, DB에서 저장 된 정보 자체를 principal 객체로 등록해서 넘겨주었다.

주의사항
could not initialize proxy 에러 발생

Principal객체에 DB에서 가져 온 유저 정보 자체를 담아서 넘겨서, 아래와 같이 다른 서비스에서 User DB에 접근해야할 때, 접근 과정을 줄이는 효과를 보고자했지만, 영속성의 문제로 그건 불가능했다.
(db에 접근하는 과정을 줄이고 싶다면 연관관계 메소드를 사용하지 않는 방식뿐인 것 같다.)

아래 캡쳐를 보면 securityUser로 넘어온 객체는 @15260, 이후 연관관계 메소드를 사용해주기 위해서 DB에 재접근하여 불러온 User 개체는 @15710으로 서로 다른 객체임을 확인할 수 있다.

//자체 생성한 클래스에 원하는 정보를 토큰에서 빼와 담는 식으로 구성
 if (authToken.validate()) {
            Claims claims = authToken.getTokenClaims();
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()})
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());
            String userNickname = claims.get(NICKNAME_KEY, String.class);
            RoleType roleType = RoleType.of(claims.get(AUTHORITIES_KEY).toString());

            User principal = userRepository.findByUserEmail(claims.getSubject()).orElseThrow();
            log.debug("claims subject := [{}]", claims.getSubject());

            return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);

(+)resolver를 사용하기 위해선 webConfig로 resolver를 꼭 등록해줘야한다.
처음에 등록하지 않고 무턱대고 사용했어서 계속 null값만 리턴받는 오류를 마주했었다.
webConfig 클래스를 만들어 꼭 등록해주도록하자


@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final UserArgumentResolver userArgumentResolver;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}

0개의 댓글