ArgumentResolver 응용 기록
이전에 매번 요청을 받을 때, 회원을 검증하는 과정을 매번 SecurityUtil에서 getCurrentMemberId() 메소드를 호출해서 사용했다.
public static Long getCurrentMemberId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null || authentication.getName().equals("anonymousUser")) {
return null;
}
return Long.parseLong(authentication.getName());
}
@Operation(summary = "로그아웃 API")
@PostMapping("/logout")
public ResponseEntity<String> logout(HttpServletRequest request, HttpServletResponse response) {
authService.logout(request, response, getCurrentMemberId());
return ResponseEntity.ok("LOGOUT!");
}
이런 식으로 여러 개의 api마다 getCurrentMemberId를 호출해서 직접 서비스 코드를 호출할 때 메소드에 직접 넣으니, 가독성도 좋지 않고 관리하기도 쉽지 않았다..
이번 서비스에서는 Resolver와 커스텀 어노테이션을 이용해 조금 더 용이하게 관리해 볼 예정이다.
이에 글로 기록하기로 했다!
기존 시큐리티 코드는 그대로 냅둔 채,
아래 클래스들을 작성해줬다.
public class CurrentUserIdArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.getParameterType().equals(Long.class) && parameter.hasParameterAnnotation(LoginMember.class);
}
@Override
public Long resolveArgument(final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) throws Exception {
NewProjectMember userDetails = (NewProjectMember) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (userDetails == null) {
throw new AuthenticationException(ErrorCode.INVALID_AUTHENTICATION);
}
return userDetails.member().getId();
}
}
supportsParameter는, 주어진 파라미터가 Long 타입이면서 동시에 @LoginMember 어노테이션이 붙어 있어야 true를 반환한다. -> 이 리졸버가 해당 파라미터를 처리할 수 있음을 알려준다.
resolveArgument에서 실제로 파라미터 값을 생성하는 로직을 구현한다.
userDetails 변수에 현재 인증된 사용자의 정보를 가져온 후, 만약 NewProjectMember 객체가 아닌 null값이라면 예외를 발생시킨다.
정상 값이라면, 객체에서 member를 추출하고 해당 회원의 ID를 반환한다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Parameter(hidden = true)
public @interface LoginMember {
}
위의 LoginMember 커스텀 어노테이션이다.
@Parameter(hidden = true)를 이용해 swagger에서 노출되지 않게 할 수 있다.
아, WebConfig에도 추가해주어야 한다.
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserIdArgumentResolver());
}
}
이처럼 우리가 구현한 CurrentUserIdArgumentResolver를 등록한다.
이렇게 코드를 작성해주면, 아래와 같이 가독성과 유지보수성을 챙길 수 있다.
+ 회원만 사용할 수 있는 api인지 명시 가능
@Operation(summary = "게시글 상세 조회")
@GetMapping("/{postId}")
public ResponseEntity<CommonResponse<LoadPostResponse>> loadPost(
@LoginMember Long memberId,
@PathVariable Long postId
) {
return CommonResponse.success(SuccessCode.SUCCESS, postService.loadPost(memberId, postId));
}
그리고, 예를 들어 위처럼 게시글 상세 조회 api가 있을 때, 서비스마다 다르겠지만 비회원도 조회가 가능한 서비스라면 @LoginMember를 붙이지 않고 호출해야 비회원 로직을 따로 처리할 수 있다.
이는 관련해서 따로 포스팅하겠다.