Spring

필터와 인터셉터

dev-rootable 2023. 7. 28. 10:56

📌 필요성

 

웹 애플리케이션에서 모든 컨트롤러에서 공통으로 관심이 있는 것, 즉 공통으로 적용하고자 하는 로직을 '공통 관심사'라고 한다. 필터와 인터셉터는 모두 컨트롤러 실행 전에 실행되고, 적용 대상도 지정할 수 있다. 

 

대표적인 예시로 로그인을 들 수 있다. 로그인 대상만 진입 가능한 페이지가 있다면 뷰를 호출하는 컨트롤러가 실행되기 전에 인증 여부를 확인해야 한다. 이때, 컨트롤러 앞단에서 필터나 인터셉터를 사용하면 문제를 해결할 수 있다.

 

웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServeltRequest를 제공한다.

 

📌 서블릿 필터(Servlet Filter)

 

🔎 필터 흐름

 

HTTP 요청 ➡ WAS ➡ 필터 ➡ 서블릿 ➡ 컨트롤러

 

서블릿은 Dispatcher Servlet으로 생각하면 된다. 필터는 서블릿 앞단에서 실행되며, 모든 고객의 요청 로그를 남기거나 인증 여부를 확인하는데 활용할 수 있다. 또한, 필터는 특정 URL 패턴에 적용할 수 있어 화이트 및 블랙리스트를 갖고 있다.

 

🔎 특징

 

✔ 필터 제한

 

적절한 요청 : HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
제한 : HTTP 요청 → WAS → 필터 → X

 

필터는 다음 필터를 호출하지 않고 리턴하게 되면 적절하지 않은 요청이라 판단하고 서블릿을 호출하지 않는다.

 

✔ 필터 체인

 

HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러

 

필터는 체인으로 구성되며, 중간에 필터를 자유롭게 추가할 수 있다.

 

🔎 필터 인터페이스

 

필터 인터페이스

 

Filter 인터페이스를 구현하고 설정 클래스에서 빈으로 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.

 

  • init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter() : 고객의 요청이 올 때마다 해당 메서드가 호출된다. 여기에 필터의 로직을 구현하면 된다. (필수 구현 메서드)
  • destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

 

📌 필터 활용

 

🔎 LogFilter - 모든 요청 로그를 남기는 필터

 

@Slf4j
public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("log filter init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request; //다운캐스팅
        String requestURI = httpRequest.getRequestURI();

        String uuid = UUID.randomUUID().toString(); //요청 구분용

        try {
            log.info("REQUEST [{}][{}]", uuid, requestURI);
            chain.doFilter(request, response); //다음 필터 호출(필수)
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    }

    @Override
    public void destroy() {
        log.info("log filter destroy");
    }

}

 

위 필터에서 가장 중요한 코드는 "chain.doFilter"다. 해당 명령이 없으면 필터는 서블릿을 호출하지 않고 흐름이 필터 단계에서 종료된다.

 

필터를 사용하기 위해서는 빈으로 등록해야 한다. 빈을 등록하려면 @Configuration이 선언된 설정 클래스가 필요하고, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록하면 된다.

 

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

}

 

  • setFilter(등록할 필터) : 필터 등록
  • setOrder(index) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을수록 먼저 동작한다.
  • addUrlPatterns(URL) : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.

 

