네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

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

컨트롤러에서 반복되는 로직을 실행해야하는 경우가 있다. 해당 엔드포인트에 대한 인증(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();
}

혹여 인증이 필요한 엔드포인트에서 인증 로직을 실수로 누락해버린다면, 큰 문제가 발생할 것 이다. 코드에서 중복은 해악이다.

 

Spring Interceptor

우리는 이 문제를 인터셉터(Interceptor)로 해결할 수 있다. 여러 컨트롤러에서 같은 관심사를 갖고 반복되어 사용하는 코드를 제거하고, 다수의 컨트롤러에 동일한 기능을 제공하기 위해 사용하는 것이 인터셉터이다.

 

인터셉터 호출 흐름

스프링의 인터셉터 동작은 크게 '컨트롤러 실행 전'/ '컨트롤러 실행 후, 뷰 실행 전'/ '뷰 실행 후' 이 세단계로 구분된다. 스프링 인터셉터를 만들기 위해서는 HandlerInterceptor 인터페이스를 구현해야 하는데, 해당 인터페이스는 preHandle(), postHandle(), afterCompletion()이라는 세 메서드를 제공한다.

 

Interceptor work with Spring MVC

1. preHandle()

컨트롤러 호출 전에 호출되는 메서드다. preHandle()의 반환타입은 boolean이다. 만약 preHandle()이 false를 반환한다면, 다음 HandlerInterceptor 혹은 컨트롤러를 실행하지 않는다.

 

2. postHandle()

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

 

3. afterCompletion()

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

 

인터셉터 작성

HandlerInterceptor 인터페이스를 구현하는 SomeInterceptor라는 클래스를 작성하여 직접 스프링의 인터셉터를 사용해보자.

public class SomeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 
        return true;
    }
    
    @Override
    public void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { 
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
    }
}

인터셉터 등록

WebMvcConfigurer 인터페이스를 구현하면 Spring MVC의 설정을 할 수 있다고 한다. 

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SomeInterceptor());
    }
}

 

인터셉터를 활용하여 인증 구현하기

이제 인터셉터가 대략 어떤 식으로 동작하는지 파악했다. 우리는 맨 처음의 코드에서 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;
    }
}

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를 주입받아 인터셉터로 등록한다.

 

Interceptor 설정하기

그런데 만약 접근시 인증이 필요없는 엔드포인트가 존재한다면 어떻게 될까? 인터셉터는 현재 모든 컨트롤러 메서드에 대해 동작하고 있다. 특정 엔드포인트만 적용하거나, 제외하고 싶다면 별도의 설정이 필요하다.

 

WebConfig 클래스에서 인터셉터 등록을 위해 InterceptorRegistry의 addInterceptor() 메서드를 실행했다. 이 메서드는 InterceptorRegistation 타입의 객체를 반환하는데, 해당 객체는 addPathPatterns()와 excludePathPatterns()라는 메서드를 제공한다. 이 메서드는 Ant Pattern이라는 경로 패턴을 사용하여 인터셉터를 등록할 경로를 설정해줄 수 있다.

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(authInterceptor)
            .addPathPatterns("/users/*");
}

 

[ Ant Pattern ]

Ant Pattern은 *, **, ? 이 3개의 특수문자를 이용하여 경로를 표현한다. 각 문자는 아래의 의미를 갖는다. 

 

* : 0개 이상의 글자와 매칭

/users/*는 /users/me, /users/123 와는 매칭되지만, /users/123/items와는 매칭이 되지 않는다.

 

** : 0개 이상의 디렉터리 혹은 파일과 매칭

/users/**  /users/me, /users/342, /users/342/favorites 와 모두 매칭된다. 또한 /a/**/z  /a/b/c/d/e/z 와 매칭된다.

 

? : 1개 글자와 매칭

/u?ers 는 /users, /uzers 와 매칭되지만 /ussers와는 매칭되지 않는다.

 

 

 출처 

https://hudi.blog/spring-handler-interceptor/

 

Spring HandlerInterceptor를 활용하여 컨트롤러 중복 코드 제거하기

우아한테크코스 레벨2 마지막 미션인 장바구니 미션에서 인증과 인가를 구현하기 위해, Spring Interceptor 를 사용해야했다. 이를 위해 학습한 내용을 정리해보았다. 컨트롤러에서 발생한 중복 코드

hudi.blog

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !