관리 메뉴

나구리의 개발공부기록

웹 서버와 서블릿 컨테이너, 웹서버와 스프링 부트 소개, 톰캣 설치, 프로젝트 설정, WAR 빌드와 배포, 톰캣 설정(인텔리제이 무료/유료), 서블릿 컨테이너 초기화, 스프링 컨테이너 등록, 스프링 MVC 서블릿 컨테이너 초기화 지원 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 부트 - 핵심 원리와 활용

웹 서버와 서블릿 컨테이너, 웹서버와 스프링 부트 소개, 톰캣 설치, 프로젝트 설정, WAR 빌드와 배포, 톰캣 설정(인텔리제이 무료/유료), 서블릿 컨테이너 초기화, 스프링 컨테이너 등록, 스프링 MVC 서블릿 컨테이너 초기화 지원

소소한나구리 2024. 11. 18. 18:12

출처 : 인프런 - 스프링 부트 - 핵심 원리와 활용(유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 웹 서버와 스프링 부트 소개

1) 외장 서버 VS 내장 서버

좌) 전통적인 방식 / 우) 최근의 스프링부트 방식

(1) 전통적인 방식

  • 과거에는 자바로 웹 애플리케이션을 개발할 때 먼저 서버에 톰캣 같은 WAS(웹 애플리케이션 서버)를 설치했음
  • 그리고 WAS에서 동작하도록 서블릿 스펙에 맞추어 코드를 작성하고 WAR형식으로 빌드해서 war 파일을 만든 후 WAS에 전달하여 배포하는 방식으로 전체 개발 주기로 동작하였음
  • 이런 방식은 WAS 기반 위에서 개발하고 실행해야하고 IDE같은 개발 환경에서도 WAS와 연동해서 실행되도록 복잡한 추가 설정이 필요함

(2) 최근 방식

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

2. 톰캣 설치 및 프로젝트 설정

1) 톰캣 설치

(1) 버전

  • 자바 : 17이상, 스프링 3.0을 사용하여 자바 17이 최소 요구 버전임
  • 톰캣 : Apache Tomcat 10 버전

(2) 톰캣 다운로드

  • 링크
  • Core에 있는 ZIP 다운로드 및 압축 해제
  • 여러 추가 작업들을 할 것이기 때문에 관리하기 좋은 곳에 폴더를 만들어서 관리(지금은 홈에 was디렉토리를 만들어서 관리)

(3) 톰캣 실행 설정

  • MAC, 리눅스 : 톰캣폴더/bin 폴더로 이동하여 chmod 755 * 로 권한을 주고 ./startup.sh로 실행, ./shutdown.sh로 종료
  • MAC, 리눅스 사용자는 권한을 주지 않으면 permission denied라는 오류가 발생할 수 있음
  • 윈도우 : 권한부여 없이 바로 톰캣폴더/bin으로 이동 후 startup.bat으로 실행, 종료는 shutdown.bat

(4) 실행 확인

  • 터미널을 실행 후 톰캣폴더/bin으로 이동하여 권한을 부여하고 ./startup.sh로 톰캣을 실행
  • 웹 브라우저에서 localhost:8080으로 접속하면 톰캣 웹페이지가 정상적으로 출력된 것을 확인하고 ./shutdown.sh를 입력해주면 정상 종료 됨
톰캣 실행 
(base) jinagyeomi@namo apache-tomcat-10.1.33 % cd bin
(base) jinagyeomi@namo bin % chmod 755 *

(base) jinagyeomi@namo bin % ./startup.sh
Using CATALINA_BASE:   /Users/jinagyeomi/was/apache-tomcat-10.1.33
Using CATALINA_HOME:   /Users/jinagyeomi/was/apache-tomcat-10.1.33
Using CATALINA_TMPDIR: /Users/jinagyeomi/was/apache-tomcat-10.1.33/temp
Using JRE_HOME:        /Users/jinagyeomi/.sdkman/candidates/java/current
Using CLASSPATH:       /Users/jinagyeomi/was/apache-tomcat-10.1.33/bin/bootstrap.jar:/Users/jinagyeomi/was/apache-tomcat-10.1.33/bin/tomcat-juli.jar
Using CATALINA_OPTS:   
Tomcat started.

톰캣 종료
(base) jinagyeomi@namo bin % ./shutdown.sh
Using CATALINA_BASE:   /Users/jinagyeomi/was/apache-tomcat-10.1.33
Using CATALINA_HOME:   /Users/jinagyeomi/was/apache-tomcat-10.1.33
Using CATALINA_TMPDIR: /Users/jinagyeomi/was/apache-tomcat-10.1.33/temp
Using JRE_HOME:        /Users/jinagyeomi/.sdkman/candidates/java/current
Using CLASSPATH:       /Users/jinagyeomi/was/apache-tomcat-10.1.33/bin/bootstrap.jar:/Users/jinagyeomi/was/apache-tomcat-10.1.33/bin/tomcat-juli.jar
Using CATALINA_OPTS:

톰캣 웰컴 페이지

(5-1) 잘 안될 때 해결방안

  • localhost:8080에 접근이 되지 않으면 로그를 잘 확인해봐야하는데, 만약 java.net.BindException: Address already in use 와 같은 메시지가 보인다면 8080 포트를 이미 사용하고 있기 때문에 2가지 방법으로 해결해야함

(5-2) 프로세스 종료

  • MAC OS: 터미널에서 sudo lsof -i: 8080으로 프로세스 ID(PID)조회 후 sudo kill -9 PID로 프로세스를 종료
  • 윈도우: cmd에서 netstat -ano | findstr :포트번호로 PID를 조회 후 taskkill /f /pid PID 번호로 프로세스를 종료

(5-3) 톰캣 서버 포트 변경

  • 만약 8080포트를 꼭 다른곳에서 사용해야하는 경우에는 톰캣 설정 파일인 server.xml의 Connector 태그에서 port를 수정하여 톰캣의 포트를 변경할 수 있음
  • 톰캣폴더/conf/server.xml
 <Connector port="8080" protocol="HTTP/1.1"
            connectionTimeout="20000"
            redirectPort="8443" />

 

2) 프로젝트 설정

(1) 프로젝트 - build.gradle확인

  • 강의 샘플 프로젝트로 진행,
  • 스프링 부트 프로젝트가아닌 순수한 자바로 서블릿을 사용하는 프로젝트
  • plugins { id 'war' } : 톰캣 같은 웹 애플리케이션 서버(WAS)위에서 동작하는 WAR파일을 만들어주는 플러그인
  • jakarta.servlet-api : 서블릿을 사용할 때 필요한 라이브러리
plugins {
    id 'java'
    id 'war'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    //서블릿
    implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

(1) 간단한 HTML 등록 - index.html

  • /src/main 하위에 webapp 폴더를 생성하여 정적 HTML 생성
<!DOCTYPE html>
<html>
<body>index html</body>
</html>

 

(2) 서블릿 등록 - testServlet

  • hello 패키지 하위에 servlet 패키지를 생성하여 작성
  • /test로 요청이 오면 해당 서블릿이 실행되고 로그를 출력함
  • 웹 브라우저로 요청하면 서블릿이 실행되고 화면에 test가 출력되어야 함(응답값이 test)
  • 이 서블릿을 실행하려면 톰캣 같은 웹 애플리케이션 서버(WAS)에 이 코드를 배포해야함
package hello.servlet;

/**
 * http://localhost:8080/test
 */
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("TestServlet.service");
        resp.getWriter().println("test");
    }
}

3. WAR 빌드와 배포

1) WAR 빌드

(1) 프로젝트 빌드

  • 프로젝트 폴더로 이동
  • 프로젝트 빌드:  MAC - ./gradlew build, 윈도우 - gradlew build
  • WAR 파일 생성 확인: build/libs에 WAR파일이 생성되어있음

생성된 war파일

(2) WAR 압축 풀기 및 결과

  • jar -xvf server-0.0.1-SNAPSHOT.war로 압축을 해제하면 WEB-INF, classes, lib같은 폴더와 우리가 만든 index.html등의 파일이 보임

war 압축 해제 결과

2) JAR, WAR 간단 소개

(1) JAR

  • 여러 클래스와 리소스를 묶어서 JAR(Java Archive)라는 압축 파일을 만들 수 있으며 이 파일은 JVM 위에서 직접 실행되거나 다른곳에서 사용하는 라이브러리로 제공됨
  • 직접 실행하는 경우 main() 메서드가 필요하고, MANIFEST.MF파일에 실행할 메인 메서드가 있는 클래스를 지정해 두어야 함
  • 쉽게 이야기해서 Jar는 클래스와 관련 리소스를 압축한 단순한 파일로 필요한 경우 이 파일을 직접 실행할 수도 있고 다른 곳에서 라이브러리로 사용할 수도 있음
  • 실행 예) java -jar abc.jar

(2) WAR

  • Web Application Archive 라는 이름에서 알 수 있듯 웹 애플리케이션 서버에 배포할 때 사용하는 파일임
  • 즉 JAR가 JVM위에서 실행 된다면 WAR는 웹 애플리케이션 서버 위에서 실행됨
  • 웹 애플리케이션 서버 위에서 실행되고 HTML 같은 정적 리소스와 클래스 파일을 모두 함께 포함하기 때문에 JAR와 비교했을 때 구조가 더 복잡하며 WAR의 구조를 지켜야함

(3) WAR 구조

  • WEB-INF 폴더 하위는 자바 클래스(classes폴더)와 라이브러리(lib폴더), 그리고 설정 정보(web.xml, 생략가능)가 들어가는 곳임
  • WEB-INF를 제외한 나머지 영역은 HTML, CSS 같은 정적 리소스가 사용되는 영역임

3) WAR 배포

(1) 톰캣 서버에 배포

  1. 톰캣 서버 종료
  2. 톰캣폴더/webapps 하위를 모두 삭제(기본 예제 프로그램 등이 깔려 있음)
  3. 빌드된 server-0.0.1-SNAPSHOT.war를 복사하여 톰캣폴더/webapps 하위에 붙여 넣기
  4. 해당 파일을 ROOT.war로 이름을 변경(대문자로 이름 변경)
  5. 톰캣 서버를 실행하면 ROOT.war가 자동으로 압축이 풀리면서 실행됨

(2) 실행 결과 확인

  • 톰캣 서버를 실행 후 localhost:8080과 localhost:8080/test로 접속해보면 만들어둔 index.html페이지와 서블릿이 정상 동작하는 것을 확인할 수 있음
  • 톰캣폴더/log/catalina.out 파일을 열어보면 실행 로그를 확인할 수 있음
  • 만일 진행이 잘 되지 않으면 해당 로그파일을 열어서 문제를 확인하고 war파일을 다시 배포
  • 실제 서버에서는 이렇게 사용하면 되지만 개발 단계에서는 war파일을 만들고 서버에 복사해서 배포하는 과정이 너무 번잡한데, 각 IDE에서는 이부분을 편리하게 자동화 해줌

4. 톰캣 설정 

** 참고

  • 이클립스 설정은 강의에서 다루지 않으므로 '이클립스 gradle 톰캣' 이라는 키워드로 검색

1) 인텔리J 유료 버전

  • 인텔리J 유료 버전은 톰캣 지원이 포함되어있음

(1) 상단 메뉴 -> Run -> Edit Configurations

  • Edit Configurations 화면에서 왼쪽 상단 + 모양 클릭
  • tomcat Server 검색 후 local 선택
  • Server 탭에서 Application server의 CONFIGURE... 클릭
  • TomcatHome의 폴더모양 클릭하여 설치한 톰캣 폴더를 선택
  • Deployment탭에서 + 모양 -> Artifact -> 아무거나 클릭(보통 exploded 선택)
  • Application context:에 적혀있는 내용 모두 삭제
  • RUN 하면 설정이 완료되어 웹 애플리케이션을 실행할 수 있음

 

** 참고

  • 마찬가지로 기존에 톰캣 서버가 띄워져있으면 에러가 발생하니 꼭 기존에 띄워놨던 서버를 종료하고 실행할 것

2) 인텔리J 무료 버전

  • 무료 버전은 조금 설정이 복잡함
  • Tomcat runner 플러그인은 최신 인텔리J에서 작동하지 않으므로 Smart Tomcat 플러그인을 설치해야함
  • 설치한 Smart Tomcat 플러그인은 유료버전에서도 사용할 수 있으나 유료에서는 내장으로 사용가능하니 굳이 중복으로 사용할 필요 없음

(1) gradle 설정 및 명령어 실행

  • 아래의 설정 후 터미널에서 프로젝트로 이동하여 ./gradlew explodedWar를 입력
  • build로 이동하면 exploded폴더가 생겨있고 해당 폴더로 들어가면 WEB-INF를 포함한 war파일이 풀려있는 모습을 확인할 수 있음
