Spring Boot는 Spring 기반 애플리케이션을 빠르게 만들 수 있게 해주는 프레임워크다. 복잡한 설정 없이 독립 실행형 프로덕션급 애플리케이션을 만들 수 있다.


Spring Boot란?

Spring vs Spring Boot

Spring Framework:
- 강력하지만 설정이 복잡
- XML 또는 Java Config 필요
- 서버 별도 설치 및 배포
- 의존성 버전 관리 직접 수행

Spring Boot:
- 자동 설정 (Auto-configuration)
- 내장 서버 (Embedded Server)
- Starter 의존성으로 간편한 설정
- 프로덕션 준비 기능 (Actuator)

Spring Boot의 목표

  1. 빠른 시작: 설정 없이 바로 개발 시작
  2. 자동 설정: 클래스패스 기반 자동 구성
  3. 독립 실행형: java -jar로 실행 가능
  4. 프로덕션 준비: 메트릭, 헬스체크, 외부 설정

Hello World

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class Application {

    @GetMapping("/")
    String hello() {
        return "Hello, Spring Boot!";
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
# 실행
./mvnw spring-boot:run
# 또는
./gradlew bootRun

프로젝트 생성

Spring Initializr

https://start.spring.io

선택 항목:
- Project: Maven / Gradle
- Language: Java / Kotlin / Groovy
- Spring Boot: 버전 선택
- Project Metadata: Group, Artifact, Name 등
- Dependencies: 필요한 스타터 선택

Gradle 프로젝트 구조

my-project/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/demo/
│   │   │       └── DemoApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── static/
│   │       └── templates/
│   └── test/
│       └── java/
│           └── com/example/demo/
│               └── DemoApplicationTests.java
├── build.gradle
└── settings.gradle

build.gradle (Gradle Kotlin DSL)

plugins {
    java
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

pom.xml (Maven)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

@SpringBootApplication

구성 요소

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
    @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
    // ...
}

세 가지 핵심 어노테이션

어노테이션 역할
@SpringBootConfiguration @Configuration과 동일, 설정 클래스 지정
@EnableAutoConfiguration 자동 설정 활성화
@ComponentScan 컴포넌트 스캔 (현재 패키지 기준)

분리해서 사용

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages = "com.example")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

자동 설정 제외

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    SecurityAutoConfiguration.class
})
public class Application { }

Starter 의존성

주요 Starter 목록

Starter 설명
spring-boot-starter 핵심 스타터 (로깅, 자동설정 등)
spring-boot-starter-web 웹 애플리케이션 (Spring MVC, 내장 톰캣)
spring-boot-starter-webflux 리액티브 웹 애플리케이션
spring-boot-starter-data-jpa JPA + Hibernate
spring-boot-starter-data-redis Redis
spring-boot-starter-data-mongodb MongoDB
spring-boot-starter-security Spring Security
spring-boot-starter-validation Bean Validation
spring-boot-starter-actuator 모니터링, 메트릭
spring-boot-starter-test 테스트 (JUnit, Mockito 등)
spring-boot-starter-cache 캐시 추상화
spring-boot-starter-mail 이메일 발송
spring-boot-starter-batch Spring Batch
spring-boot-starter-amqp RabbitMQ

Starter의 원리

spring-boot-starter-web 포함 내용:
├── spring-boot-starter
├── spring-boot-starter-json
├── spring-boot-starter-tomcat
├── spring-web
└── spring-webmvc

서드파티 Starter

// MyBatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'

// QueryDSL (비공식)
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'

자동 설정 (Auto-configuration)

동작 원리

1. @EnableAutoConfiguration 활성화
2. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 로드
3. 각 AutoConfiguration 클래스의 조건 평가
4. 조건 충족 시 Bean 등록

조건부 어노테이션

어노테이션 조건
@ConditionalOnClass 클래스패스에 특정 클래스 존재
@ConditionalOnMissingClass 클래스패스에 특정 클래스 없음
@ConditionalOnBean 특정 빈 존재
@ConditionalOnMissingBean 특정 빈 없음
@ConditionalOnProperty 특정 프로퍼티 값
@ConditionalOnResource 특정 리소스 존재
@ConditionalOnWebApplication 웹 애플리케이션
@ConditionalOnExpression SpEL 표현식

DataSource 자동 설정 예시

@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @Conditional(EmbeddedDatabaseCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import(EmbeddedDataSourceConfiguration.class)
    protected static class EmbeddedDatabaseConfiguration {
    }

    // H2가 클래스패스에 있으면 EmbeddedDatabase 생성
    // application.yml에 DataSource 설정이 있으면 해당 설정 사용
    // 직접 DataSource 빈을 정의하면 자동 설정 비활성화
}