(참고)
@ServletComponentScan @WebFilter(filterName = "logFilter", urlPatterns = "/*"로 필터 등록이 가능하지만 필터 순서 조절이 안된다. 따라서 FilterRegistrationBean을 사용하자

 

서블릿 컨테이너 부팅 후 init() 호출

 

LogFilter 동작

 

WAS를 내리면 서블릿 컨테이너도 내려가므로 destroy() 호출

 

각 컨트롤러마다 LogFilter가 동작했음을 알 수 있다. 테스트 흐름은 다음과 같다.

 

[/] ➡ 홈 화면 진입

[/login] ➡ 로그인 화면 진입

[/login] + login? Member(회원 정보) ➡ 로그인 성공

[/] ➡ 로그인 성공 후 홈 화면으로 리다이렉트

[/items] ➡ 상품 관리 화면 이동 요청

 

🔎 LoginCheckFilter - 인증 여부 확인

 

요구 사항 : 미인증 사용자는 "/items"(상품 관리) 화면에 진입하지 못하도록 한다.

 

웹 애플리케이션 운영을 위해 홈, 회원 가입, 로그인 화면, css 같은 리소스에는 인증과 무관하게 항상 허용하고, 나머지는 필터가 적용되도록 한다.

 

사용자가 상품 관리 화면으로 바로 진입할 경우 인증 여부를 확인해야 한다. 하지만 로그인 성공 후 리다이렉션으로 홈 화면으로 보내버리면 사용자 입장에서는 직전 화면으로 다시 이동해야 하는 번거로움이 발생한다. 따라서, 리다이렉션 직전 화면에 대한 URL을 쿼리 파라미터에 담아 직전 화면으로 복귀할 수 있도록 한다.

 

💡 로그인 전 직전 화면으로 복귀

1. 필터에서 쿼리 파라미터로 직전 화면의 URL 담기
2. 컨트롤러에서 @RequestParam으로 값을 찾아 redirect 명령에 포함
3. 템플릿에서 폼의 th:action 값이 비어 있으면 POST 요청 후 현재 페이지로 GET 요청이 간다. (Post ➡ Redirect ➡ GET)

 

@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request; //다운캐스팅
        String requestURI = httpRequest.getRequestURI();

        HttpServletResponse httpResponse = (HttpServletResponse) response; //다운캐스팅

        try {
            log.info("인증 체크 필터 시작 {}", requestURI);

            if (isLoginCheckPath(requestURI)) { //화이트 리스트가 아니므로 인증이 필요한 상황
                log.info("인증 체크 로직 실행 {}", requestURI);
                HttpSession session = httpRequest.getSession(false);
                if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                    log.info("미인증 사용자 요청 {}", requestURI);
                    //로그인으로 redirect
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI); //로그인 직전 머물렀던 화면으로
                    return; //미인증 사용자는 다음으로 진행하지 않고 종료
                }
            }

            chain.doFilter(request, response); //다음 필터 호출
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("인증 체크 필터 종료 {}", requestURI);
        }
    }

    /**
     * 화이트 리스트의 경우 인증 체크X
     */
    private boolean isLoginCheckPath(String requestURI) {
        //true(not whitelist), false(whitelist)
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }

}

 

whitelist라는 배열을 선언하여 필터를 적용하지 않을 경로 목록을 선언했다.

 

doFilter()는 인증 체크 로직이며, whitelist에 포함되지 않는 requestURI(요청 URI)가 오면 세션에 값이 존재하는지 확인하여 인증 여부를 확인한다. 만약, 미인증 사용자라면 로그인 화면에 머무르도록 한 후 return으로 필터를 더는 진행하지 않는다.

 

인증 사용자일 경우, chain.doFilter()를 통해 다음 필터를 호출한다.

 

LogFilter와 마찬가지로 WebConfig에 등록한다.

 

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

}

 

이제 모든 컨트롤러가 실행되기 전에 인증 여부를 확인한다. 아래 컨트롤러는 @RequestParam을 통해 로그인 요청 후 인증 사용자라면 직전 페이지로 리다이렉트 한다.

 

    @PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
                        @RequestParam(defaultValue = "/") String redirectURL, HttpServletRequest request) {

        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
        log.info("login? {}", loginMember);

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //로그인 성공 처리

        //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

        //redirectURL 적용
        return "redirect:" + redirectURL;

    }

 

미인증 사용자 필터링

 

위 결과를 보면 미인증 사용자의 상품 관리 화면(/items) 진입이 오자 필터를 종료하고 로그인 화면으로 리다이렉트 했음을 알 수 있다.

 

인증 사용자 화면 복귀

 

위 결과는 인증 사용자의 경우다. /login에 대한 POST 요청 후 컨트롤러가 등장했으므로, 인증 사용자 케이스이다. 그리고 밑에 직전 화면이었던 상품 관리(/items) 페이지로 리다이렉트 한 것을 알 수 있다.

 

📌 인터셉터(Interceptor)

 

필터와 마찬가지로 웹과 관련된 공통 관심사를 처리할 수 있는 기술이다. 차이점은 적용되는 순서와 범위, 그리고 사용 방법이 다르다. 또한, 서블릿 필터는 서블릿이 제공하는 기술이라면 인터셉터는 스프링 MVC가 제공하는 기술이다.

 

🔎 인터셉터 흐름

 

HTTP 요청 ➡ WAS ➡ 필터 ➡ 서블릿 ➡ 스프링 인터셉터 ➡ 컨트롤러

 

스프링 MVC의 시작점은 Dispatcher Servlet이다. 그래서 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.

 

스프링 인터셉터도 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과 달리 설정 클래스에서 적용할 범위와 적용하지 않을 범위를 동시에 설정할 수 있어 더욱 정교한 설정이 가능하다.

 

🔎 인터셉터 제한

 

정상 요청 : HTTP 요청 ➡ WAS ➡ 필터 ➡ 서블릿 ➡ 스프링 인터셉터 ➡ 컨트롤러
제한 : HTTP 요청 ➡ WAS ➡ 필터 ➡ 서블릿 ➡ 스프링 인터셉터 ➡ X

 

