[Spring] HandlerInterceptor의 동작을 어노테이션으로 관리해보자

Jihoon Oh·2022년 7월 17일
1
post-thumbnail

보통 로그인이 되어 있는지 처리를 하기 위해 HandlerInterceptor의 구현체를 만들어 사용합니다. 그리고 이 인터셉터 구현체의 작동을 제한하기 위해서 등록 시 인터셉터가 작동할 path 패턴을 지정해줄 수 있습니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
            .addPathPatterns("/api/posts");
    }
}

하지만 이렇게 할 경우 URL에 대해서는 인터셉터를 설정할 수 있지만 HTTP 메서드에 대해서는 설정을 할 수 없습니다. 이는 굉장히 불편합니다. 예를 들어 보겠습니다. /api/posts 라는 게시물에 대한 API URL이 존재한다고 하겠습니다. 이 URL에 대해 다음과 같은 2가지 메서드가 매핑되어 있습니다.

  • GET /api/posts: 전체 게시물 조회(로그인이 필요하지 않음)
  • POST /api/posts: 게시물 작성(로그인이 필요함)

하지만 인터셉터는 URL에 대해서만 설정할 수 있기 때문에 /api/posts에 대해 인터셉터를 설정하면 GET 요청이 들어오든 POST 요청이 들어오든 인터셉터가 기능하도록 설정할 수 밖에 없습니다. 결국 다음과 같은 코드가 추가되어야 합니다.

public class AuthInterceptor implements HandlerInterceptor {

    ...
    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        if (!HttpMethod.POST.matches(request.getMethod())) {
            return true;
        }
        ...
    }
    ...
}

그런데 만약 같은 인터셉터를 사용해야 하는데 이번에는 GET에서도 작동해야 한다면 어떻게 될까요? /api/posts에서는 GET 메서드에 대해 인터셉터가 설정되면 안되므로 preHandle 안에 if (!HttpMethod.GET.matches(request.getMethod()))를 사용할 수는 없습니다. 결국 GET용 인터셉터, POST용 인터셉터, 이런 식으로 여러 개의 인터셉터를 만들어줘야 합니다. 이런 불편함을 어노테이션을 통해 해결할 수 있다면 어떨까요? 예를 들어 컨트롤러의 @LoginRequired 이라는 어노테이션이 붙은 메서드에 대해서만 인터셉터가 동작하도록 할 수 있지 않을까요?

우선 동작의 대상이 될 어노테이션부터 만들어보도록 하겠습니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}

컨트롤러의 메서드에만 붙을 어노테이션이므로 @Traget(ElementType.METHOD)로 선언해주도록 하겠습니다. 이제 인터셉터에서는 이 어노테이션이 붙은 메서드인지 체크해주면 됩니다. preHandle의 매개변수로 들어오는 handler를 활용해서 어노테이션을 체크해주도록 하겠습니다.

public class AuthInterceptor implements HandlerInterceptor {

    ...
    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
            throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler; // (1)
        LoginRequired loginRequired = handlerMethod.getMethodAnnotation(LoginRequired.class); // (2)
        if (Objects.isNull(loginRequired)) {
            return true; // (3)
        }
        ... // (4) (인터셉터의 로직 처리)
    }
    ...
}
  • (1): HandlerMethod의 기능을 사용하기 위해 Object 타입으로 들어온 handler를 HandlerMethod 타입으로 형변환해줍니다. 윗줄의 instanceof 과정은 비검사 형변환 과정에서 런타임 에러를 방지하기 위함입니다.
  • (2): HandlerMethodgetMethodAnnotation 메서드로 우리가 정의한 RequireLogin 어노테이션을 꺼냅니다.
  • (3): 만약 대상이 되는 메서드가 @LoginRequired 어노테이션이 붙어있는 메서드가 아니라면 getMethodAnnotation으로 꺼낼 값이 없으므로 loginRequired 값은 null이 될 것입니다. 따라서 null인 경우 이 인터셉터가 처리할 메서드가 아니라고 판단하여 true를 반환해 컨트롤러의 동작을 진행시킵니다.
  • (4): 여기까지 도달했다면 대상이 되는 메서드는 @LoginRequired이 붙어 있는, 즉 로그인이 필요한 메서드입니다. 로그인 검증 처리를 하는 로직을 작성하면 됩니다.

그리고 컨트롤러에서는 로그인이 필요한 메서드에 @LoginRequired 어노테이션을 붙여주면 됩니다.

@RestController
public class PostsController {

    ...
    @GetMapping("/api/posts")
    public ResponseEntity<List<Post>> showPosts() {
        ...
    }
    
    @PostMapping("/api/posts")
    @LoginRequired // `@LoginRequired`이 붙었으므로 로그인을 체크한다.
    public ResponseEntity<Void> createPost(@RequestBody PostCreateRequest postCreateRequest) {
        ...
    }
    ...
}

이렇게 원하는 메서드에 대해서만 인터셉터가 동작하도록 설정할 수 있습니다.

참고자료

우아한테크코스 4기 모라고라 팀
🙈[Spring] Interceptor (2) - 어노테이션 작성 및 접근 권한, 세션 처리🐵

profile
Backend Developeer

1개의 댓글

comment-user-thumbnail
2022년 9월 29일

찾던 내용인데 감사합니다!

답글 달기