Spring Profiles란
Spring Profiles는 환경별로 다른 설정과 빈을 활성화하는 메커니즘이다. 개발(dev), 테스트(test), 스테이징(staging), 운영(prod) 환경마다 DB 접속 정보, 로그 레벨, 외부 API URL, 캐시 전략 등이 다를 수밖에 없다. Profiles를 사용하면 코드 변경 없이 환경 변수나 설정 파일만으로 애플리케이션의 동작을 전환할 수 있다.
단순한 설정값 분리를 넘어 특정 환경에서만 등록되는 빈, 환경별 다른 구현체 주입 등 강력한 조건부 구성을 지원한다.
Profile 활성화 방법
| 방법 | 예시 | 우선순위 |
|---|---|---|
| 환경 변수 | SPRING_PROFILES_ACTIVE=prod |
높음 |
| JVM 인자 | -Dspring.profiles.active=prod |
높음 |
| application.yml | spring.profiles.active: dev |
낮음 |
| 프로그래밍 | SpringApplication.setAdditionalProfiles() |
낮음 |
| 테스트 | @ActiveProfiles("test") |
테스트 전용 |
# Docker에서 실행
docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest
# JAR 직접 실행
java -jar app.jar --spring.profiles.active=prod
# Kubernetes Deployment
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
# 여러 프로필 동시 활성화
SPRING_PROFILES_ACTIVE=prod,redis,monitoring
YAML 설정 파일 분리
Spring Boot는 프로필별 설정 파일을 자동으로 로드한다. 공통 설정은 application.yml에, 환경별 설정은 application-{profile}.yml에 작성한다.
# application.yml — 공통 설정 (모든 환경)
spring:
application:
name: my-api
jpa:
open-in-view: false
properties:
hibernate:
default_batch_fetch_size: 100
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health,info,prometheus
# application-dev.yml — 개발 환경
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp_dev
username: dev_user
password: dev_pass
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
root: INFO
com.myapp: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql: TRACE
# application-prod.yml — 운영 환경
spring:
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
jpa:
hibernate:
ddl-auto: none # 운영에서 절대 auto DDL 금지
show-sql: false
logging:
level:
root: WARN
com.myapp: INFO
pattern:
console: '{"time":"%d","level":"%p","logger":"%logger","msg":"%m"}%n'
# application-test.yml — 테스트 환경
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
database-platform: org.hibernate.dialect.H2Dialect
logging:
level:
root: WARN
@Profile: 조건부 빈 등록
특정 프로필에서만 빈을 등록하여 환경별로 다른 구현체를 주입할 수 있다.
// 인터페이스
public interface StorageService {
String upload(MultipartFile file);
byte[] download(String key);
}
// 개발: 로컬 파일시스템
@Service
@Profile("dev")
public class LocalStorageService implements StorageService {
private final Path uploadDir = Path.of("./uploads");
@Override
public String upload(MultipartFile file) {
Path target = uploadDir.resolve(file.getOriginalFilename());
Files.copy(file.getInputStream(), target);
return target.toString();
}
@Override
public byte[] download(String key) {
return Files.readAllBytes(Path.of(key));
}
}
// 운영: AWS S3
@Service
@Profile("prod")
public class S3StorageService implements StorageService {
private final S3Client s3;
private final String bucket;
public S3StorageService(S3Client s3,
@Value("${aws.s3.bucket}") String bucket) {
this.s3 = s3;
this.bucket = bucket;
}
@Override
public String upload(MultipartFile file) {
String key = UUID.randomUUID() + "/" + file.getOriginalFilename();
s3.putObject(
PutObjectRequest.builder().bucket(bucket).key(key).build(),
RequestBody.fromInputStream(file.getInputStream(), file.getSize())
);
return key;
}
@Override
public byte[] download(String key) {
return s3.getObject(
GetObjectRequest.builder().bucket(bucket).key(key).build()
).readAllBytes();
}
}
@Profile 고급: NOT, OR 표현식
// 운영 환경이 아닐 때만 활성화
@Configuration
@Profile("!prod")
public class DevToolsConfig {
@Bean
public DataSourceInitializer dataSourceInitializer(DataSource ds) {
// 개발/테스트에서만 샘플 데이터 삽입
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("data-sample.sql"));
DataSourceInitializer init = new DataSourceInitializer();
init.setDataSource(ds);
init.setDatabasePopulator(populator);
return init;
}
}
// OR 조건: dev 또는 staging에서 활성화
@Service
@Profile({"dev", "staging"})
public class MockPaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) {
// 항상 성공 반환 (테스트용)
return PaymentResult.success("mock-txn-" + UUID.randomUUID());
}
}
// AND 조건: @Conditional 조합
@Configuration
@Profile("prod")
@ConditionalOnProperty(name = "feature.redis-cache", havingValue = "true")
public class RedisCacheConfig {
// prod + redis-cache=true일 때만 활성화
}
Profile Groups: 프로필 묶기
Spring Boot 2.4+에서 도입된 Profile Groups로 여러 프로필을 하나로 묶을 수 있다.
# application.yml
spring:
profiles:
group:
prod:
- prod-db
- prod-cache
- prod-monitoring
dev:
- dev-db
- dev-tools
# SPRING_PROFILES_ACTIVE=prod 설정 시
# → prod-db, prod-cache, prod-monitoring 프로필도 함께 활성화
# application-prod-db.yml
spring:
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
hikari:
maximum-pool-size: 30
# application-prod-cache.yml
spring:
cache:
type: redis
data:
redis:
host: ${REDIS_HOST}
cluster:
nodes: ${REDIS_CLUSTER_NODES}
# application-prod-monitoring.yml
management:
metrics:
export:
prometheus:
enabled: true
@ConfigurationProperties + Profile
타입 안전한 설정 바인딩과 Profile을 결합하면 환경별 설정을 객체로 관리할 수 있다.
@ConfigurationProperties(prefix = "app")
@Validated
public record AppProperties(
@NotBlank String name,
@NotNull ApiProperties api,
@NotNull SecurityProperties security
) {
public record ApiProperties(
@NotBlank String baseUrl,
@Min(1000) int timeoutMs,
int maxRetries
) {}
public record SecurityProperties(
@NotBlank String jwtSecret,
Duration jwtExpiration,
List<String> corsOrigins
) {}
}
// application-dev.yml
app:
name: my-api-dev
api:
base-url: http://localhost:8081
timeout-ms: 10000
max-retries: 1
security:
jwt-secret: dev-secret-not-for-production
jwt-expiration: 24h
cors-origins:
- http://localhost:3000
- http://localhost:5173
// application-prod.yml
app:
name: my-api
api:
base-url: https://api.internal.example.com
timeout-ms: 3000
max-retries: 3
security:
jwt-secret: ${JWT_SECRET}
jwt-expiration: 1h
cors-origins:
- https://app.example.com
테스트에서 Profile 활용
// 통합 테스트: test 프로필 자동 활성화
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Autowired
private StorageService storageService;
// → test 프로필이면 MockStorageService가 주입됨
@Test
void 주문_생성_성공() {
// H2 인메모리 DB 사용 (application-test.yml)
Order order = orderService.create(dto);
assertThat(order.getStatus()).isEqualTo(PENDING);
}
}
// 특정 프로필 조건 테스트
@SpringBootTest
@ActiveProfiles("prod")
class ProdConfigTest {
@Autowired
private StorageService storageService;
@Test
void 운영에서는_S3_구현체가_주입됨() {
assertThat(storageService).isInstanceOf(S3StorageService.class);
}
}
// 프로필별 설정값 검증
@SpringBootTest
@ActiveProfiles("prod")
class AppPropertiesTest {
@Autowired
private AppProperties props;
@Test
void 운영_타임아웃은_3초() {
assertThat(props.api().timeoutMs()).isEqualTo(3000);
}
@Test
void 운영_CORS는_단일_도메인() {
assertThat(props.security().corsOrigins()).hasSize(1);
assertThat(props.security().corsOrigins().get(0))
.startsWith("https://");
}
}
운영 안전 장치
// 시작 시 활성 프로필 로깅 + 위험 설정 검증
@Component
@Slf4j
public class ProfileValidator implements ApplicationRunner {
@Value("${spring.profiles.active:default}")
private String activeProfiles;
@Value("${spring.jpa.hibernate.ddl-auto:none}")
private String ddlAuto;
@Override
public void run(ApplicationArguments args) {
log.info("활성 프로필: {}", activeProfiles);
if (activeProfiles.contains("prod")) {
// 운영에서 위험한 설정 차단
if (!"none".equals(ddlAuto) && !"validate".equals(ddlAuto)) {
throw new IllegalStateException(
"운영 환경에서 ddl-auto=" + ddlAuto + "는 허용되지 않습니다"
);
}
}
}
}
Spring Cache 추상화 실전에서 다룬 캐시 설정도 Profile로 환경별 전환이 가능하다. 개발에서는 Caffeine, 운영에서는 Redis로 무중단 교체하는 패턴이 대표적이다. Spring AOP 프록시 가이드의 Aspect도 @Profile과 결합하면 환경별로 활성화/비활성화할 수 있다.
정리: Profiles 설계 체크리스트
- 공통은 application.yml: 환경 무관한 설정은 기본 파일에 유지
- 환경별 파일 분리: application-{profile}.yml로 오버라이드
- Profile Groups: 관련 프로필을 묶어 활성화 단순화
- @Profile 빈: 환경별 다른 구현체 주입 (로컬 파일 vs S3)
- 운영 안전: ddl-auto=none 강제, show-sql=false, 시크릿은 환경 변수
- 테스트 격리: @ActiveProfiles(“test”)로 인메모리 DB 사용
- 검증: @Validated + @ConfigurationProperties로 시작 시 설정값 타입 체크