스프링 인터셉터도 마찬가지로 적절하지 않은 요청이라 판단되면 컨트롤러로 요청을 보내지 않는다. 인터셉터는 컨트롤러 호출 전에 실행되는 boolean 타입의 preHandle이라는 메서드를 지원하는데, 이것을 false로 리턴하면 더 이상 흐름이 진행되지 않는다.

 

🔎 인터셉터 체인

 

HTTP 요청 ➡ WAS ➡ 필터 ➡ 서블릿 ➡ 인터셉터1 ➡ 인터셉터2 ➡ 컨트롤러

 

체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있다.

 

🔎 인터셉터 인터페이스

 

public interface HandlerInterceptor {
    
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                              Object handler) throws Exception {}
                              
    default void postHandle(HttpServletRequest request, HttpServletResponse response,
                            Object handler, @Nullable ModelAndView modelAndView) throws Exception {}

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                 Object handler, @Nullable Exception ex) throws Exception {}
                                 
}

 

스프링의 인터셉터를 사용하기 위해서 HandlerInterceptor를 구현해야 한다.

 

인터셉터는 단계적으로 잘 세분화된 총 3가지 메서드를 제공한다. 컨트롤러 호출 전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion) 등이다. 또한, 모든 메서드의 파라미터에 handler가 있다. 이것은 컨트롤러 정보를 알 수 있다는 것이고, postHandle의 경우 ModelAndView 정보도 알 수 있다.

 

🔎 인터셉터 호출 흐름

 

인터셉터 호출 흐름

 

1. preHandle : 컨트롤러 호출 전에 호출된다. 더 정확히는 핸들러 어뎁터 호출 전에 호출된다.

 - preHandle의 응답이 true이면 핸들러(컨트롤러)로 진행하고, false면 나머지 인터셉터는 물론이고 핸들러 어뎁터도 호출하지 않는다.

 

2. postHandle : 컨트롤러 호출 후에 호출된다. 더 정확히는 핸들러 어뎁터 호출 후에 호출된다.

 

3. afterCompletion : 뷰가 렌더링 된 이후에 호출된다.

 

🔎 인터셉터 예외 상황

 

인터셉터 예외 상황

 

1. preHandle : 컨트롤러 호출 전에 호출된다.

 

2. postHandle : 컨트롤러에서 예외가 발생하면 호출되지 않는다.

 

3. afterCompletion : 항상 호출된다.

 - 이 경우, 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.

 - 그래서 예외와 무관하게 공통 처리를 하고 싶을 때 사용한다.

 

📌 인터셉터 활용

 

🔎 LogInterceptor - 모든 요청 로그를 남기는 인터셉터

 

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {

        String requestURI = request.getRequestURI();

        String uuid = UUID.randomUUID().toString();
        request.setAttribute(LOG_ID, uuid);

        //@RequestMapping: HandlerMethod
        //정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true; //false 진행X

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle [{}]", modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        String logId = (String) request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}]", logId, requestURI);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

 

컨트롤러 호출 전(preHandle)에 현재 사용자의 URI 정보를 로그의 식별자인 UUID, 그리고 컨트롤러 정보와 함께 출력한다.

 

위 코드를 보면 요청마다 남기는 UUID 식별자를 preHandle과 afterCompletion에서 공유하고 있다. 서블릿 필터라면 지역 변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있어 이를 보관하기 위해 HttpServletRequest를 사용했다.

 

종료 로그를 afterCompletion에서 실행한 이유는 예외 상황에서도 호출되기 때문이다.

 

HandlerInterceptor 구현 메서드 사이에서 HttpServletRequest을 통해 값 전달이 가능하다

 

✔ HandlerMethod

 

핸들러 정보는 어떤 핸들러 매핑을 사용했는지에 따라 달라진다. 스프링을 사용하면 일반적으로 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod가 넘어온다.

 

✔ ResourceHttpRequestHandler

 

@Controller가 아니라 /resources/static와 같은 정적 리소스가 호출되는 경우 ResourceHttpRequestHandler가 핸들러 정보로 넘어온다.

 

위 코드에서는 핸들러 매핑 정보를 매핑한 후 별도 로직이 없지만, HandlerMethod를 얻은 후, 아래와 같은 다양한 기능들을 지원한다.

 

HandlerMethod가 지원하는 메서드 목록

 

핸들러 매핑 타입을 알아야 호출할 컨트롤러의 모든 정보를 조회할 수 있다.

 

필터와 달리 addInterceptors라는 WebMvcConfigurer 인터페이스의 오버라이드 메서드를 통해 등록해야 한다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }

}

 

  • addInterceptor ➡ 인터셉터 등록
  • order ➡ 인터셉터 호출 순서(낮을수록 먼저 호출됨)
  • addPathPatterns ➡ 인터셉터를 적용할 URL 패턴 지정
  • excludePathPatterns ➡ 인터셉터에서 제외할 URL 패턴 지정(whitelist)

 

