Environment와 Profile — 환경마다 다른 설정을 어떻게 관리할까
개발 환경에서는 H2 인메모리 DB를, 운영 환경에서는 MySQL을 써야 합니다. 코드를 고치지 않고 환경에 따라 설정을 바꾸려면 어떻게 해야 할까요?
개념 정의
Environment 는 스프링이 애플리케이션의 설정 정보(프로퍼티)와 활성화된 프로파일 정보를 관리하는 추상화입니다. Profile 은 특정 환경에서만 활성화할 빈이나 설정을 구분하는 메커니즘입니다.
왜 필요한가
환경마다 달라지는 것들이 많습니다.
- DB 접속 정보 (로컬 H2 vs 운영 MySQL)
- 외부 API URL (개발용 샌드박스 vs 운영 API)
- 캐시 설정 (로컬에서는 비활성화, 운영에서는 Redis)
- 로그 레벨 (개발 DEBUG vs 운영 WARN)
이런 차이를 코드에 if-else로 넣으면 유지보수가 어렵습니다. 프로파일을 사용하면 설정 파일과 빈 등록을 환경별로 깔끔하게 분리할 수 있습니다.
내부 동작
프로파일 활성화 방법
# application.yml
spring:
profiles:
active: dev # dev 프로파일 활성화
# 커맨드 라인
java -jar app.jar --spring.profiles.active=prod
# 환경 변수
export SPRING_PROFILES_ACTIVE=prod
# JVM 시스템 프로퍼티
java -Dspring.profiles.active=prod -jar app.jar
프로파일별 설정 파일
src/main/resources/
├── application.yml # 공통 설정
├── application-dev.yml # dev 프로파일 설정
├── application-prod.yml # prod 프로파일 설정
└── application-test.yml # test 프로파일 설정
# application.yml (공통)
server:
port: 8080
spring:
application:
name: my-app
# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:devdb
driver-class-name: org.h2.Driver
logging:
level:
root: DEBUG
이어서 나머지 설정 항목을 추가합니다.
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/myapp
driver-class-name: com.mysql.cj.jdbc.Driver
logging:
level:
root: WARN
application-{profile}.yml의 설정은 공통 application.yml의 같은 키를 덮어씁니다.
spring.profiles.include와 group
# application-prod.yml
spring:
profiles:
include:
- prod-db # 항상 함께 활성화
- prod-cache
스프링부트 2.4+에서는 group을 더 권장합니다.
# application.yml
spring:
profiles:
group:
prod:
- prod-db
- prod-cache
- prod-monitoring
dev:
- dev-db
- dev-mock
spring.profiles.active=prod만 설정하면 prod-db, prod-cache, prod-monitoring이 모두 활성화됩니다.
PropertySource 우선순위
스프링부트에서 프로퍼티가 로드되는 우선순위입니다 (높은 순서).
1. 커맨드 라인 인자 (--server.port=9090)
2. JVM 시스템 프로퍼티 (-Dserver.port=9090)
3. OS 환경 변수 (SERVER_PORT=9090)
4. 프로파일별 외부 application-{profile}.yml
5. 프로파일별 내부 application-{profile}.yml
6. 외부 application.yml
7. 내부 application.yml (src/main/resources)
8. @PropertySource
9. 기본값
숫자가 작을수록 우선순위가 높습니다. 같은 키가 여러 곳에 정의되면 우선순위가 높은 값이 사용됩니다.
코드 예제
@Profile로 환경별 빈 등록
public interface StorageService {
void store(String filename, byte[] data);
}
@Profile("dev")
@Service
public class LocalStorageService implements StorageService {
@Override
public void store(String filename, byte[] data) {
// 로컬 파일 시스템에 저장
Files.write(Path.of("/tmp/uploads/" + filename), data);
}
}
이어서 @Profile을 적용한 나머지 구현부입니다.
@Profile("prod")
@Service
public class S3StorageService implements StorageService {
@Override
public void store(String filename, byte[] data) {
// AWS S3에 업로드
s3Client.putObject(PutObjectRequest.builder()
.bucket("my-bucket")
.key(filename)
.build(), RequestBody.fromBytes(data));
}
}
dev 프로파일에서는 LocalStorageService가, prod 프로파일에서는 S3StorageService가 자동으로 등록됩니다.
@Profile의 다양한 표현식
@Profile("dev") // dev일 때만
@Profile("!prod") // prod가 아닐 때
@Profile({"dev", "test"}) // dev 또는 test일 때
@Profile("prod & secure") // prod이면서 secure일 때 (Spring 5.1+는 표현식 지원 제한적)
Environment API 직접 사용
@Service
@RequiredArgsConstructor
public class FeatureToggleService {
private final Environment environment;
public boolean isFeatureEnabled(String feature) {
// 현재 활성 프로파일 확인
if (environment.acceptsProfiles(Profiles.of("beta"))) {
return true; // beta 프로파일이면 모든 기능 활성화
}
이어서 나머지 구현 부분입니다.
// 프로퍼티 값으로 기능 토글
return environment.getProperty("feature." + feature + ".enabled",
Boolean.class, false);
}
public void printActiveProfiles() {
String[] profiles = environment.getActiveProfiles();
System.out.println("활성 프로파일: " + Arrays.toString(profiles));
}
}
커스텀 PropertySource 등록
@Configuration
@PropertySource("classpath:custom.properties")
@PropertySource("classpath:${app.config.location:defaults}.properties")
public class CustomPropertyConfig {
// custom.properties 파일의 프로퍼티가 Environment에 추가됨
}
테스트에서 프로파일 사용
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceTest {
// test 프로파일 설정으로 테스트 실행
// application-test.yml의 설정이 적용됨
}
YAML 멀티 도큐먼트로 프로파일 설정
하나의 application.yml 파일에서 ---로 구분하여 프로파일별 설정을 작성할 수도 있습니다.
# 공통 설정
server:
port: 8080
---
# dev 프로파일
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
이어서 나머지 설정 항목을 추가합니다.
---
# prod 프로파일
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-db:3306/myapp
환경 변수와 프로퍼티 매핑 규칙
# application.yml
my-app:
api:
base-url: https://api.example.com
timeout-seconds: 30
# 환경 변수로 오버라이드 (relaxed binding)
MY_APP_API_BASE_URL=https://staging-api.example.com
MY_APP_API_TIMEOUT_SECONDS=60
스프링부트의 Relaxed Binding은 my-app.api.base-url을 MY_APP_API_BASE_URL과 자동으로 매핑합니다.
주의할 점
1. 프로파일을 지정하지 않으면 프로파일별 빈이 하나도 등록되지 않는다
@Profile("prod")와 @Profile("dev")만 있고 기본 구현체가 없으면, 프로파일을 설정하지 않았을 때 해당 타입의 빈이 아예 없어 NoSuchBeanDefinitionException이 발생합니다. 반드시 기본 프로파일이나 @Profile("!prod") 같은 부정 표현식으로 폴백을 제공해야 합니다.
2. application-{profile}.yml의 프로퍼티 우선순위를 모르면 설정이 덮어써진다
application-prod.yml의 설정은 application.yml의 같은 키를 덮어씁니다. 공통 yml에서 의도적으로 설정한 값이 프로파일별 yml에 의해 무시될 수 있습니다. 특히 환경 변수가 모든 yml보다 우선하므로, CI/CD에서 설정한 환경 변수가 yml 설정을 예상치 못하게 덮어쓰는 사고가 빈번합니다.
3. 프로덕션 환경에서 dev 프로파일이 활성화되면 민감 정보가 노출된다
spring.profiles.active=dev가 운영 서버에 남아 있으면 H2 콘솔이 활성화되거나, DEBUG 로그에 SQL 파라미터와 민감한 데이터가 출력될 수 있습니다. 프로파일 설정은 배포 파이프라인에서 강제하고, 운영 환경에서는 dev 프로파일이 절대 활성화되지 않도록 검증해야 합니다.
정리
| 항목 | 설명 |
|---|---|
| Profile | 환경마다 다른 설정과 빈을 코드 변경 없이 전환 |
spring.profiles.group | 관련 프로파일을 하나로 묶어 관리 |
| 우선순위 | 커맨드라인 > JVM 프로퍼티 > 환경변수 > 프로파일 yml > 기본 yml |
@Profile | 특정 프로파일에서만 빈 등록 |
| 테스트 | @ActiveProfiles("test")로 프로파일 활성화 |
| 주의 | 기본 구현체 없으면 프로파일 미지정 시 NoSuchBeanDefinitionException |