//war 풀기, 인텔리J 무료버전 필요
task explodedWar(type: Copy) {
     into "$buildDir/exploded"
     with war
}
(base) jinagyeomi@namo bin % cd ~/Desktop/dev/study/spring/8.springboot/server
(base) jinagyeomi@namo server % ./gradlew explodedWar
Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details

BUILD SUCCESSFUL in 2s
2 actionable tasks: 1 executed, 1 up-to-date
(base) jinagyeomi@namo server % cd build/exploded
(base) jinagyeomi@namo exploded % ls
META-INF	WEB-INF		index.html

 

(2) Smart Tomcat 설정

  • Smart Tomcat 플러그인을 설치 후 IDE 재실행
  • 완전히 새로운 tomcat 서버를 생성 후 bin폴더로 이동하여 chmod 755 * 로 권한을 설정
  • 기존 서버를 재사용하면 설정이 꼬일 수 있어서 다운 받았던 압축파일을 다시 풀어서 생성해야함

(3) 상단 메뉴 -> Run -> Edit Configurations

  • Edit Configurations 화면에서 왼쪽 상단 + 모양 클릭
  • Smart Tomcat 선택
  • Configuration탭에서 Tomcat server: 의 CONFIGURE...를 클릭
  • 이동한 페이지에서 + 클릭하여 새로 생성한 톰캣 서버를 선택 후 OK
  • Tomcat server: 이름을 입력(폴더모양 클릭하면 이름이 자동으로 뜸)
  • Deployment directory: 현재 프로젝트 폴더의 build/exploded로 지정
  • Use classpath of module: server 선택
  • Context path: / (슬래시 하나)입력
  • OK 누르면 설정이 완료되어 웹을 띄울 수 있음


5. 서블릿 컨테이너 초기화

1) 서블릿 컨테이너 초기화 개발

  • WAS를 실행하는 시점에 필요한 초기화 작업들이 있는데, 서비스에 필요한 필터와 서블릿을 등록하고 여기에 스프링을 사용한다면 스프링 컨테이너를 만들고 서블릿과 스프링을 연결하는 디스페처 서블릿도 등록해야함
  • WAS가 제공하는 초기화 기능을 사용하면 WAS 실행 시점에 이러한 초기화 과정을 진행할 수 있음
  • 과거에는 web.xml을 사용해서 초기화 했지만 지금은 스펙에서 자바 코드를 사용한 초기화도 지원함

(1) ServletContainerInitializer

  • 서블릿이 제공하는 초기화 인터페이스로 서블릿 컨테이너를 초기화 하는 기능을 제공함
  • 서블릿 컨테이너는 실행 시점에 초기화 메서드인 onStartup()을 호출해줌으로 여기서 애플리케이션에 필요한 기능들을 초기화 하거나 등록할 수 있음
  • Set<Class<?>> c: 조금 더 유연한 초기화 기능을 제공하며 @HandlesTypes 애노테이션과 함께 사용함
  • ServletContext ctx: 서블릿 컨테이너 자체의 기능을 제공하며 이 객체를 통해 필터나 서블릿을 등록할 수 있음
public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

 

(2) MyContainerInitV1

  • hello 하위에 container 패키지를 생성하여 작성
  • 서블릿 컨테이너 초기화 인터페이스를 구현하여 실제로 동작하는지 출력문으로 확인
package hello.container;

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);
    }
}

 

(3) 경로에 파일 생성

  • WAS에 실행할 초기화 클래스를 알려주어야 함
  • resources에 META-INF/services 디렉토리를 만들어서 jakarta.servlet.ServletContainerInitializer 파일을 생성
  • 해당 파일에 hello.container.MyContainerInitV1 를 입력(서블릿 컨테이터 초기화 인터페이스를 구현한 구현체)

** 주의

  • META-INF는 대문자
  • services는 마지막에 s가 들어가는 것에 주의

(4) 실행

  • WAS를 실행해보면 구현체 클래스에 입력한 출력문들이 출력되는 것을 확인할 수 있음
  • 즉, WAS가 실행할 때 해당 초기화 클래스가 실행된다는 것을 확인할 수 있음
MyContainerInitV1 onStartup
MyContainerInitV1 c = null
MyContainerInitV1 ctx = org.apache.catalina.core.ApplicationContextFacade@36f633e4

2) 프로그래밍 방식으로 서블릿 등록

  • 서블릿은 @WebServlet 애노테이션을 이용하는 방식과 프로그래밍 방식 2가지 방법으로 등록할 수 있음

(1) HelloServlet

  • HTTP응답으로 Hello servlet!이 출력되는 서블릿
package hello.servlet;

public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("HelloServlet.service");
        resp.getWriter().println("Hello servlet!");
    }
}

3) 애플리케이션 초기화

  • 서블릿 컨테이너는 조금 더 유연한 초기화 기능을 지원하는데 여기서는 이것을 애플리케이션 초기화로 작성

(1) AppInit

  • 애플리케이션 초기화를 진행하려면 먼저 인터페이스를 만들어야함
  • 내용과 형식은 상관없고 인터페이스는 꼭 필요함
package hello.container;

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

 

(2) AppInitV1Servlet

  • 프로그래밍 방식으로 HelloServlet 서블릿을 서블릿 컨테이너에 직접 등록
  • HTTP로 /hello-servlet을 호출하면 HelloServlet 서블릿이 실행됨
package hello.container;

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");  // 매핑
    }
}

 

** 참고 - 프로그래밍 방식을 사용하는 이유

  • @WebServlet을 사용하면 애노테이션 하나로 서블릿을 편리하게 등록할 수 있지만 하드코딩 된 것처럼 동작하므로 유연하게 변경하는 것이 어려움
  • 반면 프로그래밍 방식은 코딩을 더 많이 해야하고 불편하지만 경로를 상황에 따라 바꿔서 외부 설정을 읽도록 등록하거나 특정 조건에 따라 분기를 타게하거나, 서블릿을 직접 생성하므로 생성자에 필요한 정보를 넘기는 등의 무한한 유연성을 제공함
  • 예제에서는 단순화를 위해 이런부분을 사용하지 않았지만 이러한 부분들 때문에 프로그래밍 방식으로 서블릿을 등록하는 경우가 존재함

(3) MyContainerInitV2

  • @HandlesTypes 애노테이션에 애플리케이션 초기화 인터페이스를 지정
  • 서블릿 컨테이너 초기화(ServletContainerInitializer)는 파라미터로 넘어오는 Set<Class<?>> c에 애플리케이션 초기화 인터페이스의 구현체들을 모두 찾아서 클래스 정보로 전달함
  • 여기에서는 지정한 AppInit 인터페이스를 구현한 AppInitV1Servlet.class 정보가 전달되며 객체 인스턴스를 전달하는 것이아니라 정보를 전달하기 때문에 실행하려면 객체를 생성해서 사용해야함
  • AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance(): 리플렉션을 사용하여 객체를 생성하며 해당 코드는 new AppInitV1Servlet()과 같다고 생각하면 됨
  • appInit.onStartup(ctx): 애플리케이션 초기화 코드를 직접 실행하면서 서블릿 컨테이너 정보가 담긴 ctx도 함께 전달함
package hello.container;

@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) { // 여러가지 예외가 많이 뜨지만 예제이므로 Exception 으로 한번에 해결
                throw new RuntimeException(e);
            }
        }
    }
}

 

(4) MyContainerInitV2 등록 및 실행

  • jakarta.servlet.ServletContainerInitializer 파일에 hello.container.MyContainerInitV2 입력
  • WAS를 실행해보면 실행 로그에 MyContainerInitV2 c에 AppInitV1Servlet이 담겨있어 애플리케이션 초기화가 정상 진행된 것을 로그로 확인할 수 있음
  • 매핑한 /hello-servlet으로 접속하면 HelloServlet이 동작하여 응답 메시지가 브라우저에 출력되며, HelloServlet이 실행되는 로그도 확인할 수 있음
MyContainerInitV2 onStartup
MyContainerInitV2 c = [class hello.container.AppInitV1Servlet]
MyContainerInitV2 ctx = org.apache.catalina.core.ApplicationContextFacade@48456baf
AppInitV1Servlet onStartup
[2024-11-18 04:41:56,647] Artifact Gradle : hello : server-0.0.1-SNAPSHOT.war (exploded): Artifact is deployed successfully
[2024-11-18 04:41:56,647] Artifact Gradle : hello : server-0.0.1-SNAPSHOT.war (exploded): Deploy took 173 milliseconds
HelloServlet.service

 

(5) 초기화 실행 순서

 

  • 1. 서블릿 컨테이너 초기화 실행
  • resources/META-INF/services/ jakarta.servlet.ServletContainerInitializer
  • 2. 애플리케이션 초기화 실행
  • @HandlesTypes(AppInit.class)

4) 서블릿 컨테이너 초기화만 있어도 될 것 같은데 애플리케이션 초기화라는 개념을 만든 이유

(1) 편리함

  • 서블릿 컨테이너를 초기화하려면 ServletContainerInitializer 인터페이스를 구현한 코드를 만들어야 하고 추가로resources/META-INF/services/ jakarta.servlet.ServletContainerInitializer 파일에 해당 코드를 직접 지정해주어야 함
  • 애플리케이션 초기화는 특정 인터페이스만 구현하면 됨

(2) 의존성

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

6. 스프링 컨테이너 등록

1) WAS와 스프링을 통합

(1) build.gradle - 스프링 관련 라이브러리 추가

  • spring-webmvc 라이브러리를 추가하면 스프링 MVC 뿐만 아니라 spring-core를 포함한 스프링 핵심 라이브러리들도 함께 포함됨
//스프링 MVC 추가
implementation 'org.springframework:spring-webmvc:6.0.4'

 

(2) HelloController

  • spring패키지를 생성 후 작성
  • @RestController를 사용하는 간단한 스프링 컨트롤러
  • HTTP 응답으로 hello spring!!이 응답됨
package hello.spring;

@RestController
public class HelloController {

    @GetMapping("/hello-spring")
    public String hello() {
        System.out.println("HelloController.hello");
        return "Hello Spring!!";
    }
}

 

(3) HelloConfig

  • 컴포넌트 스캔을 사용하지 않고 직접 수동으로 빈을 등록
package hello.spring;

@Configuration
public class HelloConfig {
    
    @Bean
    public HelloController helloController() {
        return new HelloController();
    }
}

 

(4-1) AppInitV2Spring

  • 애플리케이션 초기화를 사용하여 서블릿 컨테이너에 스프링 컨테이너를 생성하고 등록
  • 앞에서 AppInit을 구현하면 애플리케이션 초기화 코드가 자동으로 실행되도록 MyContainerInitV2에 이미 작업을 해두었음
package hello.container;

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("/*");
    }
}

 

(4-2) 스프링 컨테이너 생성

  • AnnotationConfigWebApplicationContext가 스프링 컨테이너이며 이름 그대로 애노테이션 기반 설정과 웹 기능을 지원하는 스프링 컨테이너
  • appContext.register(HelloConfig.class): 컨테이너에 스프링 설정을 추가

(4-3) 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결

  • new DispatcherServlet(appContext): 생성자로 스프링 컨테이너를 전달하면서 스프링 MVC가 제공하는 디스패처 서블릿을 생성
  • 이렇게 생성하면 디스패처 서블릿에 스프링 컨테이너가 연결됨
  • 이 디스패처 서블릿에 HTTP 요청이 오면 디스패처 서블릿은 해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출함

(4-4) 디스패처 서블릿을 서블릿 컨테이너에 등록

  • servletContext.addServlet("dispatcherV2", dispatcher): 디스패처 서블릿을 서블릿 컨테이너에 등록
  • /spring/* 요청이 디스패처 서블릿을 통하도록 설정
  • /spring과 그하위 요청은 모두 해당 서블릿을 통과하게 됨

** 주의

  • 서블릿을 등록할 때 이름은 원하는 이름을 등록해도 되지만 같은 이름으로 중복 등록하면 오류가 발생하므로 유의해야함

(5) WAS 실행

  • 실행 후 http://localhost:8080/spring/hello-spring 경로로 접속하면 HelloController가 정상적으로 응답한 값이 출력되는 것을 볼 수 있음
  • 다만 경로가 기존과는 다르게 /spring/hello-spring으로 접속하였는데, 디스패처 서블릿 실행을 /spring/* 패턴으로 호출했기 때문에 다음과 같이 동작함
  • 1. dispatcherV2 디스패처 서블릿이 실행됨 - /spring
  • 2. dispatcherV2 디스패처 서블릿이 스프링 컨트롤러를 찾아서 실행함 - /hello-spring
  • 이때 서블릿을 찾아서 호출하는데 사용된 /spring을 제외한 /hello-spring가 매핑된 컨트롤러의 메서드를 찾아서 실행함(디스패처 서블릿에서 매핑했던 /spring/* 에서의 * 부분으로 스프링 컨트롤러를 찾음)


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

1) 서블릿 컨테이너 초기화 과정 생략

  • 서블릿 컨테이너 초기화 과정은 상당히 번거롭고 반복되는 작업인데, 스프링 MVC는 이러한 서블릿 컨테이너 초기화 작업을 이미 만들어 두었음
  • 스프링 MVC를 사용하면 서블릿 컨테이너 초기화 과정은 생략하고 애플리케이션 초기화 코드만 작성하면됨

(1) WebApplicationInitializer

  • 스프링이 지원하는 애플리케이션 초기화를 사용하려면 해당 인터페이스를 구현하면 됨
package org.springframework.web;

public interface WebApplicationInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}

 

(2) AppInitV3SpringMvc

  • WebApplicationInitializer 인터페이스를 구현하는 부분을 제외하고는 이전의 AppInitV2 Spring과 거의 유사함
  • 디스패처 서블릿을 새로 만들어서 등록할 때 이름을 dispatcherV3로 변경하고, 모든 요청이 해당 서블릿을 통하도록 servlet.addMapping("/")으로 설정
package hello.container;

public class AppInitV3SpringMvc implements WebApplicationInitializer {
    
    /**
     * http://localhost:8080/hello-spring
     *
     * 스프링 MVC 제공 WebApplicationInitializer 활용
     * spring-web
     * META-INF/services/jakarta.servlet.ServletContainerInitializer * org.springframework.web.SpringServletContainerInitializer
     */
    @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("/");
    }
}

 

(3) WAS 실행

  • WAS를 실행 후 localhost:8080/hello-spring으로 접속하면 정상적으로 서블릿과 애플리케이션이 초기화되어 HelloController의 응답값이 출력되는 것을 확인할 수 있음

(4) 등록된 서블릿들

  • / = dispatcherV3
  • /spring/* = dispatcherV2
  • /hello-servlet = helloServlet
  • /test = TestServlet
  • 이렇게 여러가지 서블릿이 등록된 경우 더 구체적인 것이 먼저 실행됨

** 참고

  • 여기서는 이해를 돕기 위해 디스패처 서블릿도 2개 만들고 스프링 컨테이너도 2개 만들었음
  • 일반적으로는 스프링 컨테이너를 하나 만들고 디스패처 서블릿도 하나만 만들고 디스패처 서블릿의 경로 매핑도 /로하여 하나의 디스패처 서블릿을 통해서 모든 것을 처리하도록 함

2) 스프링 MVC가 제공하는 서블릿 컨테이너 초기화 분석

(1) spring-web 라이브러리 확인

  • spring-web라이브러리를 열어보면 서블릿 컨테이너 초기화를 위한 등록파일을 확인할 수 있으며, 이곳에 서블릿 컨테이너 초기화 클래스가 등록되어있음
  • /META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 org.springframework.web.SpringServletContainerInitializer가 등록되어있음

(2) SpringServletContainerInitializer 확인

  • 등록된 서블릿 컨테이너 초기화 파일을 보면 직접 실습하면서 만든 컨테이너 초기화 코드와 유사함
  • @HandlesTypes의 대상으로 WebApplicationInitializer가 등록되어있는 것을 확인할 수 있으며 해당 인터페이스는 앞서, AppInitV3SpringMvc를 생성할 때 구현했던 인터페이스임
  • 아래 이미지의 초록색 영역이 스프링이 만들어서 제공하는 영역임
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    // ... 구현 코드 생략
}

 

(3) 정리

  • 스프링 MVC도 우리가 지금까지 한 것처럼 서블릿 컨테이너 초기화 파일에 초기화 클래스를 등록해 두었고 WebApplicationInitializer 인터페이스를 애플리케이션 초기화 인터페이스로 지정해두고 이것을 생성해서 실행함
  • 즉, 스프링 MVC를 사용하면 WebApplicationInitializer 인터페이스만 구현하면 편리하게 애플리케이션 초기화를 사용할 수 있음
  • 지금까지 알아본 내용은 모두 서블릿 컨테이너 위에서 동작하는 방법이므로 항상 톰캣 같은 서블릿 컨테이너에 배포를 해야만 동작하는 방식임
  • 과거에는 서블릿 컨테이너위에서 모든 것이 동작했지만, 스프링 부트와 내장 톰캣을 사용하면서 이런 부분이 바뀌기 시작하였음