1️⃣ 서블릿 등록
🔎 @ServletComponentScan
스프링 컨테이너에 빈을 등록할 때 '@ComponentScan'을 사용할 수 있다. 서블릿 역시 서블릿 컨테이너에 서블릿을 등록하기 위해선 '@ServletComponentScan'을 사용한다. 애플리케이션을 실행하는 클래스에 붙여 하위 경로에 존재하는 모든 서블릿 클래스를 등록한다.
사실 오늘 서블릿으로 MVC 패턴을 공부하다가 '@WebServlet'을 통해 서블릿을 등록했는데 인식을 못하는 문제가 있었다. 왜그런가 생각을 해보니 서블릿을 나타내는 애노테이션만 넣었을 뿐 이를 서블릿 컨테이너에 등록하는 애노테이션은 넣지 않았기 때문이다. 따라서 서블릿을 인식하지 못하는 문제가 생겼다면 애플리케이션 실행 클래스를 다시 살펴보길 바란다. 다음과 같이 등록해야 한다.
@ServletComponentScan
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
🔎 @ComponentScan
그렇다면 '@ServletComponentScan'은 서블릿들을 어떻게 인식하고 등록하는지 궁금해진다. 그 전에 스프링의 '@ComponentScan' 애노테이션을 살펴보자. 해당 애노테이션은 자동으로 빈을 인식하여 스프링 컨테이너에 등록하는 역할을 한다. 그리고 이 때 스캔 대상은 '@Component' 애노테이션을 가진 객체들이다.
- @Component
- @Controller
- @Service
- @Repository
보이는 것처럼 해당 애노테이션들은 내부에 '@Component' 애노테이션이 포함되어 있다. 따라서 '@ComponentScan'은 해당 애노테이션이 포함된 클래스들을 스프링 컨테이너에 등록하게 되는 것이다. 이는 컴포넌트 스캔과 관련된 글에서 좀 더 자세히 다뤄보겠다.
문득 '@ServletComponentScan' 도 이러한 방식으로 동작을 하는 것인지 궁금했다. 그래서 공식 문서를 찾아보거나 관련 코드를 찾아보았다.
2️⃣ @ServletComponentScan 동작 원리
1) 애플리케이션 구동 및 컨텍스트 초기화
일반적으로 Spring Boot 애플리케이션에서 'SpringApplication.run()' 메서드가 호출되면 내부적으로 'ApplicatonContext'를 설정하고 초기화한다. 또한 Spring Boot 내부에 Tomcat, Jetty 등의 서블릿 컨테이너가 내장되어 있어 애플리케이션이 시작될 때 서블릿 컨테이너 또한 같이 시작된다. 이 단계에서 '@ServletComponentScan'이 있는 클래스가 스캔되는데, 다음과 같은 과정을 거친다.
- 'SpringApplication' 클래스 : 애플리케이션 시작시 'SpringApplicationRunListeners' 등록 및 실행
- 'SpringApplicationRunListeners' : 'SpringApplicationRunListener'의 구현체들을 관리하며, 애플리케이션 실행 중 발생하는 이벤트들을 처리. 이 중 하나가 'ContextRefreshedEvent'
- 'ContextRefreshedEvent'가 발생하면, 'SpringApplicationRunListeners'는 해당 이벤트를 수신하여 등록된 리스너에게 이벤트를 전달
- 해당 이벤트를 받은 리스너 중에는 'ServletComponentScanRegisterar'가 등록된 경우가 있음. 이 리스너가 '@ServletComponentScan' 을 처리
- 'ServletComponentScanRegistrar'는 '@ServletComponentScan' 이 붙은 클래스를 찾고, 해당 클래스나 하위 패키지를 기준으로 서블릿 컴포넌트를 스캔하고 등록
'@ServletComponent' 애노테이션이 사용된 클래스를 찾는 'ServletComponentScanRegistrar' 클래스가 중요하다고 생각하여 해당 클래스를 뜯어봤다.
'registerBeanDefinitions()' 메서드는 빈 정의를 등록하는 작업을 수행한다. 인자로 'AnnotationMetadata' 객체를 통해 '@ServletComponentScan'의 속성들을 읽어온다. 만약 registry에 'servletComponentRegisteringPostProcessor'가 있다면 업데이트를, 없다면 추가를 한다.
해당 메서드 내부에서는 'getPackagesToScan()' 메서드를 호출하는데, 다음과 같다.
1. ServletComponentScan.class.getName() 으로 애노테이션 이름을 가져온다. 이후 metadata.getAnnotationAttributes() 를 통해 애노테이션의 속성들을 읽어온 뒤, 이를 'AnnotationAttributes' 객체로 변환한다.
2. basePackages 배열에는 'AnnotationAttributes' 객체에서 'basePackages' 속성이 문자열로 바뀌어 담긴다. 해당 배열에는 패키지 이름들이 포함된다.
3. basePackageClasses 배열에는 'basePackageClasses' 속성을 읽어온 클래스들이 담긴다. 여기엔 패키지를 나타내는 클래스들이 포함된다.
4. basePackages 배열에 포함된 클래스들의 패키지 이름을 추출하여 'packageToScan'에 추가한다.
5. 만약 위 두 배열이 모두 비었을 경우, 현재 클래스의 패키지를 스캔 대상으로 추가한다.
위 과정을 통해 'getPackagesToScan()' 메서드는 '@ServletComponentScan' 에서 정의된 패키지들을 결정하고, 이를 Set<String> 형태로 반환한다. 이렇게 반환된 패키지들은 서블릿 컴포넌트를 스캔할 대상으로 사용된다.
'addPostProcessor()' 메서드는 스캔된 패키지에 대한 후처리기를 생성하고 등록한다. 이는 스캔된 패키지 내에서 발견된 서블릿 컴포넌트들을 등록하는 역할을 수행한다.
2) 애노테이션 스캔
'ServletComponentScanRegistrar'는 @ServletComponentScan'이 붙은 클래스를 찾고, 해당 클래스나 해당 클래스가 속한 패키지를 기준으로 애노테이션 스캔이 수행된다. 스캔 대상은 해당 패키지 및 하위 패키지에 존재하는 모든 클래스들이며, 서블릿 컴포넌트를 스캔하고 등록한다.
3) 서블릿 컴포넌트 찾기
애노테이션을 스캔하다가 '@WebServlet', '@WebFilter', '@WebListener' 애노테이션이 발견되면 해당 클래스를 서블릿 컨테이너에 등록할 대상으로 식별한다. 공식 문서에서도 해당 내용을 찾을 수 있다.
4) 서블릿 컨테이너 등록
찾아낸 서블릿 컴포넌트들을 서블릿 컨테이너에 등록한다. 여기서 서블릿, 필터, 리스너 등을 적절한 위치에 등록하고 구성한다. 서블릿 컴포넌트들을 등록하는 역할은 일반적으로 'ServletRegisterationBean' 및 'FilterRegistrationBean'과 같은 Spring Boot의 내장 클래스들이 담당하게 된다.
5) 스프링 빈과 통합
서블릿 컨테이너에 등록된 서블릿 컴포넌트들과 스프링 빈(Spring Bean)들이 통합된다.
Servlet Component | Spring Bean | |
역할 | HTTP 요청 및 응답 처리 등 | 비즈니스 로직, 데이터 액세스 등 |
생명주기 | 서블릿 컨테이너에 의해 관리 | Spring IoC 컨테이너에 의해 관리 |
의존성 주입 | 불가능 | 가능 |
기능 확장 및 수정 | 제한적 | 용이 |
특징 | 웹 애플리케이션 핵심 기능 담당 | Spring Framework 핵심 구성 요소 |
- Spring Boot은 ApplicationContext 내에서 서블릿 컨테이너에 등록된 서블릿 컴포넌트들을 관리한다.
- 서블릿 컴포넌트들은 스프링 컨테이너에 등록된 스프링 빈과 함께 동작한다.
- 서블릿 컴포넌트들과 스프링 빈들이 통합되면서 애플리케이션의 기능을 효과적으로 개발하고 실행할 수 있다.
6) 애플리케이션 실행
모든 설정이 완료되면 애플리케이션이 실행된다.
참고 자료