여러 컨트롤러에서 같은 관심사를 갖고 반복되어 사용하는 코드를 제거하고, 다수의 컨트롤러에 동일한 기능을 제공하기 위해 사용하는 것이 인터셉터이다. 인터셉터는 컨트롤러 로직이 실행되기 전에 실행된다.
스프링의 인터셉터의 동작은 크게 '컨트롤러 실행 전', '컨트롤러 실행 후, 뷰 실행 전', '뷰 실행 후' 이 세 단계로 구분된다.
preHandle()
컨트롤러 호출 전에 호출되는 메소드이다. preHandle()의 반환타입은 boolean 이다. 만약 preHandle() 이 false 를 반환한다면, 다음 HandlerInterceptor 혹은 컨트롤러를 실행하지 않는다.
postHandle()
컨트롤러가 정상적으로 실행된 이후에 실행되는 메소드이다. 컨트롤러에서 예외가 발생한다면, postHandle() 메소드는 실행되지 않는다.
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 를 주입받기 위함이다.
@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);
}
}
@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);
}
}