Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

외장 WAS 사용 웹 애플리케이션 셋팅 (without springboot) 본문

Spring/Spring boot - 핵심 원리와 활용

외장 WAS 사용 웹 애플리케이션 셋팅 (without springboot)

studyHub 2025. 1. 25. 00:29

외장 톰캣에 웹 애플리케이션을 배포하는 방법과 springboot 없이 웹 애플리케이션을 셋팅하는 방법, 즉 서블릿 컨테이너를 초기화해 디스패처 서블릿을 만들고 스프링 컨테이너와 연결하는 방법을 알아보자.


웹 서버와 스프링 부트 소개

외장 서버 VS 내장 서버

전통적인 방식

과거에는 웹 애플리케이션을 개발할 때 서버에 톰캣 같은 WAS를 설치했다. 그리고 war 파일로 코드를 빌드해 WAS에 전달해서 배포하는 방식을 따랐다.

 

최근 방식

최근에는 스프링 부트가 내장 톰캣을 포함하고 있다. 애플리케이션 코드 안에 톰캣 같은 WAS가 라이브러리로 내장되어 있다. 코드를 JAR로 빌드한 다음에 실행하기만 하면 WAS도 함께 실행된다. 개발자는 main() 메서드만 실행하면 되고, WAS 설치나 IDE 같은 개발 환경에서 WAS와 연동하는 복잡한 일은 수행하지 않아도 된다.

 

그런데 스프링 부트는 어떤 원리로 내장 톰캣을 사용해서 실행할 수 있는 것일까?

 


전통적인 방식으로 사용해보기

JAR, WAR 소개

JAR

클래스와 관련 리소스를 압축한 단순한 파일. 자바는 여러 클래스와 리소스를 묶어서 JAR (Java Archive)라고 하는 압축 파일을 만들 수 있다. 이 파일을 직접 실행할 수도 있고, 다른 곳에서 라이브러리로 사용할 수도 있다.

 

WAR

WAR(Web Application Archive)라는 이름에서 알 수 있듯 WAR 파일은 웹 애플리케이션 서버(WAS)에 배포할 때 사용하는 파일이다. JAR 파일이 JVM 위에서 실행된다면, WAR는 웹 애플리케이션 서버 위에서 실행된다. 웹 애플리케이션 서버 위에서 실행되고, HTML 같은 정적 리소스와 클래스 파일을 모두 함께 포함하기 때문에 JAR와 비교해서 구조가 더 복잡하다.

 

WAR 구조

WEB-INF 폴더 하위는 자바 클래스와 라이브러리, 그리고 설정 정보가 들어가는 곳이다.

WEB-INF 를 제외한 나머지 영역은 HTML, CSS 같은 정적 리소스가 사용되는 영역이다

  • WEB-INF
    • classes : 실행 클래스 모음
    • lib : 라이브러리 모음
    • web.xml : 웹 서버 배치 설정 파일(생략 가능)
  • index.html : 정적 리소스

WAR 배포

  1. 톰캣 폴더 /webapps 하위를 모두 삭제 후, 빌드된 war 파일을 복사한다. (파일명 ROOT.war)
  2. 톰캣 서버를 실행한다.

실제 서버에서는 이렇게 사용하면 되지만, 개발 단계에서는 war 파일을 만들고, 이것을 매번 서버에 복사해서 배포하는 과정이 번거롭다. 인텔리J 같은 IDE는 이 부분을 편리하게 자동화해준다.


서블릿 컨테이너 초기화

WAS를 실행하는 시점에 필요한 초기화 작업들이 있다.

톰캣은 서블릿 스펙에 맞춰 동작하도록 설계되어 있다.  서비스에 필요한 필터와 서블릿을 등록하고, 여기에 스프링을 사용한다면 스프링 컨테이너를 만들고, 서블릿과 스프링을 연결하는 디스패처 서블릿도 등록해야 한다.

WAS가 제공하는 초기화 기능을 사용하면, WAS 실행 시점에 이러한 초기화 과정을 진행할 수 있다.

 

지금부터 서블릿 컨테이너의 초기화 기능을 알아보고 이어서 이 초기화 기능을 활용해 스프링 만들고 연결해보자.

(web.xml 을 사용해서 초기화할 수도 있지만, 서블릿 스펙에서 자바 코드를 사용한 초기화 방식으로 진행한다)

 

 

서블릿 컨테이너 초기화 과정 v1

1.  ServletContainerInitializer 구현

서블릿은 ServletContainerInitializer 라는 서블릿 컨테이너 초기화 인터페이스를 제공한다. 서블릿 컨테이너는 실행 시점에 초기화 메서드인 onStartup() 을 호출해준다. 여기서 애플리케이션에 필요한 기능들을 초기화 하거나 등록할 수 있다.

public class MyContainerInitV1 implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        //서블릿 컨테이너 초기화 코드
    }
}

 

2. 초기화 클래스를 패키지 경로 지정

ServletContainerInitializer를 구현하고,  WAS에게 실행할 초기화 클래스를 알려주기 위해 다음 경로에 파일을 생성한다.

이 파일에 초기화 클래스를 패키지 경로를 포함해서 지정해준다.

resources/META-INF/services/jakarta.servlet.ServletContainerInitializer

 

이러한 서블릿 컨테이너 초기화 과정에서 서블릿을 등록할 수도 있지만, 필터/서블릿/스프링 컨테이너 등을 등록하려 초기화 클래스가 추가될 경우 매번 클래스 생성 후, 파일에 경로 지정을 해줘야 한다..

 

