개발 환경에서는 H2 인메모리 DB를, 운영 환경에서는 MySQL을 써야 합니다. 코드를 고치지 않고 환경에 따라 설정을 바꾸려면 어떻게 해야 할까요?

개념 정의

Environment 는 스프링이 애플리케이션의 설정 정보(프로퍼티)와 활성화된 프로파일 정보를 관리하는 추상화입니다. Profile 은 특정 환경에서만 활성화할 빈이나 설정을 구분하는 메커니즘입니다.

왜 필요한가

환경마다 달라지는 것들이 많습니다.

  • DB 접속 정보 (로컬 H2 vs 운영 MySQL)
  • 외부 API URL (개발용 샌드박스 vs 운영 API)
  • 캐시 설정 (로컬에서는 비활성화, 운영에서는 Redis)
  • 로그 레벨 (개발 DEBUG vs 운영 WARN)

이런 차이를 코드에 if-else로 넣으면 유지보수가 어렵습니다. 프로파일을 사용하면 설정 파일과 빈 등록을 환경별로 깔끔하게 분리할 수 있습니다.

내부 동작

프로파일 활성화 방법

YAML
# application.yml
spring:
  profiles:
    active: dev  # dev 프로파일 활성화
BASH
# 커맨드 라인
java -jar app.jar --spring.profiles.active=prod

# 환경 변수
export SPRING_PROFILES_ACTIVE=prod

# JVM 시스템 프로퍼티
java -Dspring.profiles.active=prod -jar app.jar

프로파일별 설정 파일

PLAINTEXT
src/main/resources/
├── application.yml           # 공통 설정
├── application-dev.yml       # dev 프로파일 설정
├── application-prod.yml      # prod 프로파일 설정
└── application-test.yml      # test 프로파일 설정
YAML
# 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

이어서 나머지 설정 항목을 추가합니다.

YAML
# 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

YAML
# application-prod.yml
spring:
  profiles:
    include:
      - prod-db       # 항상 함께 활성화
      - prod-cache

스프링부트 2.4+에서는 group을 더 권장합니다.

YAML
# 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 우선순위

스프링부트에서 프로퍼티가 로드되는 우선순위입니다 (높은 순서).

PLAINTEXT
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로 환경별 빈 등록

JAVA
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을 적용한 나머지 구현부입니다.

JAVA
@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의 다양한 표현식

JAVA
@Profile("dev")              // dev일 때만
@Profile("!prod")            // prod가 아닐 때
@Profile({"dev", "test"})    // dev 또는 test일 때
@Profile("prod & secure")    // prod이면서 secure일 때 (Spring 5.1+는 표현식 지원 제한적)

Environment API 직접 사용

JAVA
@Service
@RequiredArgsConstructor
public class FeatureToggleService {
    private final Environment environment;

    public boolean isFeatureEnabled(String feature) {
        // 현재 활성 프로파일 확인
        if (environment.acceptsProfiles(Profiles.of("beta"))) {
            return true; // beta 프로파일이면 모든 기능 활성화
        }

이어서 나머지 구현 부분입니다.

JAVA
        // 프로퍼티 값으로 기능 토글
        return environment.getProperty("feature." + feature + ".enabled",
                                        Boolean.class, false);
    }

    public void printActiveProfiles() {
        String[] profiles = environment.getActiveProfiles();
        System.out.println("활성 프로파일: " + Arrays.toString(profiles));
    }
}

커스텀 PropertySource 등록

JAVA
@Configuration
@PropertySource("classpath:custom.properties")
@PropertySource("classpath:${app.config.location:defaults}.properties")
public class CustomPropertyConfig {
    // custom.properties 파일의 프로퍼티가 Environment에 추가됨
}

테스트에서 프로파일 사용

JAVA
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceTest {
    // test 프로파일 설정으로 테스트 실행
    // application-test.yml의 설정이 적용됨
}

YAML 멀티 도큐먼트로 프로파일 설정

하나의 application.yml 파일에서 ---로 구분하여 프로파일별 설정을 작성할 수도 있습니다.

YAML
# 공통 설정
server:
  port: 8080

---
# dev 프로파일
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:devdb

이어서 나머지 설정 항목을 추가합니다.

YAML
---
# prod 프로파일
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://prod-db:3306/myapp

환경 변수와 프로퍼티 매핑 규칙

YAML
# application.yml
my-app:
  api:
    base-url: https://api.example.com
    timeout-seconds: 30
BASH
# 환경 변수로 오버라이드 (relaxed binding)
MY_APP_API_BASE_URL=https://staging-api.example.com
MY_APP_API_TIMEOUT_SECONDS=60

스프링부트의 Relaxed Binding은 my-app.api.base-urlMY_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
댓글 로딩 중...