자바/스프링 부트

웹 서버와 서블릿 컨테이너

UroJem 2023. 3. 11. 23:17

웹 어플리케이션 개발 시 

전통적인 방법으로 웹 서버에 WAS를 설치하고(Tomcat 같은 웹 어플리케이션 서버) WAS에서 동작할 수 있게 서블릿 스펙에 맞추어 코드를 작성하고 (서블릿 스펙 확인 https://tomcat.apache.org/whichversion.html) WAR파일로 빌드하여 WAS에 전달하여 배포하는 방식이다.

 

JAR(Java ARchive) : 자바는 여러 클래스와 리소스를 묶어서 JAR라는 압축 파일을 만들 수 있다. 이 파일은 JVM 위에서 직접 실행되거나 또는 다른 곳에서 사용하는 라이브러리로 제공된다.

직접 실행하는 경우 main 메서드가 필요하고 'MANIFEST.MF' 파일에 실행할 메인메서드가 있는 클래스를 지정해두어야 한다.

실행 예)java -jar abc.jar

JAR는 쉽게 이야기해서 클래스와 관련 리소스를 압축한 단순한 파일이다. 필요한 경우 이 파일을 직접 실행할 수 도 있고 다른 곳에서 라이브러리로 사용할 수도 있다.

 

WAR(Web Application ARchive) : 자바로 만든 웹 어플리케이션 구동에 필요한 자원(자바 서버 페이지, 자바 서블릿, 자바 클래스, XML, 파일, 태그 라이브러리, 정적 웹페이지 등..)들을 표준화된 폴더구조로 압축하여 배포하는데 사용되는 JAR 파일이다.

JAR 파일이 JVM 위에서 실행된다면, WAR는 WAS 위에서 실행된다.

WAS 위에서 실행되고, HTML 같은 정적 리소스와 클래스 파일 모두 포함하기 때문에 JAR와 비교해서 구조가 더 복잡하다.

IDE 툴(이클립스, 인텔리제이)로 웹 어플리케이션 개발환경 폴더 구조는 상이할 수 있으나 결과적으로 build하여 나온 WAR 파일의 폴더 구조는 표준화된 폴더구조를 갖고 있다. 이 구조를 반드시 지켜야한다.

 

 

표준화된 폴더구조 혹은 Servlet Directory 혹은 Web Application 혹은 Servlet Context라고 불린다.

Context부분이 Context Root, classes 폴더가 Classpath Root

 

conf 폴더 내부에는 WAS default 설정값을 갖는 파일들이 위치한다. server.xml 내부에 appBase를 지정하는 구문이 있어 WAS에서 표준화된 폴더구조를 webapps에서 찾게 된다. 어떤 WAS이건 wepapps를 찾는 설정이 기본값으로 지정되어 있다.

 

server.xml

unPackWARs : true일 경우 appBase에 있는 .war 압축파일을 파일명으로 디렉토리 생성 후 풀어서 배치한다.

autoDeploy : true일 경우 appBase에 있는 war 파일을 탐캣 구동시 자동으로 읽고 배포한다.

포트도 설정할 수 있음

 

 

conf/web.xml 과 WEB-INF/web.xml

web.xml은 웹 어플리케이션 설정을 위한 배포서술자로 서버 시작시 메모리에 로딩되어 클라이언트 요청에 배포서술자를 참조하여 각 서블릿에 맵핑시켜주는 역할을 한다. 그 외 여러가지 환경정보를 설정하는 파일이다.

conf 에 있는 web.xml은 서버내에 전체적으로 설정되는 default 값으로 전체 프로젝트에 영향을 받는 종속적인 설정이라면 context root WEB-INF/web.xml에 설정하면 독립적인 구조로 WAR로 압축하여 해당 웹 어플리케이션 설정만 가지게 된다.

다른 WAS로 프로젝트를 옮겨가도 영향을 받지 않는다.

 

기본적으로 ROOT.war 파일을 WAS/webapps 폴더에 넣고 WAS 실행시키면 압축이 풀리고 

HOST:PORT url로 접속할 수 있다. ROOT 폴더는 HOST:PORT/ <-로 접속하게 된다.

만약 다른이름의 war파일이라면 서버 구동시 압축파일이 풀리고 

HOST:PORT/압축풀린 폴더명으로 접속할 수 있다.

server.xml에 설정을 해주면 context path 설정이 가능하여 root path로 접속할 수 있다.

 

 

최근 방식으로 애플리케이션 코드 안에 Tomcat 같은 WAS가 라이브러리로 내장되어있어 개발자는 코드를 작성하고 JAR로 빌드한 다음 해당 JAR를 원하는 위치에서 실행하기만 하면 WAS도 함께 실행된다.