서블릿 컨테이너는 조금 더 유연한 초기화 기능을 지원한다. 여기서는 이것을 애플리케이션 초기화라 하겠다.

서블릿 컨테이너 초기화 인터페이스는 한번만 등록하고, 여기서 애플리케이션 초기화를 여러번 호출하면 된다.

 

서블릿 컨테이너 초기화 과정 v2 - 애플리케이션 초기화를 활용한 방식 

1. 인터페이스 생성

애플리케이션 초기화를 진행하려면 먼저 인터페이스를 만들어야 한다. 내용과 형식은 상관없다.

public interface AppInit {
    void onStartUp(ServletContext servletContext);
}

 

2. 구현체에서 서블릿 등록

public class AppInitV1Servlet implements AppInit {
    @Override
    public void onStartUp(ServletContext servletContext) {
        //순수 서블릿 코드 등록
        ServletRegistration.Dynamic helloServlet = servletContext.addServlet("helloServlet", new HelloServlet());
        helloServlet.addMapping("/hello-servlet");
    }
}

(위 코드에서는 @WebServlet이 아니라 동적으로 프로그래밍 방식으로 서블릿을 서블릿 컨테이너에 직접 등록했다.)

 

3. 서블릿 컨테이너 초기화 구현 클래스에서 애플리케이션 초기화

@HandlesTypes(AppInit.class)
public class MyContainerInitV2 implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        for (Class<?> appInitClass : c) {
            try {
                AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance(); //new AppInitV1Servlet();
                appInit.onStartUp(ctx);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

4. 서블릿 컨테이너 초기화 구현 클래스의 경로 지정

hello.container.MyContainerInitV2

 

 

서블릿 컨테이너 초기화 실행 후 애플리케이션 초기화가 실행된다. 이때 서블릿 등이 등록된다.

 

위 굳이 애플리케이션 초기화 방식을 왜 사용할까?

1. 앞으로 초기화를 추가할 때, 애플리케이션 초기화 인터페이스만 구현하면 된다. 추가로 등록, 경로 지정 등이 필요없다!

2. 애플리케이션 초기화는 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있어 서블릿 컨테이너에 대한 의존성을 줄일 수 있다. ServletContext가 필요없는 경우는 코드 내에서 의존을 완전히 줄일 수도 있다.

 

 

스프링 컨테이너 등록

이번에는 서블릿 컨테이너 초기화 후 스프링 컨테이너를 초기화하고 디스패처 서블릿을 연결해보자.

 

앞서 구현한 애플리케이션 초기화 인터페이스만 구현하면 간단하다.

 

1. 스프링 관련 라이브러리 추가

2. 컨트롤러를 만들고 @Configuration 클래스에 수동 빈 등록

3. 애플리케이션 초기화를 사용해서 서블릿 컨테이너에 스프링 컨테이너를 생성하고 등록.

public class AppInitV2Spring implements AppInit {

    @Override
    public void onStartUp(ServletContext servletContext) {
    
        //스프링 컨테이너 생성 후 컨테이너에 스프링 설정 추가
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(HelloConfig.class);

        //스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);

        //디스패처 서블릿을 서블릿 컨테이너에 등록
        ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherServletV2", dispatcherServlet);
        servlet.addMapping("/spring/*"); // /spring/* 요청이 디스패처 서블릿을 통하도록 설정
    }
}

스프링 MVC가 제공하는 디스패처 서블릿을 생성하고, 생성자에 앞서 만든 스프링 컨테이너를 전달한다. 이렇게 하면 디스패처 서블릿에 스프링 컨테이너가 연결된다.

 


스프링 MVC 서블릿 컨테이너 초기화 지원

스프링 MVC는 지금까지 진행한 아래의 서블릿 컨테이너 초기화 작업을 이미 구현해두었다

  1. ServletContainerInitializer 인터페이스를 구현해서 서블릿 컨테이너 초기화 코드 생성
  2. 여기에 애플리케이션 초기화를 만들기 위해 @HandlesTypes 애노테이션을 적용
  3. /META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스 경로를 등록

스프링이 지원하는 애플리케이션 초기화를 사용하려면 WebApplicationInitializer 인터페이스를 구현하면 된다.

public class AppInitV3SpringMvc implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        
        //스프링 컨테이너 생성 후 컨테이너에 스프링 설정 추가
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(HelloConfig.class);

        //스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);

        //디스패처 서블릿을 서블릿 컨테이너에 등록
        ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherServletV3", dispatcherServlet);
        servlet.addMapping("/"); //모든 요청이 디스패처 서블릿을 통하도록 설정
    }
}

 

어떻게 이것이 가능할까?

사실 Spring MVC가 미리 서블릿 컨테이너 초기화를 구현해 두고, WebApplicationInitializer 인터페이스를 애플리케이션 초기화 인터페이스로 지정해둔 것이다.

라이브러리를 보면 확인할 수 있다.

해당 파일을 열어보면 아래 서블릿컨테이너초기화 구현 클래스가 적혀있다.

org.springframework.web.SpringServletContainerInitializer

 

해당 클래스를 열어보면, 애플리케이션 초기화 구현체로 WebApplicationInitializer가 설정되어 있는 것을 확인할 수 있다.

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

 

초록색 영역은 이미 스프링이 만들어서 제공하는 영역이다

 


참고 자료 & 이미지 출처
스프링 부트 - 핵심 원리와 활용 (김영한 님)