woniper

Spring-MVC 읽기 #3. Spring-MVC의 시작 본문

Spring

Spring-MVC 읽기 #3. Spring-MVC의 시작

woniper1 2018. 12. 23. 15:00

주의) 저도 처음 코드를 읽으며 작성하는 글이기 때문에 어렵게 전달되거나, 틀린 부분은 언제든 피드백을 해주세요.

이번 글은 Spring-MVC의 시작에 대해 알아볼 것이다.

public static void main(String... args) {}

java 개발자라면 위 코드는 익숙하다. java에서 main 메소드는 애플리케이션의 최초 시작점이다. 그런데 Spring-MVC로 개발한 웹 애플리케이션을 war로 빌드 후 Web Application Server(이하 WAS)로 실행하는 경우엔 main 메소드가 최초 시작점이 아닌 것을 알 수 있다. WAS 실행이 최초 시작점이라고 볼 수도 있겠다.

Spring Boot를 사용하면 main 메소드가 최초 시작점이다. 이 글은 Spring Boot를 이야기하고자 하는 게 아니다.

ServletContainerInitializer

그렇다면 WAS는 어떻게 내가 만든 웹 애플리케이션을 실행할까? 바로 ServletContainerInitializer을 실행한다. 이 인터페이스는 Spring-MVC에 정의된 인터페이스가 아니다. servlet 3.0에 정의된 인터페이스다.

이 인터페이스는 단순하다.

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

onStartup 메소드가 하나가 존재한다.

그렇다면 나는 어떻게 WAS가 ServletContainerInitializer를 실행하는 걸 알았을까?

onStartup 이라는 메소드명이 왠지 시작하게 생겨서 일까? 당연히 아니다. 오픈소스 코드를 좀 더 쉽게 읽기 위해서는 시작점이 어딘지부터 찾아보는 게 좋다. 아마 한 번씩 궁금했을 텐데(나만 그런가??) Spring으로 만든 웹 애플리케이션은 어떻게 실행될까?

그럼 시작점이 ServletContainerInitializer이라는 건 어떻게 알았을까? 구글링! 구글 없이 개발할 수 있을까? 나는 자신 없다. 모르면 구글에 물어보자. ServletContainerInitializer 인터페이스의 역할을 찾아보기 위해 검색 결과의 글을 참고하자.


ServletContainerInitializer의 구조

그럼 내가 궁금했던 게 해결됐다. 일단 ServletContainerInitializer가 실행되는 건 알았는데 인터페이스뿐이다. 내가 보고 싶은 건 구현체의 코드가 보고 싶을 뿐이다. intellij의 기능을 이용해 하위 클래스 구조를 찾아보자.



ServletContainerInitializer의 구현체를 찾아보니 몇 개의 클래스들이 보인다. 구현체들이 많은데 어떻게 하나씩 다 볼 수 있을까? 그럴 수 없다. 내가 보고 싶은 구현체는 단지 Spring 웹 애플리케이션을 실행하는 구현체가 보고 싶을 뿐이다. 하위 클래스를 살펴보니 Spring 클래스가 하나 눈에 띈다. 바로 SpringServletContainerInitializer


하위 클래스가 너무 많아 어떤 클래스부터 봐야 할 지 모르겠다면, 문서나 주석을 읽어보자. Spring은 문서뿐 아니라 주석도 친절히 작성되어 있다. 코드를 읽어보지 않아도 클래스가 어떤 책임을 갖고 있는지 알 수 있다.

SpringServletContainerInitializer

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<>();

        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                                ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}
  1. 파라미터로 받은 Set<Class<?>> 을 반복해서 WebApplicationInitializer로 생성(newInstance)해 initializers list에 담는다.
  2. initializers list를 정렬해 WebApplicationInitializer#onStartup 메소드를 실행한다.

전체적인 구조를 먼저 파악하기 위해 WebApplicationInitializer가 어떤 역할을 하는 인터페이스인지부터 찾아보자.

WebApplicationInitializer

다시 인터페이스가 등장했다. 구현체를 다시 살펴보자.



Spring5에 Reactive가 추가됐다. 나는 Reactive를 당장 볼 생각은 없다.

내가 봐야 할 클래스는 AbstractContextLoaderInitializer와 그 아래 클래스들이다. 그런데 prefix로 My가 붙은 클래스는 뭘까? 코드를 따라가 보니 테스트 코드에서 사용될 mock 클래스다. 테스트 코드에 사용된 mock 클래스를 읽어볼 필요가 있을까? 좋은 방법이다. 테스트 코드를 살펴보는 건 매우 좋은 방법이다. 실행을 해봐도 좋고, 내가 추가로 테스트 코드를 작성해보며 클래스를 학습해 봐도 좋다.

AbstractContextLoaderInitializer

