매 요청마다 사용자 테이블에 쿼리 날아가는 문제 해결
요청할 때마다 member 테이블에 쿼리가 날아간다?
개발 환경에서 개발을 하던 중, 이상한 현상을 발견했다.
JDBC Connection [HikariProxyConnection@92683318 wrapping com.mysql.cj.jdbc.ConnectionImpl@67f946c3] will be managed by Spring
==> Preparing: SELECT * FROM member WHERE id = ? AND status != 'WITHDRAWN'
프론트에서 요청을 할 때마다 본 쿼리를 날리기 전에 이 쿼리를 앞서서 DB에 계속 날리고 있었다.
심지어 트랜잭션도 따로 묶여서 전반적으로 성능 하락의 원인이 될 게 뻔해 보였다. 당장 고치지 않을 수 없었다.
문제 발견
// AccessToken 만료 자동 감지 -> AccessToken 이 만료되었으면 401 Unauthorized 응답
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
if (jwtProvider.validateAccessToken(token)) {
Long memberId = jwtProvider.extractMemberId(token);
String role = jwtProvider.extractRole(token);
Member member = memberMapper.findById(memberId); // 여기!!!
...
} else {
// ❗ 유효하지 않은 토큰이면 401 반환하고 필터 중단
...
}
}
filterChain.doFilter(request, response);
}
문제가 되는 지점을 발견했다. JwtAuthenticationFilter에 member 테이블에 쿼리를 날리는 memberMapper의 호출이 있었다.
@Getter
public class CustomUserDetails implements UserDetails {
private final Member member;
public CustomUserDetails(Member member) {
this.member = member;
}
...
더 살펴보니… 개발 초기에 세션 기반 인증 로직을 구현하는 CustomUserDetails, CustomUserDetailsService 등의 코드가 아직 남아있었고, 이것을 비효율적으로 그대로 사용하고 있었던 것이다.
현재는 토큰 기반 인증을 사용하고 있고, 토큰에 memberId가 들어있으니 이것을 그냥 파싱해서 사용하면 된다.
따라서 세션 기반 인증 로직을 구현하는 코드를 없애고, 각 비즈니스 로직을 담당하는 컨트롤러에서 토큰에 담긴 사용자 정보를 편리하게 빼내서 사용할 수 있게 하기 위해 다음과 같이 만들어 주었다.
문제 해결
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentMember {
}
컨트롤러의 메서드 파라미터에 붙일 커스텀 어노테이션을 만들었다. 이 어노테이션이 붙은 파라미터는 요청을 보낸 사용자 정보로 자동 주입될 것이다.
public record CurrentMemberInfo(Long memberId, String role) {
}
토큰에서 추출한 사용자 정보를 담을 간단한 record 클래스다. memberId와 role 정보만 있으면 대부분의 비즈니스 로직을 처리할 수 있다.
@Component
public class CurrentMemberArgumentResolver implements HandlerMethodArgumentResolver {
// 어노테이션 붙일 수 있는지
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentMember.class)
&& parameter.getParameterType().equals(CurrentMemberInfo.class);
}
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return (CurrentMemberInfo) auth.getPrincipal();
}
}
Spring의 ArgumentResolver 인터페이스를 구현한 핵심 클래스다. @CurrentMember 어노테이션이 붙은 파라미터를 발견하면, SecurityContext에서 인증 정보를 가져와 CurrentMemberInfo 객체로 변환해서 주입해준다.
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentMemberArgumentResolver);
}
WebMvcConfigurer를 구현한 설정 클래스에서 위에서 만든 ArgumentResolver를 등록하는 코드다. 이렇게 등록해야 Spring이 우리가 만든 커스텀 ArgumentResolver를 인식하고 사용할 수 있다.
이제 컨트롤러에서 @CurrentMember CurrentMemberInfo memberInfo 파라미터만 추가하면 매번 DB에 쿼리를 날리지 않고도 토큰에서 바로 사용자 정보를 가져올 수 있게 되었다.
댓글남기기