스프링 부트 내용 정리

유기훈·2025년 4월 27일

스프링 부트 - 핵심 기능 5가지

  • WAS: Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨
  • 라이브러리 관리
    - 손쉬운 빌드 구성을 위한 스타터 종속성 제공
    • 라이브러리 버전 관리
  • 자동 구성: 프로젝트 시작에 필요한 스프링과 외부 라이브러리의 빈을 자동 등록
  • 외부 설정: 환경에 따라 달라져야 하는 외부 설정 공통화
  • 프로덕션 준비: 모니터링을 위한 메트릭, 상태 확인 기능 제공

스프링 부트는 스프링 프레임워크를 쉽게 사용할 수 있게 도와주는 도구일 뿐이다.

웹 서버와 서블릿 컨테이너

전체 설명

일반적으로 웹 애플리케이션 요청은 웹 서버를 통해 먼저 들어온다. 웹 서버는 정적 리소스(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에서 컨테이너 등록 구조 (ServletContainerInitializer 기반)

WAS(Web Application Server)는 단순히 서블릿 컨테이너만 실행하는 것이 아니라, 애플리케이션 구동 시 개발자가 정의한 별도의 컨테이너(예: 스프링 컨테이너, 사용자 정의 초기화 로직 등)를 등록할 수 있도록 확장 구조를 제공한다. 이 확장 지점의 핵심이 바로 ServletContainerInitializer이다.

ServletContainerInitializer란?

ServletContainerInitializer는 Java EE 6부터 도입된 인터페이스로, WAS가 웹 애플리케이션을 시작할 때 실행된다. 이를 구현하면 애플리케이션 시작 시점에 특정 클래스들을 탐색하고, 동적으로 서블릿/필터/리스너 등록 등의 초기화 로직을 수행할 수 있다.

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

사용 목적

  • DispatcherServlet 같은 서블릿 기반 프레임워크 초기화
  • Spring이나 JSF 등 외부 컨테이너를 서블릿 컨테이너에 연결
  • 애플리케이션에 필요한 사용자 정의 초기화 로직 등록

등록 방법
ServletContainerInitializer 구현체는 JAR 파일 내에 다음과 같은 경로에 등록되어야 한다:

META-INF/services/javax.servlet.ServletContainerInitializer

이 파일에는 해당 인터페이스의 구현 클래스 FQCN을 명시한다:

org.example.MyInitializer

WAS는 이 파일을 읽고 구현체를 자동으로 로딩하여 onStartup()을 호출한다.

@HandlesTypes와 컨테이너 등록

@HandlesTypes 어노테이션을 함께 사용하면, 특정 타입(예: 인터페이스, 애노테이션 등)을 구현하거나 사용하는 클래스들을 자동으로 탐지할 수 있다. 이 클래스 집합이 onStartup() 메서드의 인자로 전달된다.

  • MyContainerMarker: MyContainerMarker 인터페이스의 구현체들을 애플리케이션이라고 한다.
  • 아래 onStartup 메서드에서 하는 container에 MyContainerMarker를 구현한 클래스들을 등록하는 과정을 애플리케이션 초기화라고 한다.
@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도 이 구조를 그대로 사용한ㄴ다.

  • Spring은 SpringServletContainerInitializer를 정의하고
  • 내부에서 @HandlesTypes(WebApplicationInitializer.class)를 선언한다.
  • 이로 인해 사용자가 작성한 WebApplicationInitializer 구현체들이 자동으로 탐지되어
  • DispatcherServlet 및 스프링 컨텍스트가 등록된다.

스프링 부트와 내장 톰켓

핵심 개념

  • Spring Framework 자체는 내장 톰캣을 제공하지 않음
  • 따라서 개발자가 Apache Tomcat Embedded API를 이용해 Tomcat 객체를 직접 생성하고 설정해야 함
  • DispatcherServlet, AnnotationConfigWebApplicationContext, 스프링 설정 클래스 등을 코드로 수동 구성

내장 톰켓 사용 예시

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() → 서버 요청 대기

일반 JAR vs Fat JAR

비교 표

일반 JAR: 특징

  • myapp.jar에는 오직 애플리케이션의 클래스 파일만 들어 있음
  • lib/ 폴더 등에 있는 의존성 JAR들을 따로 지정해야 실행됨
  • 실행 시 Classpath를 지정해야 함
java -cp myapp.jar:lib/* com.example.Main

fat JAR: 특징

  • myapp-fat.jar 하나로 모든 실행에 필요한 요소가 포함됨
    (애플리케이션 + 라이브러리 모두)
  • 클래스 경로를 따로 지정하지 않고 실행 가능
  • 배포/운영 편리 (파일 하나만 넘기면 됨)
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);
  }
}
  • MySpringApplication 에서는 configClass 파일을 읽어와 스프링 컨테이너에 등록한다.
  • MySpringBootMain 에서는 MySpringApplication 에 configClass를 자신으로 등록한다.
  • @MySpringBootApplication 어노테이션에는 @ComponentScan 어노테이션이 붙어있다. @ComponentScan 어노테이션에 따로 경로를 지정해주지 않으면, 어노테이션이 붙은 클래스의 경로와 그 하위 경로에 위치한 컴포넌트들을 스캔해서 등록한다.
  • 그래서 MySpringBootMain 클래스의 경로는 프로젝트 최상단 경로에 위치를 하여야 하고, @ComponentScan 덕에 개발자는 따로 Configuration 클래스를 스프링 컨테이너에 지정하는 작업을 하지 않아도 된다.

스프링 부트와 jar

  • jar는 내부에 다른 jar를 포함하지 못한다. war는 jar를 포함할 수 있다.
  • jar는 외부 라이브러리를 포함해야하기 때문에 fatJar 방식을 사용한다.
  • fatJar는 외부 라이브러리를 다 풀어서 클래스로 바꾼다음에 하나의 jar로 만든거다.
    - fatJar는 jar가 어떤 라이브러리를 포함하고 있는지 확인하기 어려운 문제가 있음
  • 스프링 부트의 jar를 풀어보면 (jar -xvf myapp.jar) 다른 jar를 포함하고 있는 걸 확인할 수 있다.
  • 스프링 부트는 '실행가능 JAR'를 만들어서 fatJar의 문제를 해결함.

자동 구성

Spring Boot 자동 구성

  • 스프링 부트를 사용하지 않으면 사용해야 할 Bean을 하나하나 일일이 다 등록을 해주어야 해서 불편하다.
  • 스프링 부트는 기본적으로 등록해줄 수 있는 Bean들을 다 등록해준다.
  • 스프링 부트는 사용자가 직접 Bean을 만들어서 등록해두었다면 스프링 부트에서 구현한 빈은 등록을 하지 않는다.

@Conditional

  • @Conditional은 하나 이상의 Condition 클래스를 지정하여, 그 조건이 true일 때만 해당 구성(Bean 등록 등)이 적용된다.
  • @Conditional(MyCondition.class) 와 같이 사용하며, MyCondition은 org.springframework.context.annotation.Condition 인터페이스를 구현해야 한다.
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(); // 실제 프로덕션용 설정
    }
}

SpringBoot에서 제공하는 @Conditional 계열 어노테이션

  1. @ConditionalOnProperty
  • 설정 파일의 속성 유무 또는 값에 따라 빈 등록을 제어
  • 내부적으로 @Conditional(OnPropertyCondition.class)를 사용
@Configuration
public class MyServiceConfig {

    @Bean
    @ConditionalOnProperty(name = "feature.enabled", havingValue = "true")
    public MyService myService() {
        return new MyService();
    }
}
  1. @ConditionalOnMissingBean
  • 해당 타입의 Bean이 없을 경우에만 등록
@Bean
@ConditionalOnMissingBean(MyService.class)
public MyService defaultMyService() {
    return new DefaultMyService();
}
  1. @ConditionalOnBean
  • 특정 타입의 Bean이 이미 등록되어 있는 경우에만 동작
@Bean
@ConditionalOnBean(DataSource.class)
public JdbcTemplate jdbcTemplate(DataSource ds) {
    return new JdbcTemplate(ds);
}

SpringBoot 자동 구성 동작 방식

Spring Boot의 자동 구성(Autoconfiguration) 기능은 @SpringBootApplication에서 시작되어 내부적으로 다양한 어노테이션과 클래스 로딩 메커니즘을 통해 작동한다. 특히 @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector.class) → ImportSelector의 흐름이 핵심이다.

1. @SpringBootApplication의 역할

@SpringBootApplication

이 어노테이션은 다음 3가지 어노테이션을 조합한 것이다:

@Configuration
@EnableAutoConfiguration
@ComponentScan
  • @Configuration: Java Config 기반 설정 클래스
  • @ComponentScan: 현재 패키지를 기준으로 컴포넌트 스캔 수행
  • @EnableAutoConfiguration: 자동 구성 로직의 핵심, 아래에서 자세히 설명

2. @EnableAutoConfiguration → 자동 구성의 진입점

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    ...
}
@Import(AutoConfigurationImportSelector.class)

즉, AutoConfigurationImportSelector를 통해 자동 구성에 필요한 설정 클래스들을 로딩한다.

3. 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

자동 구성 흐름 요약

  1. @SpringBootApplication 선언
  2. 내부적으로 @EnableAutoConfiguration 적용됨
  3. @Import(AutoConfigurationImportSelector.class)가 작동
  4. selectImports()가 spring.factories 또는 AutoConfiguration.imports에서 클래스 목록 로딩
  5. 각 클래스에 정의된 @ConditionalXXXX 조건을 평가
  6. 조건 만족 시 해당 @Configuration 클래스가 Spring Context에 등록됨

외부설정과 프로필

외부설정 방식

  1. OS 환경변수 (Environment Variable)
  • 운영체제 수준에서 설정.
  • 예: export MY_ENV=prod
  • Spring에서는 MYENV또는@Value("{MY_ENV} 또는 @Value("{MY_ENV}")로 주입 가능.
  • Environment.getProperty("MY_ENV")로도 접근 가능.
  1. 자바 시스템 속성 (Java System Properties)
  • JVM 실행 시 -D 옵션으로 설정.
  • 예: java -Dmy.env=prod -jar app.jar
  • System.getProperty("my.env")로 접근 가능.
  • Spring에서도 ${my.env} 또는 Environment.getProperty("my.env")로 접근 가능.
  1. 커맨드 라인 인수 (Command Line Arguments)
  • java -jar app.jar --server.port=8081
  • Spring Boot는 자동으로 이 값을 파싱해서 Environment에 등록.
  • ${server.port} 또는 Environment.getProperty("server.port")로 접근 가능.
  1. 커맨드 라인 옵션 인수 (CommandLineRunner, ApplicationArguments)
  • Spring Boot는 ApplicationArguments Bean을 통해 -- 옵션을 분리해서 제공.
@Component
public class MyRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        System.out.println(args.getOptionNames());
        System.out.println(args.getOptionValues("server.port"));
    }
}
  1. application.properties / application.yml
  • Spring Boot에서 가장 일반적인 설정 파일.
  • /src/main/resources/ 아래에 존재하며 프로파일별로 application-prod.yml도 가능.

Spring의 Environment란?

Spring에서는 org.springframework.core.env.Environment 라는 인터페이스가 환경 정보를 추상화한 객체이다.

특징

  • 다양한 PropertySource를 계층적으로 관리
  • 우선순위대로 검색 (command line args > system properties > env vars > application.yml 등).
  • @Value, @ConfigurationProperties, Environment.getProperty() 등으로 접근.

환경 별 설정 분리

1. 외부 파일 사용 (JAR와 동일한 경로)

  • application.properties 또는 application.yml을 JAR 파일과 같은 디렉터리에 두어 외부 설정 파일로 사용합니다.
  • Spring Boot는 JAR 외부의 파일을 내부 파일보다 우선적으로 로드합니다.
    구성 예시
    디렉터리 구조:
/my-app/
  ├── my-app.jar
  └── application.properties   ← 외부 설정

2. 내부 파일 분리 (application-dev.properties, application-prod.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
  1. 실행 시 지정:
java -jar my-app.jar --spring.profiles.active=prod

3. 내부 파일 합체 (application.properties 파일에 --- 사용)

  • 하나의 .yml 또는 .properties 파일 내에서 여러 환경 설정을 나누어 작성
  1. application.yml에서 사용 예시:
spring:
  profiles: dev
server:
  port: 8080
---
spring:
  profiles: prod
server:
  port: 80
  1. .properties에서도 가능:
# application.properties
spring.profiles=dev
server.port=8080

#---
spring.profiles=prod
server.port=80

외부설정 사용 (@ConfigurationProperties)

1. @ConfigurationProperties를 사용한 외부 설정

  • .properties 또는 .yml 설정 파일의 값을 Java POJO에 자동 매핑
  • 접두어(prefix) 기반으로 값을 바인딩

예시

# application.yml
myapp:
  name: MyApplication
  timeout: 30
@Component
@Data
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
    private String name;
    private int timeout;

}

2. @EnableConfigurationProperties@ConfigurationPropertiesScan

@EnableConfigurationProperties

  • @Component 없이 설정 클래스를 Bean으로 등록할 때 사용
  • @SpringBootApplication 클래스에 선언
@EnableConfigurationProperties(MyAppProperties.class)
@SpringBootApplication
public class MyApplication { }

@ConfigurationPropertiesScan

  • @Component 없이 설정 클래스를 자동 스캔하여 등록
  • Spring Boot 2.2 이상부터 지원
@ConfigurationPropertiesScan
@SpringBootApplication
public class MyApplication { }
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
    private String name;
    private int timeout;

    // getter
}

3. 생성자 바인딩 방식: @ConstructorBinding

  • 생성자를 통해 값을 주입받는 불변(immutable) 설정 객체 구성 가능
  • 필드에 final 지정, setter 없이 getter만 사용
  • Spring Boot 3.0 이상 부터는 생성자가 하나인 경우 생략 가능. 생성자 2개 이상인 경우 생성자 중 하나에 어노테이션 부착 필수
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 필수

4. 기본값 설정: @DefaultValue 또는 필드 기본값

필드 기본값 지정

private String name = "defaultName";
@DefaultValue(60)
private int timeout;

@Value에서 기본값 (참고용)

@Value("${my.timeout:30}")
private int timeout;

@ConfigurationProperties에서는 보통 필드 기본값 지정 방식을 사용함

5. 설정 검증: @Validated + Bean Validation

사용 방법

@ConfigurationProperties(prefix = "myapp")
@Validated
public class MyAppProperties {

    @NotBlank
    private String name;

    @Min(1)
    private int timeout;

    // getter/setter
}

주요 어노테이션

어노테이션설명
@NotBlank문자열이 null, "", " " 모두 거부
@NotNullnull 거부
@Min(n) / @Max(n)숫자 범위 지정
@Email이메일 형식 검증
@Pattern(regexp = "...")정규식 검증

예외 발생 시

  • BindValidationException 발생
  • 검증 실패 시 애플리케이션 실행 중단

@Profile

@Profile 어노테이션이란?

  • @Profile은 Spring에서 환경(프로파일) 에 따라 빈(bean) 을 선택적으로 등록할 수 있도록 도와주는 어노테이션이다.
  • 운영환경(prod), 개발환경(dev), 테스트환경(test) 등 상황에 따라 다른 Bean을 등록하고 싶을 때 유용하다.

선언 위치

  • 클래스나 메서드에 붙여 사용
  • 주로 @Configuration, @Component, @Bean과 함께 사용

기본 문법

@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)

  • @Profile이 없는 빈은 모든 프로파일에서 등록됨
  • 단, 같은 타입의 Bean이 중복되면 @Primary로 우선순위 지정하거나 수동 주입 필요
profile
개발 블로그

0개의 댓글