💡 /* vs /**

'*' 는 경로 안의 모든 문자가 일치해야 한다.
ex) /resources/*.png ➡ /resources/ 이하의 확장자가 png인 모든 Path

'**' 는 하위 경로 모든 문자가 일치해야 한다.
ex) /resources/** ➡ /resources/ 이하의 모든 디렉터리 및 확장자

 

 

미인증 사용자 요청

 

위 결과는 미인증 사용자가 바로 상품 관리 페이지(/items)로 접근했을 때 결과다. 현재 LoginCheckFilter를 통해 인증 체크를 하고 있으며, 인터셉터는 요청 및 응답 로그를 남기는 역할로 세팅했다.

 

여기서 알 수 있는 것은 앞 챕터 내용처럼 필터는 미인증 사용자 요청을 적절히 제한했다는 것과 인터셉터는 앞에서 본 흐름대로 필터 이후에 수행되었다는 것이다. 세부적으로 preHandle ➡ postHandle ➡ afterCompletion 순서로 로그가 찍혔으며, postHandle에서 ModelAndView 정보를 볼 수 있다.

 

로그인 성공

 

LogInterceptor에서 인증 사용자로 판단되어 로그인 정보가 나온 후 세션 ID가 생성된 것을 볼 수 있다.

 

로그인 직전 페이지였던 /items로 GET 요청

 

인증을 마친 후 상품 관리 페이지(/items)로 GET 요청이 수행되어 해당 정보가 Filter와 Interceptor에서 확인할 수 있다.

 

🔎 LoginCheckInterceptor - 인증 여부 확인

 

서블릿 필터에서 사용했던 인증 체크 기능을 인터셉터로 개발해 보자

 

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();

        log.info("인증 체크 인터셉터 실행 {}", requestURI);
        HttpSession session = request.getSession();

        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            //로그인으로 redirect
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;

    }

}

 

인증 체크는 컨트롤러 호출 이전에 수행해야 의미가 있으므로, preHandle만 수행한다. 그리고 return false를 하게 되면 핸들러 어뎁터부터 이후 단계는 모두 수행되지 않고 종료된다.

 

필터와 차이점은 코드가 간단해졌다는 것이다. 이렇게 될 수 있었던 이유는 다음과 같다.

 

  • HttpServletRequest 제공으로 다운캐스팅을 하지 않아도 된다.
  • 등록 클래스에서 설정을 완료했으므로, 화이트리스트를 별도로 선언하지 않아도 된다.
  • 화이트리스트가 없으므로 해당 클래스에서 매칭 유틸(PatternMatchUtils)을 사용할 필요가 없어졌다.

 

마찬가지로 WebConfig에 등록해야 한다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/", "/members/add", "/login", "/logout",
                        "/css/**", "/*.ico", "/error"
                );
    }

}

 

테스트는 기능의 중복을 막기 위해 LogInterceptor와 LoginCheckInterceptor만 동작하도록 했다.

 

미인증 사용자 요청

 

위 결과는 미인증 사용자가 바로 상품 관리 페이지(/items)로 접근했을 때 결과다. LogInterceptor의 로그를 보면 afterCompletion의 로그가 남겨져 있다. 결과적으로 필터처럼 미인증 사용자 요청을 인지하고 그대로 종료한 것이다.

 

인증 완료 후 /items GET 요청

 

LogInterceptor의 preHandle 부분이 우선 출력되었다.

 

ModelAndView의 내용을 보면 로그인 직전 페이지였던 상품 관리 페이지(/items)로 잘 리다이렉트 된 것을 확인할 수 있다. 또한, 로그인 후에 상품 관리 페이지로 리다이렉트 될 때 컨트롤러가 호출된 것이므로, 여기서도 LoginCheckInterceptor가 동작하여 인증 체크를 수행하는 것을 볼 수 있다.

 

인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다. 또한, 서블릿 필터보다 더욱 정교한 설정이 가능하기 때문에 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.

 

ArgumentResolver를 통해서 세션에 값이 있는지 여부를 확인해 주는 커스텀 애노테이션을 생성하는 방법도 있다. 이 방법은 아래 글의 예시 부분을 참고하자

https://dev-rootable.tistory.com/79
 

요청 매핑 핸들러 어뎁터 구조

📌 개요 스프링 MVC 구조에서 요청이 들어오면 가장 먼저 Dispatcher Servlet에서 요청과 매핑되는 핸들러를 찾는다. 이 핸들러를 실행하는 주체는 HandlerAdapter다. 그래서 Dispatcher Servlet은 매핑된 핸들

dev-rootable.tistory.com

 

Reference:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com