main 메서드를 실행하기만 하면 되고, WAS 설치가 IDE 같은 개발환경에서 WAS와 연동하는 복잡한 일은 수행하지 않아도 된다.

 

 

스프링 3.0을 사용하기 위해 자바 17버전, Tomcat은 10버전 Tomcat 서치

https://tomcat.apache.org/download-10.cgi

 

Apache Tomcat® - Apache Tomcat 10 Software Downloads

Welcome to the Apache Tomcat® 10.x software download page. This page provides download links for obtaining the latest version of Tomcat 10.1.x software, as well as links to the archives of older releases. Unsure which version you need? Specification versi

tomcat.apache.org

https://jdk.java.net/17/

 

JDK 17 Releases

JDK 17 Releases JDK 17 has been superseded. Please visit jdk.java.net for the current version. Older releases, which do not include the most up to date security vulnerability fixes and are no longer recommended for use in production, remain available in th

jdk.java.net

 

 

build.gradle

 

servlet mapping

간단한 html 파일 작성
웹 앱에 접속하기 위한 HttpServlet 구현한 서블릿 생성

 

web.xml에 설정하여 서블릿을 맵핑하는 방식이 아닌 자바 코드로 서블릿 맵핑하는 방법 중 하나

 

@WebServlet 애노테이션 사용

서블릿 3.0 / 톰캣 7.0 부터 생긴 서블릿 매핑방식. 선언한 속성값 urlPatterns로 서블릿을 맵핑해준다. 사용시 해당 클래스는 반드시 HttpServlet을 상속하여 doGet, doPost나 service 메서드를 Overrriding 해야 한다. (안그럼 에러나는 코드임)

 

WAR build, deployment

WAR 파일 생성

인텔리제이 Terminal을 사용하여 프로젝트를 build 한다.
build\libs 안에 생성된 war 파일
war 파일 압축 해제 명령어
직접 만든 표준화된 폴더구조의 코드들을 확인할 수 있다.
사용할 Tomcat 서버 webapps 폴더에 만든 server-0.0.1-SNAPSHOT.war 파일을 ROOT.war 이름으로 변경하여 넣어준다.
Tomcat 시작 하면 ROOT.war가 압축 해제되며 웹 앱 구동 성공
WAS 로고 확인시 TestServlet.service가 찍힌 것 확인

 

지금까지는 직접 Tomcat을 컴퓨터에 설치하여 해당 Tomcat으로 어플리케이션 구동을 하였다면

이제는 인텔리제이에 프로젝트 Tomcat을 설치하여 진행한다.

 

서블릿 컨테이너 초기화 1

WAS를 실행하는 시점에 필요한 초기화 작업들이 있다. 서비스에 필요한 필터와 서블릿을 등록하고, 스프링을 사용한다면 스프링 컨테이너를 만들고, 서블릿과 스프링을 연결하는 디스패쳐 서블릿도 등록해야 한다.

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

과거에는 web.xml을 사용해서 초기화했지만, 지금은 서블릿 스펙에서 자바 코드를 사용한 초기화도 지원한다.

 

 

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

 

web.xml을 대체해서 사용할 수 있는 진입점을 제공하는 인터페이스

Set<Class<?>> c : 좀 더 유연한 초기화 기능 제공. @HandlesTypes 애노테이션과 함께 사용한다. ServletContainerInitializer service provider를 찾은뒤 @HandleTypes 로 정의된 클래스를 실행한다.

ServletContext ctx : 서블릿 컨테이너 자체의 기능을 제공한다. 이 객체를 통해 필터나 서블릿을 등록할 수 있다. 해당 객체 자체가 서블릿 컨테이너에 접근할 수 있게 해주는 객체

 

서블릿 컨테이너에게 초기화 메서드를 알려주는 파일 생성 폴더 resources/META-INF 폴더에 services 폴더 하위 초기화 메서드를 가진 실제 객체 FQCN으로 파일 이름을 정하고 내부에 해당 객체를 실제 초기화 메서드 사용한 FQCN을 적어준다.

META-INF 폴더는 manifest 파일을 담거나 프로젝트와 관련된 설정파일을 둔 폴더로 manifest 파일이란 jar 파일의 사용 매뉴얼이나 스펙을 가지고 있는 사용설명서와 비슷한 개념이다.

예를들면, 실행되는 main 함수가 어떤 class에 위치하는지, 프로그램 보안정책이 어떻게 되는지 sealing 정보 등을 담고있다.

 

2. 서블릿을 등록하는 프로그래밍 방식

@WebServlet 애노테이션을 사용하지 않은 servlet 클래스 생성

서블릿 컨테이너 초기화와 구분되는 애플리케이션 초기화.

서블릿 컨테이너는 조금 더 유연한 초기화 기능을 제공한다. 서블릿 등록 프로그래밍 방식을 위해 애플리케이션 초기화로 따로 명명함.

구현시 반드시 onStartup 메서드를 구현해야 하는 인터페이스를 만듦 여러곳에 쓰여질 것에 대비
HelloServlet 객체를 서블릿 컨테이너에 등록해준다. 이름만 봤을 때 동적으로 서블릿 컨테이너 실행시 서블릿을 등록하는 객체로 등록인 된 것 같다. 그 후 접속할 urlPattern을 지정하여 맵핑해준다.

 

@WebServlet 애노테이션이 서블릿 등록하기에 쉽게 사용되는 방법이지만 유연하게 변경하는 것이 어렵고 하드코딩된 것처럼 동작한다. urlPatterns 속성을 변경하고 싶으면 코드를 직접 변경해야 하꿀수 있다.

반면 프로그래밍 방식은 코딩을 더 많이 해야하고 불편하지만 무한한 유연성을 제공한다.

예를들어 '/hello-servlet'경로를 상황에 따라 바꾸어 외부 설정을 읽어서 등록할 수 있다.

서블릿 자체도 특정 조건에 따라 if 문으로 분기해서 등록하거나 뺼 수 있다.

서블릿을 내가 직접 생성하기 떄문에 생성자에 필요한 정보를 넘길 수 있다.

컨테이너가 실행될 때 onStartup 메서드 실행되며 동적으로 서블릿이 등록되니 onStartup 내부 if 분기문으로 상황에 따라 내가 원하는 서블릿으로 등록하게 만들 수 있다. 그리고 urlPattern도 분기분으로 다른 url을 넣어줄 수도 있으니 좀 더 유연한 처리가 되는 것.

 

 

AppInit 애플리케이션 초기화는 어떻게 실행될까?

컨테이너 실행시 AppInitV1Servlet 정보가 넘어온 것을 확인할 수 있다.

@HandlesTyles 애노테이션은 속성에 정의한 클래스를 구현한 클래스 메타 정보들을 onStartup메서드 실행시 파라미터 Set<Class<?>> c 에 담아온다. 해당 리스트들을 반복문 돌려 실제 인스턴스 생성하고 인스턴스 내부에 onStartup 메서드를 실행하여 초기화 서블릿 등록을 한다.

 

애플리케이션 초기화 과정

  1. @HandleTypes 애노테이션에 애플리케이션 초기화 인터페이스를 지정한다.
    • 여기서는 앞서만든 AppInit.class 인터페이스를 지정했다.
  2. 서블릿 컨테이너 초기화(ServletContainerInitializer)는 파라미터로 넘어오는 Set<class<?>> c</class<?>에 리플렉션을 활용하여 애플리케이션 초기화 인터페이스의 구현체들을 모두 찾아 클래스 정보로 전달한다.
    • 여기서는 @HandleTypes(AppInit.class)를 지정했으므로 AppInit.class 의 구현체인 AppInitV1Servlet.class 정보가 전달된다.
    • 참고로 객체 인스턴스가 아닌 클래스 정보를 전달하기 때문에 실행하려면 객체를 생성해서 사용해야 한다.
  3. appInitClass.getDeclaredConstructor().newInstance()
    • 리플렉션을 사용해서 객체를 생성한다. 참고로 이 코드는 new AppInitV1Servlet()과 같다고 생각하면 된다.
  4. appInit.onStartup(ctx)
    • 애플리케이션 초기화 코드를 직접 실행하면서 서블릿 컨테이너 정보가 담긴 'ctx'도 함께 전달한다.

복잡한 애플리케이션 초기화를 만든 이유?

편리함

서블릿 컨테이너를 초기화하려면 ServletContainerInitializer 인터페이스를 구현한 코드를 만들어야 한다. 여기에 추가로 META-INF/services/jakarta.servlet.servletContainerInitializer 파일에 해당 코드를 직접 지정해주어야 한다.

애플리케이션 초기화 구성만 잘 해놓으면 필요한 인터페이스 구현만 하면 된다.

얘를 따로 NETA-INF에 작성 안했던 것 처럼

 

의존성

애플리케이션 초기화는 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있다. 이를 통해 애플리케이션 초기화 코드가 서블릿 컨테이너에 대한 의존을 줄일 수 있다. 특히 ServletContext ctx 파라미터 인자가 필요없는 애플리케이션 초기화 코드라면 의존을 완전히 제거할 수도 있다.

