스프링 부트는 스프링 프레임워크를 쉽게 사용할 수 있게 도와주는 도구일 뿐이다.
일반적으로 웹 애플리케이션 요청은 웹 서버를 통해 먼저 들어온다. 웹 서버는 정적 리소스(HTML, CSS, JS 등)를 처리하거나, 동적 처리가 필요한 요청은 WAS(Web Application Server)로 전달한다. 대표적인 WAS로는 Tomcat이 있으며, 이는 웹 서버와 서블릿 컨테이너(WAS 기능)를 함께 갖춘 구조이다.
WAS는 서블릿 컨테이너를 통해 HTTP 요청을 처리한다. 이 서블릿 컨테이너가 WAS의 핵심 구성 요소로, 서블릿의 생명주기 관리, 요청 매핑, 응답 반환 등을 담당한다.
Spring MVC와 같은 프레임워크를 사용할 때, HTTP 요청을 곧바로 Spring 컨테이너로 전달할 수는 없다. 먼저 서블릿 컨테이너가 요청을 받아야 하며, Spring MVC는 DispatcherServlet이라는 서블릿을 기본 서블릿 컨테이너에 등록함으로써 이 문제를 해결한다.
DispatcherServlet은 HTTP 요청을 받아 내부적으로 Spring ApplicationContext(Spring 컨테이너)와 연동하여 컨트롤러를 호출하고, 비즈니스 로직을 실행한 뒤, 응답을 반환한다.
WAS(Web Application Server)는 단순히 서블릿 컨테이너만 실행하는 것이 아니라, 애플리케이션 구동 시 개발자가 정의한 별도의 컨테이너(예: 스프링 컨테이너, 사용자 정의 초기화 로직 등)를 등록할 수 있도록 확장 구조를 제공한다. 이 확장 지점의 핵심이 바로 ServletContainerInitializer이다.
ServletContainerInitializer는 Java EE 6부터 도입된 인터페이스로, WAS가 웹 애플리케이션을 시작할 때 실행된다. 이를 구현하면 애플리케이션 시작 시점에 특정 클래스들을 탐색하고, 동적으로 서블릿/필터/리스너 등록 등의 초기화 로직을 수행할 수 있다.
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
사용 목적
등록 방법
ServletContainerInitializer 구현체는 JAR 파일 내에 다음과 같은 경로에 등록되어야 한다:
META-INF/services/javax.servlet.ServletContainerInitializer
이 파일에는 해당 인터페이스의 구현 클래스 FQCN을 명시한다:
org.example.MyInitializer
WAS는 이 파일을 읽고 구현체를 자동으로 로딩하여 onStartup()을 호출한다.
@HandlesTypes 어노테이션을 함께 사용하면, 특정 타입(예: 인터페이스, 애노테이션 등)을 구현하거나 사용하는 클래스들을 자동으로 탐지할 수 있다. 이 클래스 집합이 onStartup() 메서드의 인자로 전달된다.
@HandlesTypes(MyContainerMarker.class)
public class MyContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> classes, ServletContext ctx) {
// 외부 컨테이너 초기화
MyContainer container = new MyContainer();
// MyContainerMarker를 구현한 클래스들을 등록
for (Class<?> clazz : classes) {
container.register(clazz);
}
ctx.setAttribute("myContainer", container); // ServletContext에 저장
}
}
실제 예: SpringServletContainerInitializer
Spring Framework도 이 구조를 그대로 사용한ㄴ다.
dependencies {
implementation 'org.springframework:spring-web:5.3.32'
implementation 'org.springframework:spring-webmvc:5.3.32'
implementation 'org.apache.tomcat.embed:tomcat-embed-core:9.0.85'
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.85'
implementation 'javax.servlet:javax.servlet-api:4.0.1'
}
public class MySpringApp {
public static void main(String[] args) throws Exception {
// 1. 톰캣 객체 생성
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
// 2. 웹 애플리케이션 루트 디렉토리 설정
String docBase = new File(".").getAbsolutePath();
Context context = tomcat.addContext("", docBase);
// 3. 스프링 컨텍스트 생성 및 DispatcherServlet 등록
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(AppConfig.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
Tomcat.addServlet(context, "dispatcher", dispatcherServlet);
context.addServletMappingDecoded("/", "dispatcher");
// 4. 톰캣 실행
tomcat.start();
tomcat.getServer().await();
}
}
흐름 요약
1. Tomcat 객체를 직접 생성하여 포트 및 컨텍스트 설정
2. AnnotationConfigWebApplicationContext를 통해 스프링 컨테이너 생성
3. DispatcherServlet을 생성하고 컨텍스트에 등록
4. tomcat.start() → 톰캣 부팅
5. tomcat.getServer().await() → 서버 요청 대기

java -cp myapp.jar:lib/* com.example.Main
java -jar myapp-fat.jar
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
shadowJar {
archiveBaseName.set('myapp')
archiveClassifier.set('')
archiveVersion.set('1.0')
}
임베디드 톰캣과 스프링 프레임워크를 함께 빌드하기 위해서는 fatJar 방식으로 하나의 jar 파일을 만들어야 한다. 하지만 fatJar 방식에는 치명적인 단점이 있다. 라이브러리들의 클래스 또는 파일이 겹치면 파일 충돌이 발생한다. 이러한 fatJar의 문제를 스프링 부트는 해결했다.
public class MySpringApplication {
public static void run(Class configClass, String[] args) {
System.out.println("MySpringBootApplication.run args=" + List.of(args));
//톰캣 설정
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new
AnnotationConfigWebApplicationContext();
appContext.register(configClass);
//스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처 서블릿 등록
Context context = tomcat.addContext("", "/");
tomcat.addServlet("", "dispatcher", dispatcher);
context.addServletMappingDecoded("/", "dispatcher");
try {
tomcat.start();
} catch (LifecycleException e) {
throw new RuntimeException(e);
}
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}
@MySpringBootApplication
public class MySpringBootMain {
public static void main(String[] args) {
System.out.println("MySpringBootMain.main");
MySpringApplication.run(MySpringBootMain.class, args);
}
}
public class OnProdProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String profile = context.getEnvironment().getProperty("spring.profiles.active");
return "prod".equals(profile);
}
}
@Configuration
@Conditional(OnProdProfileCondition.class)
public class ProdOnlyConfig {
@Bean
public DataSource prodDataSource() {
return new HikariDataSource(); // 실제 프로덕션용 설정
}
}
@Configuration
public class MyServiceConfig {
@Bean
@ConditionalOnProperty(name = "feature.enabled", havingValue = "true")
public MyService myService() {
return new MyService();
}
}
@Bean
@ConditionalOnMissingBean(MyService.class)
public MyService defaultMyService() {
return new DefaultMyService();
}
@Bean
@ConditionalOnBean(DataSource.class)
public JdbcTemplate jdbcTemplate(DataSource ds) {
return new JdbcTemplate(ds);
}
Spring Boot의 자동 구성(Autoconfiguration) 기능은 @SpringBootApplication에서 시작되어 내부적으로 다양한 어노테이션과 클래스 로딩 메커니즘을 통해 작동한다. 특히 @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector.class) → ImportSelector의 흐름이 핵심이다.
@SpringBootApplication
이 어노테이션은 다음 3가지 어노테이션을 조합한 것이다:
@Configuration
@EnableAutoConfiguration
@ComponentScan
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
@Import(AutoConfigurationImportSelector.class)
즉, AutoConfigurationImportSelector를 통해 자동 구성에 필요한 설정 클래스들을 로딩한다.
AutoConfigurationImportSelector는 ImportSelector 인터페이스를 구현한 클래스
public class AutoConfigurationImportSelector implements DeferredImportSelector {
...
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 여기서 자동 구성할 클래스 목록을 가져옴
return this.getAutoConfigurationEntry(annotationMetadata).getConfigurations();
}
}
동작 방식:
1. META-INF/spring.factories 또는 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에서
자동 구성 클래스 목록을 읽어옴
2. 이 목록은 Spring Boot 스타터 라이브러리(spring-boot-autoconfigure) 안에 존재
3. 조건(@ConditionalOnClass, @ConditionalOnProperty, …)을 만족하는 Bean 설정 클래스만 실제로 활성화됨
예:
# spring-boot-autoconfigure-xxx.jar 내부의 spring.factories 또는 AutoConfiguration.imports
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
@Component
public class MyRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println(args.getOptionNames());
System.out.println(args.getOptionValues("server.port"));
}
}
Spring에서는 org.springframework.core.env.Environment 라는 인터페이스가 환경 정보를 추상화한 객체이다.
특징
/my-app/
├── my-app.jar
└── application.properties ← 외부 설정
application-<profile>.properties 또는 .yml 파일을 여러 개 만들어 환경별로 분리# src/main/resources/application-dev.properties
server.port=8080
spring.datasource.url=jdbc:h2:mem:devdb
# src/main/resources/application-prod.properties
server.port=80
spring.datasource.url=jdbc:mysql://prod-db:3306/app
활성화 방법
1. application.properties 또는 .yml에서 설정:
spring.profiles.active=dev
java -jar my-app.jar --spring.profiles.active=prod
spring:
profiles: dev
server:
port: 8080
---
spring:
profiles: prod
server:
port: 80
# application.properties
spring.profiles=dev
server.port=8080
#---
spring.profiles=prod
server.port=80
@ConfigurationProperties를 사용한 외부 설정.properties 또는 .yml 설정 파일의 값을 Java POJO에 자동 매핑# application.yml
myapp:
name: MyApplication
timeout: 30
@Component
@Data
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
private String name;
private int timeout;
}
@EnableConfigurationProperties와 @ConfigurationPropertiesScan@EnableConfigurationProperties@Component 없이 설정 클래스를 Bean으로 등록할 때 사용@SpringBootApplication 클래스에 선언@EnableConfigurationProperties(MyAppProperties.class)
@SpringBootApplication
public class MyApplication { }
@ConfigurationPropertiesScan@Component 없이 설정 클래스를 자동 스캔하여 등록@ConfigurationPropertiesScan
@SpringBootApplication
public class MyApplication { }
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
private String name;
private int timeout;
// getter
}
@ConstructorBindingfinal 지정, setter 없이 getter만 사용myapp:
name: MyApp
timeout: 30
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
private final String name;
private final int timeout;
@ConstructorBinding
public MyAppProperties(String name, int timeout) {
this.name = name;
this.timeout = timeout;
}
// getter only
}
주의사항
@Component는 사용하지 않음@EnableConfigurationProperties 또는 @ConfigurationPropertiesScan 필수@DefaultValue 또는 필드 기본값private String name = "defaultName";
@DefaultValue(60)
private int timeout;
@Value에서 기본값 (참고용)@Value("${my.timeout:30}")
private int timeout;
@ConfigurationProperties에서는 보통 필드 기본값 지정 방식을 사용함
@Validated + Bean Validation@ConfigurationProperties(prefix = "myapp")
@Validated
public class MyAppProperties {
@NotBlank
private String name;
@Min(1)
private int timeout;
// getter/setter
}
| 어노테이션 | 설명 |
|---|---|
@NotBlank | 문자열이 null, "", " " 모두 거부 |
@NotNull | null 거부 |
@Min(n) / @Max(n) | 숫자 범위 지정 |
@Email | 이메일 형식 검증 |
@Pattern(regexp = "...") | 정규식 검증 |
BindValidationException 발생선언 위치
@Profile("dev")
@Component
public class DevMailService implements MailService {
...
}
@Profile("prod")
@Component
public class ProdMailService implements MailService {
...
}
@Configuration
public class AppConfig {
@Bean
@Profile("test")
public DataSource testDataSource() {
return new H2DataSource();
}
}
다중 프로파일 지정
@Profile({"dev", "local"})
@Component
public class LocalOrDevBean { ... }
프로파일 미지정(default)