zenn.skin 무료버전 배포중!
자세히보기

Web/Spring

[Spring Web 6.1] DispatcherServlet 뜯어보기 - Constructor & init

koosco! 2024. 10. 4. 18:11

이번에는 init 관련 메서드를 정의해보려 한다. getter, setter와 함께 보려 했는데 init 관련 함수만 해도 생각보다 양이 많아서 나누어 정리해보려 한다.

 

Constructor

public DispatcherServlet() {
    this.setDispatchOptionsRequest(true);
}

public DispatcherServlet(WebApplicationContext webApplicationContext) {
    super(webApplicationContext);
    this.setDispatchOptionsRequest(true);
}

dispatcherServlet은 코드양에 비해 아주 간단한 생성자를 가진다.

wac을 받거나 받지 않는 경우인데, wac을 매개변수로 받게되면 부모인 FrameworkServlet에게 wac을 넘기게 된다.

set관련 부분에 대해서는 getter & setter 부분에서 다루려 한다.

 

onRefresh & initStrategies

protected void onRefresh(ApplicationContext context) {
    this.initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    this.initMultipartResolver(context);              // 파일 업로드 처리
    this.initLocaleResolver(context);                 // 지역화 정보 처리
    this.initThemeResolver(context);                  // 테마 처리
    this.initHandlerMappings(context);                // 요청 URL과 핸들러 매핑
    this.initHandlerAdapters(context);                // 핸들러 실행
    this.initHandlerExceptionResolvers(context);      // 예외 처리 전략
    this.initRequestToViewNameTranslator(context);    // 요청을 뷰 이름으로 변환
    this.initViewResolvers(context);                  // 뷰 리졸버
    this.initFlashMapManager(context);                // 플래시 맵 관리
}

onRefresh 메서드는 DispatcherServlet이 ApplicationContext를 초기화하거나 리프레시할 때 호출된다

initStrategies는 ApplicationContext로부터 DispatcherServlet이 필요로 하는 값들을 초기화시켜주는 메서드이다.

ThemeResolver는 deprecated되었는데 initStrategies에서는 아직 함께 초기화되는 것을 확인할 수 있다.

 

FrameworkServlet

onRefresh 함수가 어디서부터 실행되는지를 알아보기 위해 디버거로 확인해보니 DispatcherServlet이 상속받고 있는 부모 클래스인 FrameworkServlet에서 호출되고 있다. FrameworkServlet은 HttpServletBean을 상속받아 HttpServlet의 기능을 확장하고 ApplicationContext와의 통합을 위한 메서드를 제공한다.

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
	protected final void initServletBean() throws ServletException {
        ServletContext var10000 = this.getServletContext();
        String var10001 = this.getClass().getSimpleName();
        var10000.log("Initializing Spring " + var10001 + " '" + this.getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
        }

        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var4) {
            this.logger.error("Context initialization failed", var4);
            throw var4;
        }

        if (this.logger.isDebugEnabled()) {
            String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data";
            this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
        }

        if (this.logger.isInfoEnabled()) {
            this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }

    }

    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
        WebApplicationContext wac = null;
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }
                    this.configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }

        if (wac == null) {
            wac = this.findWebApplicationContext();
        }

        if (wac == null) {
            wac = this.createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
            synchronized(this.onRefreshMonitor) {
                this.onRefresh(wac);
            }
        }

        if (this.publishContext) {
            String attrName = this.getServletContextAttributeName();
            this.getServletContext().setAttribute(attrName, wac);
        }

        return wac;
    }
}

위 코드에서 wac는 WebApplicationContext를 의미하는데, 아마도 ApplicationContext들 중 Web을 지원하는 컨텍스트들을 말하는 것 같다. 여기서 wac를 dispatcherServlet의 onRefresh 메서드를 호출하면서 넘겨주게 되고 dispatcherServlet의 초기화가 이루어지는 것으로 보인다.

 

