이때동안 모든 실습에 있어 스프링 부트를 활용해왔다. 스프링 부트는 톰캣 WAS를 내장하고있고 개발자는 코드를 작성해서 실행하기만 하면 WAS가 함께 실행되었다. 부트가 없던 옛날 스프링 개발자들은 직접 WAS를 설치하고 war를 만들어 WAS에 배포하는 과정이 필요했었고 이를 직접 경험해보자 한다.
Tomcat 설치 다운로드 링크 (스프링 부트 3이상 => 톰켓 10 이상)
권한주기 :bin -> chmod 755 *
실행 : bin -> ./startup.sh
종료 : bin -> ./shutdown.sh
실행 로그 : 톰캣폴더/logs/catalina.out
종료 다른 방법 : sudo lsof -i :8080 (프로세스 ID(PID) 조회) -> sudo kill -9 PID(프로세스 종료)
서버 포트 변경법 : 톰캣폴더/conf/server.xml ->
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> 수정
자바는 여러 클래스와 리소스를 묶어 JAR라는 압축 파일로 단일화할 수 있다. 이 파일은 JVM 위에서 직접 실행된다. 반면 WAR 좀 더 특수한 압축 파일이다. 웹 어플리케이션 서버 위에서 실행되기 위한 파일로 JAR에 비해 좀 더 복잡하다는 특징이 있다.
우리가 부트를 이용한 프로젝트로 하여금 JAR만을 활용할 때 부트는 웹 애플리케이션에 WAR로 배포되는 과정을 자동 처리해준다.
WAR를 만들어 외장 톰켓에 배포하기 위해 다음의 과정을 따를 수 있다.
id 'war' 추가./gradlew build로 하여금 WAR파일(.snapshot.WAR) 생성ROOT.WAR 로 이름 변경이러한 과정들이 굉장히 복잡하다. 이를 IDE plugin으로 하여금 편리하게 바꿀 수 있다.(빌드 및 배포 자동화 -> smart tomcat 사용)
WAS에 자바 프로젝트를 올려 배포절차를 마무리했다면 WAS의 실행으로 하여금 우리는 localhost:8080 접근이 가능하다.
WAS를 실행하는 시점에 필요한 초기화 작업들이 존재하는데, 대표적으로 서블릿을 등록하고 스프링을 사용한다면 스프링 컨테이너를 만들고 서블릿과 스프링을 연결하는 dispatcherServlet을 서블릿 컨테이너에 등록해야한다.
과거에는 이를 web.xml을 통해 초기화했지만 현재는 자바 코드로 하여금 초기화도 지원한다.
자바에 내장된 서블릿은 ServletContainerInitializer 초기화 인터페이스를 제공한다. 서블릿 컨테이너를 초기화하기 위해 이 인터페이스를 상속받아 onStartup()을 호출하게끔 하는 것이다.
그리고 WAS에게 ServletContainerInitializer 인터페이스로 하여금 초기화 할 것임을 알리기 위해 resources/META-INF/services/jakarta.servlet.ServletContainerInitializer 이 경로에 파일을 생성한다. 그리고 파일에 인터페이스를 구현한 구현체를 패키지 경로를 포함하여 작성한다.
public class MyContainerInitV1 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws
ServletException {
System.out.println("MyContainerInitV1.onStartup");
System.out.println("MyContainerInitV1 c = " + c);
System.out.println("MyContainerInitV1 ctx = " + ctx);
}
}
구현체는 두 가지 파라미터를 받는데, c는 해당 인터페이스로 구현된 모든 구현체를 Set에 담아주며, ctx는 ServletContext, 즉 서블릿 컨테이너이다. 톰캣은 초기화시 onStartup호출을 위해 우리의 코드를 참고하여 구현체들을 담아주고, 서블릿 컨테이너를 주입해준다.
onStartup()호출시 ctx에 서블릿 객체를 담기 위해 서블릿 클래스를 작성할 수 있을 것이다.
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet.service");
resp.getWriter().println("hello servlet!");
}
}
서블릿 컨테이너는 조금 더 유연한(하지만 좀 더 복잡한) 초기화 기능을 지원한다.
/**
* http://localhost:8080/test
*/
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("TestServlet.service");
resp.getWriter().println("test");
}
}
위와 같이 @WebServlet을 사용하여 애노테이션으로 매우 간단하게 서블릿을 등록(초기화)해서 사용할 수도 있지만 다음과 같은 복잡하지만 더 자유도가 높은 등록도 가능하다.
public interface AppInit {
void onStartup(ServletContext servletContext);
}
//////
public class AppInitV1Servlet implements AppInit {
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("AppInitV1Servlet.onStartup");
// 순수 서블릿 코드 등록
ServletRegistration.Dynamic helloServlet =
servletContext.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
}
}
//////
@HandlesTypes(AppInit.class)
public class MyContainerInitV2 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println("MyContainerInitV2.onStartup");
System.out.println("MyContainerInitV2 c = " + c);
System.out.println("MyContainerInitV2 ctx = " + ctx);
//class hello.container.AppInitV1Servlet
for (Class<?> appInitClass : c) {
try {
// new AppInitV1Servlet()과 같은 코드
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
appInit.onStartup(ctx);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
AppInit(이름을 무엇으로 해도 상관 없음.) 인터페이스를 작성하고 하나의 메서드(onStartup(), 이 또한 이름을 달리해도 됨)를 작성하여 구현한다. 다만 메서드에 파라미터로 서블릿 컨테이너인 ServletContext를 받아준다. 그리고 초기화메서드를 본격적으로 작성하기 위해 ServletContainerInitializer를 상속받은 구현체를 작성해주고 @HandlesTypes(AppInit.class) 애노테이션을 붙여준다.
이로 하여금 톰켓은 c에 AppInit에 해당하는 구현체들을 담아준다. 해당 구현체들의 메서드를 생각해보면 파라미터로 ctx를 받아야하므로 위의 코드 흐름이 어색하진 않을 것이다. 애플리케이션 초기화는 서블릿 컨테이너 초기화 과정에서 호출되는 것이다.
AppInit 구현체의 메서드를 보면 서블릿을 직접 등록하는 과정의 코드가 존재한다. @WebServlet(경로) 애노테이션으로 하여금 등록하는 것보다 등록과정을 더 유연하게 조정할 수 있다.(ex. 경로를 동적으로 매핑)
MyContainerInitV2 자체가 초기화 시점에 작동하기 위해서는 이전과 동일하게 INF/services/jakarta.servlet.ServletContainerInitializer경로에 초기화 클래스를 경로포함해서 작성해주어야 한다.
결론적으로 이 과정은 정말 복잡하다. 다만 이해는 어렵지 않을 것이다. 이 과정을 외워 활용할 경우는 없을 것이나 스프링부트로 하여금 우리가 main()을 구동했을 때 복잡하고 빠르게 콘솔창에서 일어나는 일을 대강 짐작할 수 있을 것이다.

WAS에 튜토리얼 느낌으로 서블릿 컨테이너, 서블릿 객체, WAS 실행시 서블릿 컨테이너에 서블릿 객체를 등록하는 과정을 모두 완료했다. 이제 스프링 컨테이너를 도입하려고 한다.

dependencies {
//서블릿
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
//스프링 MVC 추가
implementation 'org.springframework:spring-webmvc:6.0.4'
}
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("HelloController.hello");
return "hello spring!!";
}
}
////////////
@Configuration
public class HelloConfig {
@Bean
public HelloController helloController() {
return new HelloController();
}
}
@RestController로 빈 등록(스프링 컨테이너 등록)이 자동적으로 되지 않는다. 자동등록 로직인 컴포넌트 스캔은 부트 환경 아래에서만 가능했다. 그러므로 직접 설정파일을 작성해서 빈으로 등록해주어야 한다.
당연히 설정 클래스인 HelloConfig또한 스프링 컨테이너에 직접 넣어주어야 한다.
public class AppInitV2Spring implements AppInit {
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("AppInitV2Spring.onStartup");
// 스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
// 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너와 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
// 디스패처 서블릿을 서블릿 컨테이너에 등록
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV2", dispatcher);
// /spring/*로 오는 모든 요청 디스패처 서블릿으로
servlet.addMapping("/spring/*");
}
}
스프링 컨테이너를 생성하고, 이곳에 Config를 등록시켜 Config에 의해 Bean 등록또한 이루어진다.
그리고 스프링으로 연결될 디스패처 서블릿을 만들고 스프링 컨테이너에 등록해준다.
스프링이 제공하는WebApplicationInitializer으로 하여금 복잡한 초기화 구축을 단순하게 바꿔낼 수 있다.
우리는 AppInit이라는 인터페이스에 구현체를 작성하고 이 인터페이스를 @HandlesTypes로 하여금 ServletContainerInitializer를 구현한 구현체에서 복잡한 초기화 실행 메서드를 호출해서 초기화를 진행했다.
하지만 WebApplicationInitializer를 상속받는 것으로 ServletContainerInitializer의 구현체를 만들 필요 없이 바로 초기화 코드를 작성할 수 있다.
public class AppInitV3SpringMvc implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("AppInitV3SpringMvc.onStartup");
// 스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
// 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너와 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
// 디스패처 서블릿을 서블릿 컨테이너에 등록
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV3", dispatcher);
// 모든 요청 디스패처 서블릿으로
servlet.addMapping("/");
}
}