자동 설정 확인

# 실행 시 --debug 옵션
java -jar app.jar --debug

# 또는 application.yml
debug: true
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource'

Negative matches:
-----------------
   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory'

커스텀 자동 설정 만들기

@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyProperties properties) {
        return new MyService(properties.getName());
    }
}
@ConfigurationProperties(prefix = "my.service")
public class MyProperties {
    private String name = "default";
    // getter, setter
}
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration

외부 설정

application.properties vs application.yml

# application.properties
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
# application.yml
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: password

설정 우선순위 (높은 순)

1. 명령줄 인자 (--server.port=9090)
2. SPRING_APPLICATION_JSON 환경 변수
3. ServletConfig/ServletContext 파라미터
4. JNDI 속성
5. Java 시스템 프로퍼티 (-Dserver.port=9090)
6. OS 환경 변수
7. RandomValuePropertySource (random.*)
8. 외부 application-{profile}.yml
9. 내부 application-{profile}.yml
10. 외부 application.yml
11. 내부 application.yml
12. @PropertySource
13. 기본 속성 (SpringApplication.setDefaultProperties)

@ConfigurationProperties

@ConfigurationProperties(prefix = "app")
public class AppProperties {

    private String name;
    private int maxConnections = 100;
    private Duration timeout = Duration.ofSeconds(30);
    private List<String> servers = new ArrayList<>();
    private final Security security = new Security();

    // getter, setter

    public static class Security {
        private String username;
        private String password;
        // getter, setter
    }
}
app:
  name: MyApp
  max-connections: 200
  timeout: 60s
  servers:
    - server1.example.com
    - server2.example.com
  security:
    username: admin
    password: secret
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {

    @Bean
    public MyService myService(AppProperties properties) {
        return new MyService(properties.getName(), properties.getMaxConnections());
    }
}

@Value 사용

@Component
public class MyComponent {

    @Value("${app.name}")
    private String appName;

    @Value("${app.max-connections:100}")  // 기본값 지정
    private int maxConnections;

    @Value("${APP_SECRET:}")  // 환경 변수
    private String secret;

    @Value("#{${app.map}}")  // SpEL
    private Map<String, String> map;
}

타입 안전한 설정 바인딩

@ConfigurationProperties(prefix = "mail")
@Validated
public class MailProperties {

    @NotNull
    private String host;

    @Min(1)
    @Max(65535)
    private int port = 25;

    @Valid
    private final Credentials credentials = new Credentials();

    public static class Credentials {
        @NotEmpty
        private String username;
        private String password;
    }
}

프로파일 (Profiles)

프로파일별 설정 파일

src/main/resources/
├── application.yml           # 공통 설정
├── application-dev.yml       # 개발 환경
├── application-prod.yml      # 운영 환경
├── application-test.yml      # 테스트 환경
└── application-local.yml     # 로컬 환경

프로파일 활성화

# application.yml - 기본 프로파일 지정
spring:
  profiles:
    active: dev
# 명령줄로 활성화
java -jar app.jar --spring.profiles.active=prod

# 환경 변수로 활성화
export SPRING_PROFILES_ACTIVE=prod

# JVM 옵션으로 활성화
java -Dspring.profiles.active=prod -jar app.jar

프로파일별 Bean 등록

@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://prod-db:3306/mydb");
        return dataSource;
    }
}

프로파일 그룹

spring:
  profiles:
    group:
      production:
        - proddb
        - prodmq
      development:
        - devdb
        - devmq

프로파일 조건

@Component
@Profile("!prod")  // prod가 아닌 경우
public class DevOnlyComponent { }

@Component
@Profile({"dev", "test"})  // dev 또는 test
public class NonProdComponent { }

내장 서버

지원 서버

서버 Starter 특징
Tomcat spring-boot-starter-tomcat 기본 내장, 가장 널리 사용
Jetty spring-boot-starter-jetty 경량, WebSocket 강점
Undertow spring-boot-starter-undertow 고성능, 논블로킹
Netty spring-boot-starter-reactor-netty WebFlux 기본

서버 변경

// Tomcat 제외하고 Undertow 사용
implementation('org.springframework.boot:spring-boot-starter-web') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-undertow'

서버 설정

