웹 서버와 서블릿 컨테이너
서블릿 컨테이너 초기화1
WAS를 실행하는 시점에 필요한 초기화 작업들이 있다.
서비스에 필요한 필터와 서블릿을 등록하고, 여기에 스프링을 사용한다면 스프링 컨테이너를 만들고,
서블릿과 스프링을 연결하는 디스페처 서블릿도 등록해야 한다.
WAS가 제공하는 초기화 기능을 사용하면, WAS 실행 시점에 이러한 초기화 과정을 진행할 수 있다.
과거에는 web.xml 을 사용해서 초기화했지만, 지금은 서블릿 스펙에서 자바 코드를 사용한 초기화도 지원한다.
디스패처 서블릿은 스프링 MVC의 핵심 요소로,
클라이언트의 모든 요청을 받아서 적절한 컨트롤러로 위임하는 프론트 컨트롤러(Front Controller) 역할
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
// URL 요청이 들어올 때
http://localhost:8080/users/login
// 디스패처 서블릿이 이를 받아서
// UserController의 적절한 메서드로 요청을 전달
@Controller
public class UserController {
@RequestMapping("/users/login")
public String login() {
// 로그인 처리 로직
return "loginPage";
}
}
서블릿을 이용할때는 저수준 API였지만
스프링 어노테이션을 사용해서 스프링 MVC구조로 바꾸게 되면서 직접 서블릿을 작성할 필요없이
어노테이션 기반 개발로 고수준 API를 제공하게 된다.
디스패치서블릿으로 인해 서블릿 기술과 스프링 기술이 합쳐져서 스프링 MVC를 구성할 수 있게된다.
그러므로 디스패치 서블릿이 서블릿과 스프링을 연결하는 역할이다.
서블릿 컨테이너와 스프링 컨테이너
웹 어플리케이션 서버(WAS)안에 서블릿 컨테이너와 스프링 컨테이너가 있고, HTTP Request가 오면
DispatcherServlet이 요청을 해결할 수 있는 HandlerMethod를 찾고, 해당 HandlerMethod가 있는 컨트롤러 빈을 찾아
요청처리를 시킨다.
지금부터 서블릿 컨테이너의 초기화 기능을 알아보고 이어서 이 초기화 기능을 활용해 스프링 만들고 연결해보자.
[ 디스패처 서블릿도 등록하고, 스프링컨테이너도 만들어볼 것이다. ]
서블릿 컨테이너 초기화 개발
서블릿은 ServletContainerInitializer라는 초기화 인터페이스를 제공한다.
이름 그대로 서블릿 컨테이너를 초기화 하는 기능을 제공한다.
서블릿 컨테이너는 실행 시점에 초기화 메서드인 onStartup() 을 호출해준다.
여기서 애플리케이션에 필요한 기능들을 초기화 하거나 등록할 수 있다
ServletContainerInitializer
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws
ServletException;
}
Class<?>: Class는 Java의 클래스 타입을 나타내는 객체입니다.
<?>는 제너릭 타입을 사용하는 부분으로, <?>는 "어떤 타입이든 상관 없다"는 의미입니다.
즉, 이 메소드는 Class 객체의 종류에 제한을 두지 않고, 다양한 클래스를 받을 수 있도록 되어 있습니다.
Set<Class<?>> c : 조금 더 유연한 초기화를 기능을 제공한다.
@HandlesTypes 애노테이션과 함께 사용한다. 이후에 코드로 설명한다.
ServletContext ctx : 서블릿 컨테이너 자체의 기능을 제공한다.
이 객체를 통해 필터나 서블릿을 등록할 수 있다.
MyContainerV1
public class MyContainerV1 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("MyContainerV1.onStartup");
System.out.println("MyContainerV1 Set<Class<?>> set = " + set);
System.out.println("MyContainerV1 ServletContext = " + servletContext);
}
}
서블릿 초기화 인터페이스를 구현한 클래스
Servlet Container한테 서블릿 초기화 메소드는 이것이라는걸 알려줘야한다. [ 초기화 클래스는 이거라는걸 알려줘야함 ]
다음 경로에 파일을 생성하자
resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
jakarta.servlet.ServletContainerInitializer이라는 파일에
방금 만든 MyContainerInitV1 클래스를 패키지 경로를 포함해서 지정해주었다.
이렇게 하면 WAS를 실행할 때 해당 클래스를 초기화 클래스로 인식하고 로딩 시점에 실행한다
hello.container.MyContainerV1
주의! 경로와 파일 이름을 주의해서 작성해야한다.
META-INF 는 대문자이다.
services 는 마지막에 s 가 들어간다.
파일 이름은 jakarta.servlet.ServletContainerInitializer 이다.
이렇게 해두면 톰캣이 servlet container 초기화 시점에 MyContainerV1의 onStartup()를 실행한다.
WAS를 실행해보면
Error running 'Tomcat': Address localhost:1099 is already in use
다음과 같은 에러가 발생한다면
cmd 에서
net stop winnat
net start winnat
실행결과 로그를 보면
WAS를 실행할 때 해당 초기화 클래스가 실행된 것을 확인할 수 있다
서블릿 컨테이너 초기화2
서블릿 컨테이너 초기화를 조금 더 자세히 알아보자
여기서는 HelloServlet이라는 서블릿을 서블릿 컨테이너 초기화 시점에 프로그래밍 방식으로 직접 등록해줄 것이다.
서블릿을 등록하는 2가지 방법
- @WebServlet 애노테이션
- 프로그래밍 방식
HelloServlet
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!");
}
}
이 서블릿을 등록하고 실행하면 다음과 같은 결과가 나온다.
로그: HelloServlet.service HTTP
응답: hello servlet!
뒤쪽에서 프로그래밍 방식으로 서블릿을 등록해보자.
애플리케이션 초기화
서블릿 컨테이너는 조금 더 유연한 초기화 기능을 지원한다.
여기서는 이것을 애플리케이션 초기화라 하겠다.
이 부분을 이해하려면 실제 동작하는 코드를 봐야 한다.
[서블릿 컨테이너 초기화와 어플리케이션 초기화는 각각 다른거다. ]
AppInit
public interface AppInit {
void onStartup(ServletContext servletContext);
}
애플리케이션 초기화를 진행하려면 먼저 인터페이스를 만들어야 한다.
내용과 형식은 상관없고, 인터페이스는 꼭 필요하다.
예제 진행을 위해서 여기서는 AppInit 인터페이스를 만들자
앞서 개발한 애플리케이션 초기화( AppInit ) 인터페이스를 구현해서 실제 동작하는 코드를 만들어보자
public class AppInitV1Servlet implements AppInit{
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("AppInitV1Servlet.onStartup");
// 순수 서블릿 코드 등록 -> 서블릿을 프로그래밍 방식으로 등록, 서블릿이름, 서블릿객체를 파라미터로 전달
// 즉 서블릿 컨테이너에 해당 서클릿을 넣는것이다.
ServletRegistration.Dynamic helloServlet =
servletContext.addServlet("helloServlet", new HelloServlet());
// addMapping 메소드를 호출하여 등록된 서블릿이 특정 URL 패턴에 대해 요청을 처리하도록 설정
helloServlet.addMapping("/hello-servlet");
// 이런식으로 체인방식을 사용해도 된다.
// servletContext
// .addServlet("helloServlet", new HelloServlet())
// .addMapping("/hello-servlet");
}
}
여기서는 프로그래밍 방식으로 HelloServlet 서블릿을 서블릿 컨테이너에 직접 등록한다.
HTTP로 /hello-servlet 를 호출하면 HelloServlet 서블릿이 실행된다.
서블릿을 등록하는 2가지 방법
- @WebServlet 애노테이션
- 프로그래밍 방식
참고 - 프로그래밍 방식을 사용하는 이유
@WebServlet 을 사용하면 애노테이션 하나로 서블릿을 편리하게 등록할 수 있다.
하지만 애노테이션 방식을 사용하면 유연하게 변경하는 것이 어렵다. 마치 하드코딩 된 것 처럼 동작한다
[ 어노테이션 방식은 조건에 따라 동적으로 경로를 바꾸는것이 불가능하다. ]
아래 참고 예시를 보면 /test 경로를 변경하고 싶으면 코드를 직접 변경해야 바꿀 수 있다.
참고 - 예시
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {}
반면에 프로그래밍 방식은 코딩을 더 많이 해야하고 불편하지만 무한한 유연성을 제공한다.
예를 들어서
- /hello-servlet 경로를 상황에 따라서 바꾸어 외부 설정을 읽어서 등록할 수 있다.
- 서블릿 자체도 특정 조건에 따라서 if 문으로 분기해서 등록하거나 뺄 수 있다.
- 서블릿을 내가 직접 생성하기 때문에 생성자에 필요한 정보를 넘길 수 있다
서블릿 컨테이너 초기화
- 정의: 서블릿 컨테이너 초기화는 웹 애플리케이션이 시작될 때 서블릿 컨테이너가 수행하는 초기화 과정입니다. 이 단계에서는 서블릿 컨테이너가 필요한 리소스를 준비하고, 설정을 로드하며, 모든 서블릿을 초기화합니다.
- 특징:
- 서블릿 컨테이너는 web.xml 파일이나 ServletContainerInitializer 인터페이스를 통해 서블릿과 필터를 등록합니다.
- 서블릿의 init() 메소드가 호출되어 초기 설정이 이루어집니다.
- 이는 일반적으로 웹 애플리케이션의 전체적인 설정을 다루며, 서블릿을 포함한 모든 리소스가 준비된 후 애플리케이션이 사용자 요청을 처리할 수 있도록 합니다.
애플리케이션 초기화
- 정의: 애플리케이션 초기화는 특정 애플리케이션에 맞춘 초기화 작업을 의미합니다. 일반적으로 비즈니스 로직이나 특정 기능에 대한 초기 설정을 포함합니다.
- 특징:
- 애플리케이션 초기화는 서블릿 컨테이너 초기화 이후에 수행될 수 있으며, 개발자가 정의한 클래스를 통해 애플리케이션 특유의 설정을 수행합니다.
- 예를 들어, 특정 데이터베이스 연결, 애플리케이션 속성 로드, 외부 서비스 설정 등을 포함할 수 있습니다.
- 이 작업은 보통 ServletContextListener나 ServletContainerInitializer를 통해 수행되며, 애플리케이션의 비즈니스 로직에 따라 달라질 수 있습니다.
요약
- 서블릿 컨테이너 초기화는 서블릿 및 필터와 같은 웹 애플리케이션 구성 요소를 준비하는 과정이며, 주로 서버와 관련된 설정입니다.
- 애플리케이션 초기화는 비즈니스 로직 및 애플리케이션 특정 설정을 포함한 초기화 작업으로, 개발자가 필요에 따라 구현할 수 있습니다.
- 이 두 초기화 과정은 서로 다른 목적을 가지고 있으며, 각각의 단계에서 중요한 역할을 수행합니다.
서블릿 컨테이너 초기화( ServletContainerInitializer )는 앞서 알아보았다.
[ MyContainerV1를 통해서 ]
그런데 애플리케이션 초기화 ( AppInit )는 어떻게 실행되는 것일까?
다음 코드를 만들어 보자.
MyContainerInitV2
@HandlesTypes(AppInit.class) // 이렇게해주면 해당 인터페이스의 구현체가 밑에 onStartup()의 매개변수인 set에 들어간다.
public class MyContainerInitV2 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("MyContainerInitV2.onStartup");
System.out.println("MyContainerInitV2 Set<Class<?>> set = " + set);
System.out.println("MyContainerInitV2 servletContext = " + servletContext);
}
}
jakarta.servlet.ServletContainerInitializer
해당 파일에 초기화 클래스를 추가해주는것을 잊으면 안된다.
hello.container.MyContainerV1
hello.container.MyContainerInitV2
위의 현 상태의 MyContainerInitV2를 가지고 톰캣을 실행해보면
set 매개변수에 @HandlerTypes에 적힌 인터페이스의 구현체들을 저장해주는것을 확인할 수 있다.
MyContainerInitV2
다음과 같이 코드 수정
@HandlesTypes(AppInit.class) // 이렇게해주면 해당 구현체가 밑에 onStartup()의 매개변수인 set에 들어간다.
public class MyContainerInitV2 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("MyContainerInitV2.onStartup");
System.out.println("MyContainerInitV2 Set<Class<?>> set = " + set);
System.out.println("MyContainerInitV2 servletContext = " + servletContext);
// 이 set안에는 클래스가 들어오는게 아닌 클래스의 메타정보가 들어온다.
for (Class<?> appInitClass : set) {
try {
// 클래스 메타정보를 기반으로 생성자를 통해 객체를 생성해준다.
// 이 코드는 new AppInitV1Servlet(); 이랑 같은 코드라고 생각하면 된다.
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
appInit.onStartup(servletContext); // 애플리케이션 초기화 코드를 직접 실행
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
애플리케이션 초기화 과정
1. @HandlesTypes 애노테이션에 애플리케이션 초기화 인터페이스를 지정한다.
여기서는 앞서 만든 AppInit.class 인터페이스를 지정했다.
2. 서블릿 컨테이너 초기화( ServletContainerInitializer )는 파라미터로 넘어오는
Set<Class<?>> c 에 애플리케이션 초기화 인터페이스의 구현체들을 모두 찾아서
클래스 정보로 전달한다.
여기서는 @HandlesTypes(AppInit.class) 를 지정했으므로
AppInit.class 의 구현체인 AppInitV1Servlet.class 정보가 전달된다.
참고로 객체 인스턴스가 아니라 클래스 정보를 전달하기 때문에
실행하려면 객체를 생성해서 사용해야 한다.
서블릿 컨테이너 초기화가 먼저 일어나고 서블릿 컨테이너 초기화가 진행되는
도중에 어플리케이션 초기화 관련 객체들을 가져와서 어플리케이션 초기화를 실행시켜 주는것이다.
3. appInitClass.getDeclaredConstructor().newInstance()
리플렉션을 사용해서 객체를 생성한다.
참고로 이 코드는 new AppInitV1Servlet() 과 같다생각하면 된다.
4. appInit.onStartup(ctx)
애플리케이션 초기화 코드를 직접 실행하면서 서블릿 컨테이너 정보가 담긴 ctx도 함께 전달한다.
WAS를 실행해보자.
톰캣이 실행되면서 서블릿 컨테이너 초기화가 발생하는데 서블릿 컨테이너 초기화 과정속에서
어플리케이션 초기화도 진행된다.
어플리케이션 초기화 관련 클래스를 가져와서 어플리케이션 초기화를 진행하므로
로그상에서 onStartup이라는 어플리케이션 초기화 메소드가 실행된것을 볼 수 있다.
그러므로 어플리케이션 초기화에서 등록한 HelloServlet의 기능또한 잘 동작하는것을 볼 수 있다.
ServletRegistration.Dynamic helloServlet =
servletContext.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
이렇게 HelloServlet이 ServletContext에 등록되었고
HelloServlet의 기능은 다음과 같았다.
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("HelloServlet.service");
resp.getWriter().println("hello servlet!");
}
톰캣실행시
잘 동작하는것을 확인가능.
초기화는 다음 순서로 진행된다.
1. 서블릿 컨테이너 초기화 실행
resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
2. 애플리케이션 초기화 실행
@HandlesTypes(AppInit.class)
jakarta.servlet.ServletContainerInitializer
여기 등록되어있는 서블릿 컨테이너 초기화 클래스들을 하나씩 실행하면서
서블릿 컨테이너 초기화를 실행한다.
서블릿 컨테이너 초기화 클래스를 실행하면서
@HandlesTypes 어노테이션을 만나면 해당 인터페이스의 구현체의 메타정보를 가져와서 전달
메타정보를 이용하여 객체 생성 및 실행하여 어플리케이션 초기화까지 실행된다.
참고
서블릿 컨테이너 초기화만 있어도 될 것 같은데, 왜 이렇게 복잡하게 애플리케이션 초기화라는 개념을 만들었을까?
편리함
서블릿 컨테이너를 초기화 하려면 ServletContainerInitializer 인터페이스를 구현한 코드를 만들어야 한다.
여기에 추가로 META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에
해당 코드를 직접 지정해주어야 한다.
하지만 애플리케이션 초기화는 특정 인터페이스만 구현하면 된다.
[@HandlerTypes를 이용해 해당 인터페이스의 구현체는 다 가져와 실행될것이니까]
의존성
애플리케이션 초기화는 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있다.
이를 통해 애플리케이션 초기화 코드가 서블릿 컨테이너에 대한 의존을 줄일 수 있다.
특히 ServletContext ctx 가 필요없는 애플리케이션 초기화 코드라면 의존을 완전히 제거할 수도 있다.
참고
클래스 메타정보란 클래스 자체에 대한 구조와 속성 정보를 담고 있는 데이터를 말합니다.
메타정보에는 클래스의 이름, 필드, 메소드, 생성자, 상속 관계, 접근 제한자 등이 포함됩니다.
이 정보를 통해 프로그램이 실행 중에도 클래스의 내부 구조에 접근하고 조작할 수 있습니다.
자바에서는 Class 클래스를 통해 이 메타정보에 접근할 수 있습니다.
클래스 메타정보의 활용 예시
Java에서 클래스 메타정보는 리플렉션(Reflection)이라는 기법을 통해 주로 활용됩니다. 리
플렉션을 사용하면 특정 클래스의 메타정보를 조회하고, 프로그램 실행 중에 해당 클래스의 객체를 생성하거나
메소드를 호출하는 등의 작업을 할 수 있습니다.
예를 들어, 아래와 같이 String 클래스의 메타정보를 가져올 수 있습니다:
Class<?> clazz = String.class;
// 클래스 이름 가져오기
System.out.println("Class Name: " + clazz.getName());
// 메소드 정보 가져오기
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method: " + method.getName());
}
// 필드 정보 가져오기
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field: " + field.getName());
}
클래스 메타정보에 포함되는 주요 요소
- 클래스 이름: 클래스의 전체 이름(패키지 포함).
- 필드 정보: 필드(변수) 이름, 타입, 접근 제어자 등.
- 메소드 정보: 메소드 이름, 반환 타입, 매개변수 타입, 접근 제어자 등.
- 생성자 정보: 생성자의 매개변수 타입 등.
- 상속 및 인터페이스 정보: 상속받은 클래스나 구현된 인터페이스 정보.
메타정보의 활용 사례
- 프레임워크: 스프링과 같은 프레임워크는 리플렉션을 사용해 애노테이션을 읽고, 클래스 메타정보를 이용해 빈(Bean)을 생성하고 설정합니다.
- 객체지향 설계 도구: 클래스 다이어그램 생성 도구는 클래스 메타정보를 읽어 자동으로 다이어그램을 생성합니다.
- 유닛 테스트: JUnit 같은 테스트 프레임워크는 테스트 메소드를 동적으로 찾기 위해 클래스 메타정보를 활용합니다.
이처럼 메타정보는 코드 작성 시점에 알 수 없는 클래스나 메소드를 동적으로 처리하는 데 큰 역할을 하며,
Java의 강력한 기능 중 하나입니다.
리플렉션(Reflection)은 Java 프로그램이 실행 중에 클래스, 인터페이스, 필드, 메소드 등의 정보(메타정보)를 동적으로 분석하고, 조작할 수 있는 기능입니다. 리플렉션을 사용하면 코드 작성 시점에 알지 못하는 클래스에 접근하고, 해당 클래스의 객체를 생성하거나 메소드를 호출하는 등의 작업을 수행할 수 있습니다.
리플렉션의 주요 기능
- 클래스 정보 조회: 클래스 이름, 필드, 메소드, 생성자, 접근 제한자 등 클래스의 구조를 실행 중에 확인할 수 있습니다.
- 객체 생성: 리플렉션을 사용해 클래스의 기본 생성자 또는 특정 생성자를 호출하여 객체를 동적으로 생성할 수 있습니다.
- 메소드 호출: 메소드 이름과 매개변수를 통해 메소드를 호출할 수 있습니다.
- 필드 접근 및 수정: 필드 값을 읽거나 수정할 수 있습니다. private 필드에도 접근이 가능합니다(다만 보안 설정이 허용할 때)
리플렉션 예시
리플렉션을 사용해 클래스의 객체를 생성하는 예를 들어보겠습니다.
아래 코드에서 appInitClass.getDeclaredConstructor().newInstance()를 통해 인스턴스를 생성하는 과정을 포함하여 자세히 설명합니다.
// 특정 클래스 정보를 가져온다. 여기서는 "AppInitV1Servlet"을 가정
Class<?> appInitClass = Class.forName("AppInitV1Servlet");
// 기본 생성자를 가져와서 객체를 생성한다.
Object appInitInstance = appInitClass.getDeclaredConstructor().newInstance();
// 생성된 인스턴스가 AppInit 타입인지 확인하고, 맞다면 캐스팅
if (appInitInstance instanceof AppInit) {
AppInit appInit = (AppInit) appInitInstance;
appInit.onStartup(servletContext);
}
코드 설명
- Class 객체 얻기: Class.forName("AppInitV1Servlet")을 통해 "AppInitV1Servlet"이라는 클래스의 정보를 가져옵니다. appInitClass는 Class<?> 타입의 객체로, 해당 클래스에 대한 모든 메타정보를 담고 있습니다.
- 객체 생성:
- appInitClass.getDeclaredConstructor().newInstance()를 통해 AppInitV1Servlet의 기본 생성자를 호출하여 인스턴스를 생성합니다. 이때 getDeclaredConstructor()는 AppInitV1Servlet 클래스의 기본 생성자를 가져오는 역할을 합니다.
- newInstance()는 기본 생성자를 호출하여 인스턴스를 생성하고 반환합니다.
- 인스턴스 타입 확인: 리플렉션을 통해 생성된 객체가 AppInit 인터페이스를 구현하는지 확인하기 위해 instanceof를 사용합니다.
- 메소드 호출: 조건이 만족되면 appInit.onStartup(servletContext);처럼 해당 인스턴스의 메소드를 호출할 수 있습니다.
리플렉션의 장점
- 유연성: 컴파일 시점에 클래스를 몰라도, 런타임에 클래스 정보를 동적으로 가져올 수 있어 프로그램의 유연성을 높입니다.
- 프레임워크 개발: 스프링과 같은 프레임워크는 리플렉션을 사용하여 의존성 주입과 애노테이션 처리를 수행합니다.
리플렉션의 단점
- 성능 저하: 일반적인 메소드 호출에 비해 리플렉션은 느립니다.
- 보안 문제: 접근 제한자를 무시할 수 있으므로 보안 이슈가 발생할 수 있습니다.
- 오류 발견 어려움: 컴파일 시점에 오류를 확인하기 어렵고, 실행 시점에 오류가 발생할 수 있습니다.
리플렉션은 주로 프레임워크나 라이브러리 개발에서 많이 사용되며, 일반적인 애플리케이션 개발에서는 필요한 경우에만 사용하는 것이 좋습니다.
스프링 컨테이너 등록
이번에는 WAS와 스프링을 통합해보자.
앞서 배운 서블릿 컨테이너 초기화와 애플리케이션 초기화를 활용하면 된다.
다음과 같은 과정이 필요할 것이다.
- 스프링 컨테이너 만들기
- 스프링MVC 컨트롤러를 스프링 컨테이너에 빈으로 등록하기
- 스프링MVC를 사용하는데 필요한 디스패처 서블릿을 서블릿 컨테이너 등록하기
서블릿 컨테이너와 스프링 컨테이너
현재 라이브러리에는 스프링 관련 라이브러리가 전혀 없다.
스프링 관련 라이브러리를 추가하자.
build.gradle
spring-webmvc 추가
dependencies {
//서블릿
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
//스프링 MVC 추가
implementation 'org.springframework:spring-webmvc:6.0.4'
}
spring-webmvc 라이브러리를 추가하면 스프링 MVC 뿐만 아니라
spring-core 를 포함한 스프링 핵심 라이브러리들도 함께 포함된다
참고
implementation 'org.springframework.boot:spring-boot-starter-web'을 추가하면
implementation 'org.springframework:spring-webmvc:6.0.4'는 별도로 추가할 필요가 없다.
spring-boot-starter-web은 Spring Boot에서 제공하는 스타터로,
웹 애플리케이션을 만들기 위한 필수 의존성들이 이미 포함되어 있습니다.
이 스타터에는 spring-webmvc와 spring-core, spring-context 등이 포함되어 있어,
별도로 spring-webmvc를 추가하지 않아도 됩니다.
HelloController
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("HelloController.hello");
return "hello spring!";
}
}
간단한 스프링 컨트롤러다.
실행하면 HTTP 응답으로 hello spring! 이라는 메시지를 반환한다.
HelloConfig
@Configuration
public class HelloConfig {
/*
@Controller 가 이미 붙어있기 컴포넌트 스캔으로 자동 빈 등록이 되지만
설정클래스를 이용해서 스프링컨테이너에 빈 등록이 됨을 보여주기 위해 수동 빈 등록 코드 추가
*/
@Bean
public HelloController helloController() {
return new HelloController();
}
}
컨트롤러를 스프링 빈으로 직접 등록한다.
참고로 여기서는 컴포넌트 스캔을 사용하지 않고 빈을 직접 등록 했다.
이제 애플리케이션 초기화를 사용해서 서블릿 컨테이너에 스프링 컨테이너를 생성하고 등록하자.
AppInitV2Spring
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 dispatcherServlet = new DispatcherServlet(appContext);
// 디스패처 서블릿이 요청을 받아서 요청을 처리할수있는 컨트롤러 빈을 찾기 때문에 위와 같이
// 디스패치 서블릿에 스프링 컨테이너를 넣어 생성하는것은 자연스럽다.
// 서블릿 컨테이너에 디스패처 서블릿을 등록 [ 이름이 중복되면 안된다. ]
servletContext
.addServlet("dispatcherV2", dispatcherServlet)
.addMapping("/spring/*");
}
}
AppInitV2Spring 는 AppInit 을 구현했다.
AppInit 을 구현하면 애플리케이션 초기화 코드[ onStartup() ]가 자동으로 실행된다.
앞서 MyContainerInitV2 에 관련 작업을 이미 해두었다
[ jakarta.servlet.ServletContainerInitializer에 MyContainerInitV2 추가 및
MyContainerInitV2 에는 @HandlesTypes(AppInit.class) 존재 ]
스프링 컨테이너 생성
AnnotationConfigWebApplicationContext가 바로 스프링 컨테이너이다.
AnnotationConfigWebApplicationContext 부모를 따라가 보면 ApplicationContext 인터페이스를 확인할 수 있다.
이 구현체는 이름 그대로 애노테이션 기반 설정과 웹 기능을 지원하는 스프링 컨테이너로 이해하면 된다.
[ ApplicationContext == 스프링 컨테이너 ]
appContext.register(HelloConfig.class)
컨테이너에 스프링 설정을 추가한다.
스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
ispatcherServlet dispatcherServlet = new DispatcherServlet(appContext)
코드를 보면 스프링 MVC가 제공하는 디스패처 서블릿을 생성하고,
생성자에 앞서 만든 스프링 컨테이너를 전달하는 것을 확인할 수 있다.
이렇게 하면 디스패처 서블릿에 스프링 컨테이너가 연결된다
이 디스패처 서블릿에 HTTP 요청이 오면 디스패처 서블릿은
해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출한다.
[ 요청을 처리할 수 있는 핸들러 메소드를 가진 컨트롤러 빈을 호출한다.]
디스패처 서블릿을 서블릿 컨테이너에 등록
servletContext.addServlet("dispatcherV2", dispatcher)
디스패처 서블릿을 서블릿 컨테이너에 등록한다.
servletContext
.addServlet("dispatcherV2", dispatcherServlet)
.addMapping("/spring/*");
/spring/* 요청이 디스패처 서블릿을 통하도록 설정
/spring/* 이렇게 경로를 지정하면 /spring 과 그 하위 요청은 모두 해당 서블릿을 통하게 된다
/spring/hello-spring
/spring/hello/go
원래대로라면 디스패처 서블릿은 모든 요청에 대해 처리하니까
addMapping을 "/*" 이런식으로 모든경로에 매핑되게끔 했을 것 같다.
디스패치 서블릿을 여러개만들어서 운영하는 경우는 하나의 서버에 여러 어플리케이션을 운영한다든지 등 특수한 경우에 사용되는것 같다.
일반적인 스프링 웹 애플리케이션에서는
보통 /*로 모든 요청을 매핑
하나의 디스패처 서블릿이 모든 요청을 처리
디스패치 서블릿을 여러개 사용하면 다음과 같은 상황에서 유용할 수 있습니다:
- 하나의 서버에서 여러 독립적인 애플리케이션 운영
- 서로 다른 설정이나 보안 정책이 필요한 경우
- 레거시 시스템과 새로운 시스템을 함께 운영하는 경우
주의!
서블릿을 등록할 때 이름은 원하는 이름을 등록하면 되지만 같은 이름으로 중복 등록하면 오류가 발생한다.
여기서는 dispatcherV2 이름을 사용했는데, 이후에 하나 더 등록할 예정이기 때문에 이름에 유의하자
WAS를 실행해보면
어플리케이션 초기화가 잘 된것을 확인가능
http://localhost:8080/spring/hello-spring
요청시
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("HelloController.hello");
return "hello spring!";
}
}
해당 컨트롤러의 hell() 메소드가 실행된것을 확인 가능하다.
디스패처 서블릿을
.addMapping("/spring/*");
이렇게 매핑해놨으므로
/spring/hello-spring 로 요청이 들어온다면
/spring 이후의 /hello-spring을 처리할 수 있는 핸들러메소드를 가지는 컨트롤러 빈을
스프링 컨테이너에서 찾아서 처리하는것 같다.
디스패처 서블릿은 어떤 특정 url 패턴에 대해 해당 서블릿이 처리하도록 설정 할 수 있고
그래서 특정 url 패턴 뒤쪽의 url을 가지고 컨트롤러를 호출하는것 같다.
- 디스패처 서블릿 매핑 설정:
- dispatcherServlet.addMapping("/spring/*")로 설정했기 때문에, /spring/* 경로 하위의 모든 요청은 디스패처 서블릿을 통해 처리됩니다. 즉, /spring/hello-spring 경로로 요청이 들어오면 디스패처 서블릿이 이를 받아들입니다.
- URL 경로 분석 및 컨트롤러 메소드 호출:
- 디스패처 서블릿은 /spring/ 이후의 경로인 /hello-spring을 가지고, 해당 경로를 처리할 수 있는 핸들러 메소드가 있는 컨트롤러 빈을 스프링 컨테이너에서 찾습니다.
- 요청 경로에 맞는 @GetMapping("/hello-spring")이 지정된 HelloController의 hello() 메소드가 핸들러로 매핑됩니다.
- 컨트롤러 메소드 호출:
- 매핑된 메소드가 실행되며, hello spring!을 반환합니다.
- 디스패처 서블릿은 반환된 응답을 HTTP 응답으로 전달하여 클라이언트가 결과를 확인할 수 있도록 합니다.
실행 과정 정리
/spring/hello-spring
실행을 /spring/* 패턴으로 호출했기 때문에 다음과 같이 동작한다
dispatcherV2 디스패처 서블릿이 실행된다. ( /spring )
dispatcherV2 디스패처 서블릿은 스프링 컨트롤러를 찾아서 실행한다. ( /hello-spring )
이때 서블릿을 찾아서 호출하는데 사용된 /spring 을 제외한
/hello-spring 가 매핑된 컨트롤러 HelloController )의 메서드를 찾아서 실행한다
(쉽게 이야기해서 뒤에 * 부분으로 스프링 컨트롤러를 찾는다.)
웹 어플리케이션(WAS)이 실행될때 서블릿 컨테이너가 초기화되고
서블릿 컨테이너가 초기화 될때 어플리케이션 초기화가 되는데
그때 어플리케이션 초기화 관련 인터페이스의 구현체가 생성 및 실행된다.
스프링 MVC 서블릿 컨테이너 초기화 지원
지금까지의 과정을 생각해보면 서블릿 컨테이너를 초기화 하기 위해 다음과 같은 복잡한 과정을 진행했다.
ServletContainerInitializer 인터페이스를 구현해서 서블릿 컨테이너 초기화 코드를 만들었다.
여기에 애플리케이션 초기화를 만들기 위해 @HandlesTypes 애노테이션을 적용했다.
/META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스 경로를 등록했다.
서블릿 컨테이너 초기화 과정은 상당히 번거롭고 반복되는 작업이다.
스프링 MVC는 이러한 서블릿 컨테이너 초기화 작업을 이미 만들어두었다.
덕분에 개발자는 서블릿 컨테이너 초기화 과정은 생략하고, 애플리케이션 초기화 코드만 작성하면 된다.
스프링이 지원하는 애플리케이션 초기화를 사용하려면 다음 인터페이스를 구현하면 된다
WebApplicationInitializer
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
스프링이 지원하는 애플리케이션 초기화 코드를 사용해보자.
AppInitV3SpringMvc
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 dispatcherServlet = new DispatcherServlet(appContext);
// 디스패처 서블릿이 요청을 받아서 요청을 처리할수있는 컨트롤러 빈을 찾기 때문에 위와 같이
// 디스패치 서블릿에 스프링 컨테이너를 넣어 생성하는것은 자연스럽다.
// 서블릿 컨테이너에 디스패처 서블릿을 등록 [ 이름이 중복되면 안된다. ]
servletContext
.addServlet("dispatcherV3", dispatcherServlet)
.addMapping("/"); // 해당 디스패처 서블릿이 모든 요청을 처리하도록 수정
}
}
WebApplicationInitializer 인터페이스를 구현한 부분을 제외하고는 이전의 AppInitV2Spring과 거의 같은 코드이다
WebApplicationInitializer는 스프링이 이미 만들어둔 애플리케이션 초기화 인터페이스이다
여기서도 디스패처 서블릿을 새로 만들어서 등록하는데, 이전 코드에서는 dispatcherV2 라고 했고,
여기서는 dispatcherV3 라고 해주었다.
참고로 이름이 같은 서블릿을 등록하면 오류가 발생한다
servlet.addMapping("/") 코드를 통해 모든 요청이 해당 서블릿을 타도록 했다.
따라서 다음과 같이 요청하면 해당 디스패처 서블릿을 통해 /hello-spring 이 매핑된 컨트롤러 메서드가 호출된다.
우선순위는 더 구체적인 것이 먼저 실행된다.
스프링 컨테이너 또한 V2,V3 디스패처 서블릿 각각 만들어 줬기 때문에 2개가 된다.
두 스프링 컨테이너 다 HelloController 빈을 가지고있는 이유는 같은 스프링 설정 클래스를 등록 했기 때문이다.
참고
여기서는 이해를 돕기 위해 디스패처 서블릿도 2개 만들고, 스프링 컨테이너도 2개 만들었다.
일반적으로는 스프링 컨테이너를 하나 만들고, 디스패처 서블릿도 하나만 만든다.
그리고 디스패처 서블릿 의 경로 매핑도 / 로 해서 하나의 디스패처 서블릿을 통해서 모든 것을 처리하도록 한다.
스프링 MVC가 제공하는 서블릿 컨테이너 초기화 분석
스프링은 어떻게 WebApplicationInitializer 인터페이스 하나로 애플리케이션 초기화가 가능하게 할까?
스프링도 결국 서블릿 컨테이너에서 요구하는 부분을 모두 구현해야 한다.
spring-web 라이브러리를 열어보면 서블릿 컨테이너 초기화를 위한 등록 파일을 확인할 수 있다.
그리고 이곳에 서블릿 컨테이너 초기화 클래스가 등록되어 있다.
org.springframework.web.SpringServletContainerInitializer
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
코드를 보면 우리가 앞서 만든 서블릿 컨테이너 초기화 코드와 비슷한 것을 확인할 수 있다.
@HandlesTypes 의 대상이 WebApplicationInitializer 이다.
그리고 이 인터페이스의 구현체를 생성하고 실행하는 것을 확인할 수 있다.
우리는 앞서 이 인터페이스를 구현했다.
초록색 영역은 이미 스프링이 만들어서 제공하는 영역이다.
정리
스프링 MVC도 우리가 지금까지 한 것 처럼 서블릿 컨테이너 초기화 파일에 초기화 클래스를 등록해두었다.
그리고 WebApplicationInitializer 인터페이스를 애플리케이션 초기화 인터페이스로 지정해두고,
이것을 생성해서 실행한다
따라서 스프링 MVC를 사용한다면 WebApplicationInitializer 인터페이스만 구현하면 AppInitV3SpringMvc에서 본 것 처럼 편리하게 애플리케이션 초기화를 사용할 수 있다.
지금까지 서블릿 컨테이너 초기화를 사용해서 필요한 서블릿도 등록하고,
스프링 컨테이너도 생성해서 등록하고
또 스프링 MVC가 동작하도록 디스패처 서블릿도 중간에 연결해보았다.
그리고 스프링이 제공하는 좀 더 편리한 초기화 방법도 알아보았다.
지금까지 알아본 내용은 모두 서블릿 컨테이너 위에서 동작하는 방법이다.
따라서 항상 톰캣 같은 서블릿 컨테이너에 배포를 해야만 동작하는 방식이다.
과거에는 서블릿 컨테이너 위에서 모든 것이 동작했지만,
스프링 부트와 내장 톰캣을 사용하면서 이런 부분이 바뀌기 시작했다.
'인프런 > 스프링 부트 - 핵심 원리와 활용' 카테고리의 다른 글
6) 자동 구성(Auto Configuration) (2) | 2024.11.08 |
---|---|
5) 스프링 부트 스타터와 라이브러리 관리 (0) | 2024.11.07 |
4) 스프링 부트와 내장 톰캣 (2) (2) | 2024.11.05 |
3) 스프링 부트와 내장 톰캣 (1) (2) | 2024.11.04 |
1) 스프링부트 소개, 톰캣 설정 (3) | 2024.10.31 |
댓글