
소개
Spring Boot는 빠른 시작과 생산성 향상을 위해 설계된 프레임워크입니다. 자동설정(auto-configuration), 스타터(starters), 구성 바인딩(Configuration Properties) 같은 개념이 핵심이며, 운영 환경에 따라 여러 트레이드오프를 고려해야 합니다. 이 글은 안정적이고 널리 사용되는 개념을 정리하고, 간단한 실용 예제를 통해 적용 방식을 보여줍니다.
핵심 개념 요약
간단하게 정리하면 다음 요소들이 Spring Boot 경험의 중심입니다.
- 스타터(Starters): 관련 라이브러리를 묶어서 의존성 관리를 단순화합니다. 예: spring-boot-starter-web, spring-boot-starter-actuator 등.
- 자동설정(Auto-configuration): 클래스패스와 설정을 기반으로 필요한 빈을 자동 등록합니다. 편하지만 필요하면 오버라이드하거나 비활성화하여 제어할 수 있습니다.
- 구성 바인딩(Configuration Properties): application.yml 또는 application.properties의 값을 타입 세이프하게 바인딩합니다(@ConfigurationProperties).
- 프로파일(Profiles): dev/prod 같은 환경별 설정을 분리할 수 있습니다.
- Actuator: 애플리케이션 헬스, 메트릭스, 트레이스 등의 운영용 기능을 제공합니다.
- AOT/네이티브: 시작 시간과 메모리 최적화를 위해 AOT 컴파일 또는 네이티브 빌드를 사용할 수 있지만 제한 사항(리플렉션, 프록시 등)을 고려해야 합니다.
주요 트레이드오프
Spring Boot는 편의성과 추상화를 제공하지만, 그에 따른 선택의 지점이 존재합니다. 대표적인 트레이드오프는 다음과 같습니다.
- 편의성 vs 제어: 자동설정은 빠른 시작을 돕지만, 내부 동작을 완전히 이해하지 못하면 문제 발생 시 디버깅이 어려워집니다. 필요한 경우 특정 자동설정을 비활성화하거나 직접 빈을 정의하세요.
- Servlet(Spring MVC) vs Reactive(WebFlux): Reactive는 큰 동시성 부담과 I/O 바운드 처리에서 장점이 있지만, 프로그래밍 모델이 더 복잡하고 서드파티 라이브러리 호환성 문제(블로킹 라이브러리)를 고려해야 합니다. 단순 CRUD 기반 서비스라면 전통적인 Servlet 기반이 더 간단하고 안정적일 수 있습니다.
- Fat JAR vs Layered/Thin Image: Fat JAR은 배포가 쉽지만, 컨테이너 레이어 캐시를 활용한 이미지 빌드(레이어드 JAR 또는 Jib 등)를 사용하면 CI/CD에서 효율적입니다.
- AOT/네이티브 빌드: 시작 시간과 메모리 이득이 있으나 리플렉션 사용, 동적 프록시, 런타임 바인딩이 많은 코드베이스에서는 추가 작업(힌트, AOT 리팩토링)이 필요합니다.
실용 예제: 프로파일 + 구성 바인딩을 사용하는 간단한 REST API
다음 예제는 Spring Boot 애플리케이션에서 구성 바인딩(@ConfigurationProperties)과 프로파일(@Profile)을 사용해 환경별 동작을 분리하는 방법을 보여줍니다. 예제 구성은 Maven 기반이며, 최소 구성만 포함합니다.
프로젝트 구조(요약):
src/main/java/com/example/demo/
DemoApplication.java
config/GreetingProperties.java
service/GreetingService.java
service/DefaultGreetingService.java
service/DevGreetingService.java
web/GreetingController.java
src/main/resources/application.yml
src/main/resources/application-dev.yml
pom.xml (핵심 의존성)
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.x.x</version> <!-- Spring Boot 3.x 계열을 사용한다고 가정 -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
DemoApplication.java
package com.example.demo;
import com.example.demo.config.GreetingProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(GreetingProperties.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
GreetingProperties.java
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app.greeting")
public class GreetingProperties {
private String prefix = "Hello";
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
}
GreetingService 인터페이스 및 구현
package com.example.demo.service;
public interface GreetingService {
String greet(String name);
}
// Default 구현
package com.example.demo.service;
import com.example.demo.config.GreetingProperties;
import org.springframework.stereotype.Service;
@Service
public class DefaultGreetingService implements GreetingService {
private final GreetingProperties props;
public DefaultGreetingService(GreetingProperties props) {
this.props = props;
}
@Override
public String greet(String name) {
return props.getPrefix() + ", " + name + "!";
}
}
// dev 프로파일용 대체 구현
package com.example.demo.service;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
@Profile("dev")
@Service
public class DevGreetingService implements GreetingService {
@Override
public String greet(String name) {
return "[DEV] Hi, " + name;
}
}
GreetingController.java
package com.example.demo.web;
import com.example.demo.service.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService service;
public GreetingController(GreetingService service) {
this.service = service;
}
@GetMapping("/greet")
public String greet(@RequestParam(defaultValue = "World") String name) {
return service.greet(name);
}
}
application.yml
server:
port: 8080
app:
greeting:
prefix: "Hello"
management:
endpoints:
web:
exposure:
include: health,info
application-dev.yml
app:
greeting:
prefix: "Howdy"
실행 예시:
# 기본 프로파일 (prod 같은 설정)
mvn spring-boot:run
# dev 프로파일로 실행
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 패키징
mvn -DskipTests package
java -jar target/demo-0.0.1-SNAPSHOT.jar
컨테이너화(간단한 Dockerfile)
간단한 예로 JVM 기반 이미지를 사용한 Dockerfile입니다. CI에서 레이어 캐시를 활용하려면 Spring Boot의 레이어드 JAR 기능 또는 Jib를 고려하세요.
FROM eclipse-temurin:17-jre-alpine
ARG JAR_FILE=target/demo-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
운영과 확장 시 고려사항
실서비스에 배포할 때는 다음을 점검하세요.
- 로그·모니터링·트레이스(로그 포맷, Actuator, Micrometer 등)를 초기에 설계하세요.
- 프로파일을 통해 민감 정보를 분리하고, 시크릿은 외부 저장소(예: Vault, K8s Secret)로 관리하세요.
- Reactive로 전환할 경우 라이브러리 생태계(특히 DB 드라이버)가 논블로킹을 지원하는지 확인하세요.
- AOT/네이티브 빌드는 장단점이 있으므로, 프로파일이나 특정 서비스에서 시범 적용 후 점진적으로 확장하세요.
맺음말
Spring Boot는 빠른 개발과 운영 편의성을 제공하지만, 자동화된 동작의 내부를 이해하고 필요한 제어점을 확보하는 것이 중요합니다. 위 예제는 구성 바인딩과 프로파일을 활용한 안정적인 패턴을 보여주며, 실제 환경에서는 관찰성(Observability), 보안, 빌드·배포 전략을 함께 설계해야 합니다.