[스프링] HandlerInterceptor

동동주·2024년 5월 10일
0

초록 스터디

목록 보기
4/4

여러 컨트롤러에서 같은 관심사를 갖고 반복되어 사용하는 코드를 제거하고, 다수의 컨트롤러에 동일한 기능을 제공하기 위해 사용하는 것이 인터셉터이다. 인터셉터는 컨트롤러 로직이 실행되기 전에 실행된다.

📌interceptor 호출 흐름

스프링의 인터셉터의 동작은 크게 '컨트롤러 실행 전', '컨트롤러 실행 후, 뷰 실행 전', '뷰 실행 후' 이 세 단계로 구분된다.

  • 스프링 인터셉터를 만들기 위해서는 HandlerInterceptor 인터페이스를 구현해야하는데, 해당 인터페이스는 preHandle(), postHandle(), afterCompletion() 이라는 세 메소드를 제공한다.
  1. preHandle()
    컨트롤러 호출 전에 호출되는 메소드이다. preHandle()의 반환타입은 boolean 이다. 만약 preHandle() 이 false 를 반환한다면, 다음 HandlerInterceptor 혹은 컨트롤러를 실행하지 않는다.

  2. postHandle()
    컨트롤러가 정상적으로 실행된 이후에 실행되는 메소드이다. 컨트롤러에서 예외가 발생한다면, postHandle() 메소드는 실행되지 않는다.

  3. afterCompletion()
    뷰가 클라이언트에 응답을 전송한 뒤에 실행된다. 컨트롤러 실행과정에서 예외가 발생한 경우 해당 예외가 afterCompletion() 메소드의 4번째 파라미터로 전달되어, 로그로 남기는 등 후처리를 위해 사용될 수 있다.

📌컨트롤러에서 발생한 중복 코드

컨트롤러에서 반복되는 로직을 실행해야할 경우가 존재한다. 인증(Authentication)과 인가(Authorization) 기능이 대표적일 것이다. 아래 코드는 인증을 구현하기 위해 컨트롤러의 여러 메소드에서 중복 코드가 발생한 예시이다.

@GetMapping("/me")
public ResponseEntity<String> getMyInfo(@RequestHeader("Authorization") String token) {
    if (!authService.validateToken(token)) {
        throw new AuthException();
    } // 인증이 필요한 컨트롤러 메소드마다 등장하는 중복된 인증 로직

		// 유저 정보 가져오는 로직
    return ResponseEntity.ok("유저 정보");
}

@PatchMapping("/me")
public ResponseEntity<String> updateMyInfo(@RequestHeader("Authorization") String token) {
    if (!authService.validateToken(token)) {
        throw new AuthException();
    } // 인증이 필요한 컨트롤러 메소드마다 등장하는 중복된 인증 로직

		// 유저 정보 수정하는 로직
    return ResponseEntity.noContent().build();
}

@DeleteMapping("/me")
public ResponseEntity<String> deleteMyInfo(@RequestHeader("Authorization") String token) {
    if (!authService.validateToken(token)) {
        throw new AuthException();
    } // 인증이 필요한 컨트롤러 메소드마다 등장하는 중복된 인증 로직

		// 유저 정보 제거하는 로직
    return ResponseEntity.noContent().build();
}
  • 위 코드는 단일 컨트롤러 클래스에서 발생한 중복이어서 당장은 큰 문제같아 보이지 않겠지만, 이런 컨트롤러 클래스가 여러개라면? 유지보수하기 매우 힘든 코드이다.

❗인터셉터를 구현해서 해결해보자

이전 코드에서 AuthService 를 통해 토큰을 검증하는 로직과 유효하지 않은 토큰에 대한 응답을 반환하는 부분이 중복되었음을 확인했다. 이 코드를 인터셉터를 사용하여 개선해보기로 하자.

@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final AuthService authService;

    public AuthInterceptor(AuthService authService) {
        this.authService = authService;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");

        if (!authService.validateToken(token)) {
            throw new AuthException();
        }

        return true;
    }
}

인터셉터에 @Component 어노테이션을 달아 빈으로 만들어준 이유는, AuthService 를 주입받기 위함이다.

  • preHandle() 메소드에서 HTTP Header 를 통해 가져온 토큰을 AuthService 의 validateToken() 을 통해 검증하고, 유효하지 않은 토큰이 들어왔다면 예외를 발생시킨다.
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;

    public WebConfig(AuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor);
    }
}
  • WebConfig 는 빈으로 등록된 AuthInterceptor 를 주입받아 인터셉터로 등록한다. 이렇게 인터셉터를 추가하면 아래와 같이 코드가 중복없이 개선된다.

❗완성본

@GetMapping("/me")
public ResponseEntity<String> getMyInfo() {
    // 유저 정보 가져오는 로직
    return ResponseEntity.ok("유저 정보");
}

@PatchMapping("/me")
public ResponseEntity<String> updateMyInfo() {
    // 유저 정보 수정하는 로직
    return ResponseEntity.noContent().build();
}

@DeleteMapping("/me")
public ResponseEntity<String> deleteMyInfo() {
    // 유저 정보 제거하는 로직
    return ResponseEntity.noContent().build();
}
@RestController
@RequestMapping("/items")
public class ItemController {
    @GetMapping
    public ResponseEntity<List<Item>> getAllItems() {
        // 상품 목록 가져오는 로직
        return ResponseEntity.ok(items);
    }
}

📖출처: https://hudi.blog/spring-handler-interceptor/

0개의 댓글