컨테이너 실행시 초기화 메서드가 아닌 각종 필요한 메서드로 만들 수 있다.

 

스프링 컨테이너 등록

build.gradle에 스프링 라이브러리 의존성 추가. spring 라이브러리가 있어야 스프링 컨테이너에 API들을 사용할 수 있다.
RestController인 HelloController 클래스 작성
@Configuration 애노테이션으로 설정파일 만들고 @Bean 애노테이션으로 스프링 컨테이너에 HelloController 주입
AppInitV2Spring은 AppInit을 구현했다. AppInit을 구현하면 애플리케이션 초기화 코드가 자동으로 실행된다.

 

AnnotationConfigWebApplicationContext가 스프링 컨테이너다. 해당 객체는 애노테이션 기반 설정과 웹 기능을 지원하는 스프링 컨테이너다.

ApplicationContext는 스프링 컨테이너라고도 하는데 BeanFactory 하위 인터페이스로 BeanFactory는 스프링 컨테이너의 최상위 인터페이스이고 스프링 빈을 관리하고 조회하는 역할을 한다. ApplicationContext는 BeanFactory의 모든 기능을 가지고 있다.

ApplicationContext는 BeanFactory + 부가기능(국제화 기능, 환경 변수 관련 처리, 애플리케이션 이벤트, 리소스 조회)를 가진다. 

ApplicationContext의 구현체가 여러가지 있는데, 구현체에 따라 스프링 컨테이너를 XML을 기반으로 만들 수도 있고, 자바 클래스로 만들 수도 있다. 이게 가능한 이유는 빈 등록을 BeanDefinition으로 추상화해서 생성 하기 때문이다. XML로 하든, 자바로 하든 BeanDefinition이 생성된다.

스프링 컨테이너 내부에는 빈 저장소가 존재한다. 빈 저장소는 key로 빈 이름을 가지고 있으며, value로 실제 빈 객체를 가지고 있다.

 

스프링 컨테이너는 기본적으로 빈을 싱글톤으로 관리해준다. 따라서 싱글톤 컨테이너라고 불리기도 한다. 스프링 컨테이너가 빈을 싱글톤으로 관리해주면서 기존 싱글턴 패턴의 문제점(싱글톤 패턴 구현을 위한 코드가 추가되어야함, 구체 클래스에 의존, 유연성이 떨어짐 etc)은 없어지고, 싱글톤의 장점(매번 인스턴스를 생성할 필요없이 단 하나만 생성해서 비용을 줄일 수 있다.)만 가져갈 수 있다.

 

https://velog.io/@max9106/Spring-ApplicationContext

 

[Spring] 스프링 컨테이너(ApplicationContext)

ApplicationContext를 스프링 컨테이너라고 한다.(정확히는 스프링 컨테이너를 부를 때, BeanFactory, ApplicationContext를 구분해서 말하지만, BeanFactory를 직접적으로 사용하는 경우는 거의 없다.)

velog.io

 

appContext.register(HelloConfig.class)로 컨테이너에 스프링 설정을 추가하면 HelloController가 빈으로 등록된다.

스프링 MVC가 제공하는 디스패쳐 서블릿 생성하여 스프링 컨테이너에 전달하면 디스패쳐 서블릿에 스프링 컨테이너가 연결된다. 이 디스패쳐 서블릿에 HTTP 요청이 오면 디스패처 서블릿은 해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출한다.

 

servletContext.addServlet("dispatcherV2", dispatcher) 로 디스패쳐 서블릿을 서블릿 컨테이너에 등록하면 클라이언트에서 /spring/*으로 요청이 오면 디스패쳐 서블릿을 통해 각 처리할 빈을 위임하여 처리 후 클라이언트로 보낸다.

 

 

간단한 스프링 컨테이너 초기화 과정

WAS 실행시 WebApplicationInitializer만 구현하면 해당 onStartup 메서드가 실행되어 별도로 META-INF에 등록하지 않아도 해당 메서드가 실행된다.

AppInit 인터페이스처럼 WebApplicationInitializer 인터페이스를 구현한 클래스의 onStartup 메서드를 실행시키기 때문

스프링MVC도 우리가 지금까지 한 것 처럼 서블릿 컨테이너 초기화 파일에 초기호 ㅏ클래스를 등록해두었다.

그리고 WebApplicationInitializer 인터페이스를 애플리케이션 초기화 인터페이스로 지정해두고, 이것을 생성해서 실행한다.

따라서 스프링 MVC를 사용한다면 WebApplicationInitializer 인터페이스만 구현하면 AppInitV3springMvc에서 본 것 처럼 편리하게 애플리케이션 초기화를 사용할 수 있다.

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard