
소개
Spring Boot는 Spring 기반 애플리케이션을 빠르게 시작하고 운영하기 위한 도구입니다. 보일러플레이트를 줄이고, 스타터 의존성과 자동설정을 통해 개발 생산성을 높여줍니다. 이 글에서는 안정적인 핵심 개념과 함께 주요 설계 트레이드오프를 정리하고, 실무에 바로 적용할 수 있는 간단한 예제를 제공합니다.
핵심 개념 요약
다음은 Spring Boot를 이해할 때 반드시 알아야 할 개념들입니다.
자동설정(Autoconfiguration) — classpath와 빈 정의를 검사해 필요한 빈을 자동 등록합니다. 개발 초기에는 매우 편리하지만, 내부 동작을 모르면 예기치 않은 빈 충돌이나 설정 오버라이드 문제가 생길 수 있습니다.
스타터(Starters) — 관련 의존성을 그룹화한 의존성 묶음입니다. 예: spring-boot-starter-web, spring-boot-starter-data-jpa. 편의성은 높지만 불필요한 전이 의존성까지 포함될 수 있으므로 의존성 트리를 주기적으로 점검해야 합니다.
구성 속성(Configuration Properties) — application.properties 또는 application.yaml과 @ConfigurationProperties를 통해 타입 안전하게 설정을 주입합니다. 프로파일(profile)과 조합하면 환경별 구성을 깔끔하게 관리할 수 있습니다.
프로파일(Profiles) — dev, test, prod 같은 환경별 설정 분기 메커니즘입니다. 빌드 타임 분기와 런타임 분기를 혼동하지 않도록 주의하세요.
Actuator — 헬스체크, 메트릭, 트레이스 등 운영 관련 엔드포인트를 제공합니다. 운영 환경에서 보안과 노출을 신중히 설정해야 합니다.
설계상의 주요 트레이드오프
Spring Boot는 많은 결정을 자동으로 내려줍니다. 이런 편의성은 장점이지만 아래와 같은 트레이드오프를 이해하고 설계에 반영해야 합니다.
- 편의성 vs 가시성 — 자동설정은 개발 속도를 올리지만 내부 빈 등록과 의존성 해석이 '보이지 않는' 상태가 됩니다. 문제가 발생하면
spring.main.allow-bean-definition-overriding이나spring.autoconfigure.exclude를 활용해 통제해야 합니다. - 컴포넌트 스캔 vs 명시적 등록 — Component scanning은 빠르지만, 큰 코드베이스에서는 의도치 않은 빈 충돌이 발생할 수 있습니다. 중요한 빈은 @Configuration으로 명시적 등록을 검토하세요.
- 모놀리식 vs 마이크로서비스 — Spring Boot는 마이크로서비스를 쉽게 만들 수 있게 해주지만, 서비스 수가 늘어나면 운영(배포, 관찰성, 데이터 관리)이 복잡해집니다. 팀과 조직 능력에 맞춰 경계선을 정하세요.
- 임페러티브 vs 리액티브 — WebFlux(리액티브)는 높은 동시성을 적은 스레드로 처리할 수 있지만, 라이브러리 호환성, 디버깅 난이도, 학습비용이 있습니다. IO 패턴과 팀 역량을 고려해 선택하세요.
- JVM Fat JAR vs Native Image — Fat JAR은 이식성이 좋고 빌드가 간단합니다. GraalVM 네이티브 이미지는 시작 시간과 메모리 장점이 있지만, 리플렉션/동적 클래스 로딩 이슈, 빌드 복잡성을 감수해야 합니다.
실전 예제: 간단한 REST 서비스
아래 예제는 Spring Boot 애플리케이션의 최소 구성 요소를 보여줍니다. 기능:
- HTTP GET 엔드포인트
- 타입 안전한 구성(@ConfigurationProperties)
- 프로파일에 따른 설정
- Actuator와 커스텀 헬스 인디케이터
1) Maven 의존성 예시 (pom.xml 내부의 dependencies 일부)
<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>
2) 메인 애플리케이션 (Java)
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3) 타입 안전한 설정 클래스
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String greeting = "Hello";
public String getGreeting() { return greeting; }
public void setGreeting(String greeting) { this.greeting = greeting; }
}
4) REST 컨트롤러 (프로파일을 이용한 동작 분기)
package com.example.demo.web;
import com.example.demo.config.AppProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class HelloController {
private final AppProperties props;
@Autowired
public HelloController(AppProperties props) {
this.props = props;
}
@GetMapping("/hello")
public String hello() {
return props.getGreeting() + ", world!";
}
}
5) 커스텀 헬스 인디케이터 (Actuator 확장)
package com.example.demo.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class ExampleHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// 간단한 예: 항상 UP 반환 (실제 체크: DB, 외부 API 등)
return Health.up().withDetail("example", "ok").build();
}
}
6) application.yaml 예시 (프로파일 분기)
spring:
profiles:
active: dev
app:
greeting: "Hello"
---
spring:
config:
activate:
on-profile: prod
app:
greeting: "Welcome"
위 예제에서 개발 환경(dev)과 운영 환경(prod)의 메시지를 분리했습니다. Actuator는 기본적으로 /actuator 엔드포인트에 노출되므로 운영 환경에서 보안 설정(예: 인증, IP 제한)을 반드시 적용하세요.
운영과 배포 관련 팁
간단한 권장 사항:
- Actuator 엔드포인트는 기본적으로 제한하고, 필요한 엔드포인트만 노출하세요.
- 의존성 트리를 정기적으로 확인해 불필요한 트랜짓 의존성을 제거하세요.
- 설정은 가능하면 타입 안전(@ConfigurationProperties)으로 관리하고, 비밀값은 비밀 관리 시스템(Secrets Manager)으로 분리하세요.
- 성능 문제가 발생하면 APM, JVM 프로파일링, 스레드 덤프를 활용하세요. 리액티브 모델을 도입할지 결정할 때는 라이브러리 호환성과 팀 기술 스택을 고려하세요.
맺음말
Spring Boot는 생산성을 크게 높여주지만, 자동화된 결정들이 내부 동작을 숨길 수 있어 시스템 설계 시 주의가 필요합니다. 이 글의 핵심 개념과 트레이드오프를 바탕으로, 작은 샘플부터 시작해 점진적으로 제어를 늘려가는 방식으로 적용하면 위험을 줄일 수 있습니다.