DispatcherServlet이 초기화되는 시점?

브레이크 포인트를 찍고 애플리케이션을 실행시켰는데 dispatcherServlet이 초기화되지 않는 것을 확인했다. (??) 혹시나 하는 마음에 서버에 요청을 보내니 첫 요청 시점에서 dispatcherServlet이 초기화되는 것을 알게 되었다. 코드가 변경되고 devtools를 이용하여 reload하면 dispatcherServlet이 다시 초기화되는 것을 확인할 수 있는데 아마도 이런 유연성을 위해 애플리케이션이 실행되는 시점이 아닌 첫 요청이 들어오는 순간에 onRefresh 메서드를 통해 초기화를 진행하는 것 같다.

 

DispatcherServlet 초기화

initStrategies 메서드가 호출되면 dispatcherServlet을 구성하는 요소들의 초기화가 이루어진다. 아래는 그 중 하나인 MultipartResolver 코드이다. 코드를 보면 FrameworkServlet으로부터 받은 wac을 통해 관련 bean들을 찾아서 의존성을 넣어주는 것으로 보인다.

private void initMultipartResolver(ApplicationContext context) {
    try {
        this.multipartResolver = (MultipartResolver)context.getBean("multipartResolver", MultipartResolver.class);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Detected " + this.multipartResolver);
        } else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
        }
    } catch (NoSuchBeanDefinitionException var3) {
        this.multipartResolver = null;
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No MultipartResolver 'multipartResolver' declared");
        }
    }

}

코드를 보면 각종 resolver, handlerMapping, handlerAdapter들이 초기화되는 것을 알 수 있는데 낯선 녀석이 있어 이 녀석만 살펴보려고 한다.

 

FlashMapManager

Spring MVC에서 요청 간 일시적인 데이터를 저장하고 전달하는 데 사용되는 객체이다.
주로 리다이렉트 이후에 데이터(예: 메시지, 폼 데이터)를 전달할 때 사용되며, 한 번 사용된 후에는 자동으로 소멸되는 특성을 가지고 있다.
이를 통해 리다이렉트 후 일회성 데이터를 손쉽게 전달하고, 불필요한 세션 사용을 피할 수 있다.

리다이렉트를 하면서 일시적으로 데이터를 저장하기 위해 사용하는 용도같은데 restcontroller에서도 사용되는지 모르겠다??

아마도 ssr 방식 코드를 작성할 때 데이터를 넘기기 위해 사용되고 api server에서는 사용되지 않는 것 같다.

대략적인 사용법은 다음과 같다.

@Controller
public class LoginController {

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request, HttpServletResponse response) {
        // 로그인 로직 수행 (생략)
        boolean loginSuccess = true;

        if (loginSuccess) {
            // FlashMap에 로그인 성공 메시지 저장
            FlashMap flashMap = new FlashMap();
            flashMap.put("successMessage", "로그인에 성공하였습니다.");
            FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
            flashMapManager.saveOutputFlashMap(flashMap, request, response);

            // 로그인 성공 후 리다이렉트
            return "redirect:/home";
        } else {
            return "login";
        }
    }
}
@Controller
public class HomeController {

    @GetMapping("/home")
    public String home(Model model, HttpServletRequest request) {
        // FlashMap에서 로그인 성공 메시지 가져오기
        FlashMap inputFlashMap = RequestContextUtils.getInputFlashMap(request);
        if (inputFlashMap != null) {
            String successMessage = (String) inputFlashMap.get("successMessage");
            model.addAttribute("message", successMessage);
        }
        return "home";
    }
}

LoginController에서 요청을 처리한 후 리다이렉트하면서 사용할 값들을 FlashMap에 저장해놓으면 리다이렉트된 controller에서 꺼내서 사용하는 식으로 사용하는 것 같다.

'Web/Spring'의 다른글

  • 현재글 [Spring Web 6.1] DispatcherServlet 뜯어보기 - Constructor & init

관련글