public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

    protected final Log logger = LogFactory.getLog(getClass());

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerContextLoaderListener(servletContext);
    }

    protected void registerContextLoaderListener(ServletContext servletContext) {
        WebApplicationContext rootAppContext = createRootApplicationContext();
        if (rootAppContext != null) {
            ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
            listener.setContextInitializers(getRootApplicationContextInitializers());
            servletContext.addListener(listener);
        }
        else {
            logger.debug("No ContextLoaderListener registered, as " +
                    "createRootApplicationContext() did not return an application context");
        }
    }

    @Nullable
    protected abstract WebApplicationContext createRootApplicationContext();

    @Nullable
    protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
        return null;
    }

}
  1. AbstractContextLoaderInitializer 클래스는 추상 클래스다.
  2. onStartup 메소드는 registerContextLoaderListener 메소드를 호출한다.
  3. registerContextLoaderListener 메소드는 WebApplicationContext(rootAppContext)를 생성한다.
  4. ContextLoaderListener를 생성해 setContextInitializers 메소드를 호출한다.
  5. ServletContext#addListene 메소드에 ContextLoaderListener를 추가한다.

ContextLoaderListener가 무엇인지는 나중으로 미루고, WebApplicationContext를 생성하는 createRootApplicationContext 메소드를 먼저 보자. WebApplicationContext 인터페이스의 구현체를 살펴보자. 그런데 WebApplicationContext를 생성하는 createRootApplicationContext 메소드는 추상 메소드다. 그렇다면 하위 클래스에서 createRootApplicationContext 메소드를 정의하고 있지 않을까?

AbstractDispatcherServletInitializer

구조를 살펴봤을 때 AbstractContextLoaderInitializer의 하위 클래스는 AbstractDispatcherServletInitializer 클래스다. 이 클래스 역시 이름을 보니 추상 클래스인 거 같다.

public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

    public static final String DEFAULT_SERVLET_NAME = "dispatcher";


    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        registerDispatcherServlet(servletContext);
    }
    
    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        Assert.hasLength(servletName, "getServletName() must not return null or empty");

        WebApplicationContext servletAppContext = createServletApplicationContext();
        Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

        FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
        Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
        dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

        ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
        if (registration == null) {
            throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
                    "Check if there is another servlet registered under the same name.");
        }

        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());
        registration.setAsyncSupported(isAsyncSupported());

        Filter[] filters = getServletFilters();
        if (!ObjectUtils.isEmpty(filters)) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }

        customizeRegistration(registration);
    }

    protected abstract WebApplicationContext createServletApplicationContext();

    protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
        return new DispatcherServlet(servletAppContext);
    }

    // ... 중략 ...

}
  1. onStartup 메소드는 registerDispatcherServlet 메소드를 호출한다.
  2. AbstractDispatcherServletInitializer 클래스 역시 createRootApplicationContext 메소드를 재정의하고 있지 않다.
  3. registerDispatcherServlet 메소드를 보니 중간에 createServletApplicationContextcreateDispatcherServlet 메소드를 호출한다. ApplicationContext와 DispatcherServlet을 생성하는 메소드인 거 같다.
  4. createServletApplicationContext 메소드는 추상 메소드다.
  5. 다시 createServletApplicationContextcreateRootApplicationContext의 구현 메소드를 찾아보자.

AbstractAnnotationConfigDispatcherServletInitializer

createServletApplicationContextcreateRootApplicationContext 메소드 구현체를 찾기 위해 AbstractDispatcherServletInitializer 클래스의 하위 클래스인 AbstractAnnotationConfigDispatcherServletInitializer를 봐야 한다. WebApplicationInitializer 인터페이스의 마지막 구현체다.

public abstract class AbstractAnnotationConfigDispatcherServletInitializer
        extends AbstractDispatcherServletInitializer {

    @Override
    @Nullable
    protected WebApplicationContext createRootApplicationContext() {
        Class<?>[] configClasses = getRootConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
            context.register(configClasses);
            return context;
        }
        else {
            return null;
        }
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        Class<?>[] configClasses = getServletConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            context.register(configClasses);
        }
        return context;
    }

    @Nullable
    protected abstract Class<?>[] getRootConfigClasses();

    @Nullable
    protected abstract Class<?>[] getServletConfigClasses();

}
  1. AbstractAnnotationConfigDispatcherServletInitializer 클래스에서 createServletApplicationContextcreateRootApplicationContext 메소드를 구현하고 있다.
  2. createServletApplicationContextcreateRootApplicationContext 메소드 모두 AnnotationConfigWebApplicationContext 클래스를 생성한다.
  3. 두개의 ApplicationContext를 생성하는 것을 알 수 있다. 하나의 애플리케이션에 ApplicationContext를 1개 이상 생성이 가능하다.
  4. AnnotationConfigWebApplicationContext#register 메소드를 호출해 애플리케이션의 Bean을 초기화한다.