server:
  port: 8080
  address: 0.0.0.0

  # SSL 설정
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: secret
    key-store-type: PKCS12

  # 압축 설정
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,application/json
    min-response-size: 1024

  # 세션 설정
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
        secure: true

  # Tomcat 전용 설정
  tomcat:
    max-threads: 200
    min-spare-threads: 10
    max-connections: 10000
    accept-count: 100
    connection-timeout: 20000
    accesslog:
      enabled: true
      directory: logs
      pattern: "%h %l %u %t \"%r\" %s %b %D"

프로그래밍 방식 커스터마이징

@Component
public class ServerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.setPort(9090);
        factory.addConnectorCustomizers(connector -> {
            connector.setProperty("maxThreads", "500");
        });
    }
}

Spring Boot Actuator

의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-actuator'

주요 엔드포인트

엔드포인트 설명
/actuator/health 애플리케이션 상태
/actuator/info 애플리케이션 정보
/actuator/metrics 메트릭 정보
/actuator/env 환경 변수
/actuator/beans 등록된 빈 목록
/actuator/mappings 요청 매핑 정보
/actuator/loggers 로거 설정
/actuator/threaddump 스레드 덤프
/actuator/heapdump 힙 덤프
/actuator/prometheus Prometheus 메트릭

설정

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
        # include: "*"  # 모든 엔드포인트 노출
      base-path: /actuator

  endpoint:
    health:
      show-details: always  # always, when-authorized, never
      show-components: always

  health:
    db:
      enabled: true
    redis:
      enabled: true
    diskspace:
      enabled: true
      threshold: 10MB

  info:
    env:
      enabled: true
    git:
      mode: full

  metrics:
    export:
      prometheus:
        enabled: true

커스텀 Health Indicator

@Component
public class CustomHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        boolean isHealthy = checkExternalService();

        if (isHealthy) {
            return Health.up()
                .withDetail("service", "External API")
                .withDetail("status", "available")
                .build();
        }
        return Health.down()
            .withDetail("service", "External API")
            .withDetail("error", "Connection refused")
            .build();
    }

    private boolean checkExternalService() {
        // 외부 서비스 상태 확인
        return true;
    }
}

커스텀 Metrics

@Component
public class OrderMetrics {

    private final Counter orderCounter;
    private final Timer orderTimer;

    public OrderMetrics(MeterRegistry registry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("Number of orders created")
            .tag("type", "total")
            .register(registry);

        this.orderTimer = Timer.builder("orders.processing.time")
            .description("Time to process an order")
            .register(registry);
    }

    public void recordOrder() {
        orderCounter.increment();
    }

    public void recordProcessingTime(Duration duration) {
        orderTimer.record(duration);
    }
}

Info 엔드포인트 커스터마이징

info:
  app:
    name: ${spring.application.name}
    version: 1.0.0
    description: My Application
  build:
    artifact: "@project.artifactId@"
    version: "@project.version@"

로깅

기본 로깅 (Logback)

logging:
  level:
    root: INFO
    com.example: DEBUG
    org.springframework.web: WARN
    org.hibernate.SQL: DEBUG

  file:
    name: logs/application.log
    max-size: 10MB
    max-history: 30

  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
      total-size-cap: 1GB

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProfile name="dev">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%clr(%d{HH:mm:ss.SSS}){faint} %clr(%-5level) %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n</pattern>
            </encoder>
        </appender>
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>

    <springProfile name="prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>logs/app.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
        <root level="INFO">
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>
</configuration>

테스트

테스트 의존성

testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 포함: JUnit 5, Mockito, AssertJ, Hamcrest, JSONPath, Spring Test

@SpringBootTest

@SpringBootTest
class ApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    void contextLoads() {
        assertThat(userService).isNotNull();
    }
}

Web Layer 테스트

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void getUser_ShouldReturnUser() throws Exception {
        User user = new User(1L, "John");
        when(userService.findById(1L)).thenReturn(user);

        mockMvc.perform(get("/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"));
    }
}

Data Layer 테스트

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void findByEmail_ShouldReturnUser() {
        User user = new User("john@example.com", "John");
        entityManager.persistAndFlush(user);

        Optional<User> found = userRepository.findByEmail("john@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
}

슬라이스 테스트

어노테이션 테스트 대상
@WebMvcTest Spring MVC (Controller)
@WebFluxTest WebFlux (Controller)
@DataJpaTest JPA Repository
@DataMongoTest MongoDB
@DataRedisTest Redis
@JdbcTest JDBC
@JsonTest JSON 직렬화
@RestClientTest REST Client

TestContainers 사용

@SpringBootTest
@Testcontainers
class IntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Test
    void test() {
        // 실제 PostgreSQL로 테스트
    }
}

빌드 및 배포

실행 가능한 JAR 빌드

# Maven
./mvnw clean package
java -jar target/app-0.0.1-SNAPSHOT.jar

# Gradle
./gradlew clean build
java -jar build/libs/app-0.0.1-SNAPSHOT.jar

JAR 구조

app.jar
├── BOOT-INF/
│   ├── classes/        # 애플리케이션 클래스
│   ├── lib/            # 의존성 JAR
│   └── classpath.idx   # 클래스패스 인덱스
├── META-INF/
│   └── MANIFEST.MF
└── org/springframework/boot/loader/  # Spring Boot Loader

Docker 이미지

# 멀티 스테이지 빌드
FROM eclipse-temurin:21-jdk as builder
WORKDIR /app
COPY . .
RUN ./gradlew build -x test

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Layered JAR (최적화된 Docker)

FROM eclipse-temurin:21-jre as builder
WORKDIR /app
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Native Image (GraalVM)

plugins {
    id 'org.graalvm.buildtools.native' version '0.9.28'
}
./gradlew nativeCompile
./build/native/nativeCompile/app

Spring Boot 3.x 주요 변경사항

Java 17+ 필수

// Java 17 기능 활용
public record UserDto(Long id, String name, String email) {}

var users = List.of(
    new UserDto(1L, "John", "john@example.com"),
    new UserDto(2L, "Jane", "jane@example.com")
);

Jakarta EE 9+ 마이그레이션

// Before (Spring Boot 2.x)
import javax.persistence.*;
import javax.validation.constraints.*;
import javax.servlet.*;

// After (Spring Boot 3.x)
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import jakarta.servlet.*;

자동 설정 파일 위치 변경

# Before (Spring Boot 2.x)
META-INF/spring.factories

# After (Spring Boot 3.x)
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Observability (관찰 가능성)

implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
management:
  tracing:
    sampling:
      probability: 1.0
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

Problem Details (RFC 7807)

spring:
  mvc:
    problemdetails:
      enabled: true
{
    "type": "https://example.com/errors/not-found",
    "title": "Not Found",
    "status": 404,
    "detail": "User with id 123 not found",
    "instance": "/users/123"
}

HTTP Interface Client

public interface UserClient {

    @GetExchange("/users/{id}")
    User getUser(@PathVariable Long id);

    @PostExchange("/users")
    User createUser(@RequestBody User user);
}

@Configuration
public class ClientConfig {

    @Bean
    public UserClient userClient(RestClient.Builder builder) {
        RestClient restClient = builder.baseUrl("https://api.example.com").build();
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builderFor(RestClientAdapter.create(restClient))
            .build();
        return factory.createClient(UserClient.class);
    }
}

Best Practices

패키지 구조

com.example.app/
├── config/           # 설정 클래스
├── controller/       # REST Controller
├── service/          # 비즈니스 로직
├── repository/       # 데이터 접근
├── domain/           # 엔티티, VO
├── dto/              # DTO
├── exception/        # 예외 클래스
└── Application.java  # 메인 클래스 (최상위)

설정 분리

# application.yml - 공통
spring:
  application:
    name: my-app

---
# application-dev.yml - 개발
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:testdb

---
# application-prod.yml - 운영
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://prod-db:3306/mydb

예외 처리

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", e.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
        List<String> errors = e.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .toList();
        return ResponseEntity.badRequest()
            .body(new ErrorResponse("VALIDATION_ERROR", errors.toString()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        log.error("Unexpected error", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "서버 오류가 발생했습니다."));
    }
}

Graceful Shutdown

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

보안 설정

# 민감 정보는 환경 변수로
spring:
  datasource:
    password: ${DB_PASSWORD}

# Actuator 보안
management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: when-authorized

정리

기능 설명
자동 설정 클래스패스 기반 자동 Bean 등록
Starter 관련 의존성 일괄 관리
외부 설정 application.yml, 환경변수, 명령줄
프로파일 환경별 설정 분리
내장 서버 Tomcat, Jetty, Undertow
Actuator 모니터링, 헬스체크
테스트 @SpringBootTest, 슬라이스 테스트

Spring Boot 선택 기준

Spring Boot가 적합한 경우:
- 빠른 개발이 필요한 경우
- 마이크로서비스 아키텍처
- 컨테이너 기반 배포
- 표준적인 웹 애플리케이션

순수 Spring이 적합한 경우:
- 세밀한 제어가 필요한 경우
- 레거시 시스템 통합
- 특수한 서버 환경

Spring Boot는 “Convention over Configuration” 철학을 따른다. 기본 설정으로 시작하고, 필요할 때만 커스터마이징하자.