이 클래스를 이해하기 위해서는 Spring의 BeanFactoryApplicationContext를 이해해야 하는데, 이 글에서는 설명하지 않고 여기를 참고하자.

Abstract Class

지금까지 살펴본 SpringServletContainerInitializer 구현체는 모두 추상 클래스다. 다시 SpringServletContainerInitializer로 돌아가서 onStartup 메소드를 다시 보자.

@Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<>();

        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                                ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        // ... 중략 ...
    }
  • webAppInitializerClasses를 반복해서 WebApplicationInitializer를 생성한다.
  • 그런데 WebApplicationInitializer를 생성하기 위한 조건이 있다.
  • if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass))
    • waiClass가 인터페이스가 아니고, 추상 클래스도 아닌 경우에만 WebApplicationInitializer를 생성한다.
  • 지금까지 본 하위 클래스는 모두 추상 클래스다.

JavaConfig로 WEB 애플리케이션 설정

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { AppConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/*" };
    }
}

WebConfig 클래스는 JavaConfig를 사용해 웹 애플리케이션을 설정한 클래스다. Spring에서 제공되는 클래스가 아닌 내가 작성한 코드다. 아마 JavaConfig를 사용해본 개발자라면 익숙한 코드일 텐데, AbstractAnnotationConfigDispatcherServletInitializer 클래스를 상속하고 있다. (또는 WebApplicationInitializer 인터페이스를 구현해도 된다.)

  1. SpringServletContainerInitializer#onStartup 메소드에서 WebApplicationInitializer 생성하기 위한 조건은 인터페이스가 아니고, 추상 클래스도 아니다. 일반 클래스다.
  2. WebConfig 클래스는 일반 클래스다.
  3. 때문에 SpringServletContainerInitializer#onStartup 메소드에서 생성되는 WebApplicationInitializer의 대상은 WebConfig이다.

요약

  1. Spring-MVC에서 서블릿 기반의 웹 애플리케이션 설정을 담당하는 구현체는 ServletContainerInitializer 인터페이스를 구현한 SpringServletContainerInitializer 클래스다.
  2. SpringServletContainerInitializer#onStartup 메소드는 WebApplicationInitializer 인터페이스를 상속한 (인터페이스, 추상 클래스가 아닌)일반 클래스를 생성한다.
  3. WebApplicationInitializer 인터페이스의 구현체는 AbstractContextLoaderInitializer, AbstractDispatcherServletInitializer, AbstractAnnotationConfigDispatcherServletInitializer 추상 클래스다.
  4. 웹 애플리케이션 설정을 위해 WebApplicationInitializer 인터페이스를 상속한 JavaConfig(WebConfig) 클래스를 만들어야 한다.
  5. SpringServletContainerInitializer#onStartup 메소드에서 생성되는 WebApplicationInitializer 인터페이스의 구현체는 우리가 만든 WebConfig 구현체로 생성된다.

tips

오픈소스. 어떻게 읽을까?

앞서 시작점부터 읽는 게 좀 더 쉽게 읽는 방법이라고 했다. 더 쉽게 읽기 위한 또 다른 방법이 있다. 바로 궁금한 것부터 찾아보는 것이다. 예를 들면, Spring-MVC는 @Controller 애노테이션이 설정된 클래스를 어떻게 찾아서 실행할까?로 시작하는 것이다. 오랜시간 유지돼 온 오픈소스는 그만큼 코드 양도 방대하기 때문에 코드를 보는 자신만의 기준 없이 무턱대고 읽기 시작하면 구조를 파악하기 힘들다.

이건 내가 제시하는 방법이고, 읽다 보면 자신만의 읽는 방법과 요령이 생긴다.

intellij Diagram


intellij는 module, class 단위로 Diagram 기능을 이용해 의존성을 한눈에 볼 수 있다. 위 다이어그램은 앞서 살펴본 인터페이스와 구현체들의 의존 관계다.

 


Diagram을 보고 싶은 클래스에서 오른쪽 마우스 클릭 > Diagram 선택



그럼 이렇게 클래스 Diagram이 그려진다.


 

WebApplicationInitializer와 AbstractContextLoaderInitializer의 의존 관계를 보기 위해 클래스를 추가한다. 

오른쪽 마우스 클릭 > Add Class to Diagram...



끝!

마무리

인터페이스부터 인터페이스의 구현체를 찾아가며 구조만 살펴봤다. 몇 가지 선수 지식이 필요하고, 약간은 추상적인 설명이 많았다. 아직까진 모든 코드를 이해하지 못하더라도 대략적인 구조만 이해해도 좋다. 다음 글에서는 AbstractContextLoaderInitializer, AbstractDispatcherServletInitializer, AbstractAnnotationConfigDispatcherServletInitializer를 하나씩 자세히 읽어볼 예정이다.

Comments