본문으로 건너뛰기
김신건의 로그

[Spring] @Conditional, Profile, @ConfigurationProperties

· 수정 · 📖 약 1분 · 410자/단어 #spring #conditional #profile #configuration
spring conditional, @Profile, @ConfigurationProperties, auto-configuration condition

정의

Spring Boot는 조건부 Bean 등록 메커니즘을 제공한다. classpath, property, 다른 Bean 유무 등에 따라 자동 구성을 활성화/비활성화. @Profile, @ConditionalOnX 어노테이션이 핵심.

@Profile

환경별 다른 Bean.

@Configuration
public class DataConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder().build();
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        // 진짜 DB
        ...
    }

    @Bean
    @Profile("!test")
    public EmailService realEmailService() {
        return new SmtpEmailService();
    }

    @Bean
    @Profile({"dev", "test"})
    public EmailService mockEmailService() {
        return new LoggingEmailService();
    }
}

활성 프로파일에 매칭되는 Bean만 등록.

# application.yml
spring:
  profiles:
    active: prod

---
spring:
  config:
    activate:
      on-profile: dev

custom:
  feature: enabled

spring.profiles.active=prod 또는 SPRING_PROFILES_ACTIVE=prod.

@ConditionalOnX (Spring Boot)

@ConditionalOnClass / @ConditionalOnMissingClass

@Configuration
@ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource")
public class HikariConfig {
    @Bean
    public DataSource hikariDataSource() {
        return new HikariDataSource(...);
    }
}

classpath에 HikariCP가 있을 때만.

@ConditionalOnBean / @ConditionalOnMissingBean

@Bean
@ConditionalOnMissingBean    // 사용자가 정의 안 했으면 기본 Bean
public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager();
}

Spring Boot 자동 구성의 핵심 패턴. 사용자가 같은 타입 정의하면 자동 양보.

@ConditionalOnProperty

@Bean
@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true")
public FeatureService featureService() { ... }

@Bean
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis", matchIfMissing = false)
public RedisCacheManager redisCacheManager() { ... }

@Bean
@ConditionalOnProperty(prefix = "app", name = "auth-method", havingValue = "jwt")
public JwtAuthFilter jwtAuthFilter() { ... }

property 값 조건. 신규 기능 토글, 환경별 분기.

@ConditionalOnExpression (SpEL)

@Bean
@ConditionalOnExpression("${app.feature.enabled:false} and '${spring.profiles.active}' != 'test'")
public FeatureService featureService() { ... }

복잡한 조건.

@ConditionalOnWebApplication / @ConditionalOnNotWebApplication

@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public ServletBean servletBean() { ... }

@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public ReactiveBean reactiveBean() { ... }

@ConditionalOnJava

@Bean
@ConditionalOnJava(JavaVersion.SEVENTEEN)    // Java 17+
public ModernBean modernBean() { ... }

@ConditionalOnResource

@Bean
@ConditionalOnResource(resources = "classpath:my-config.json")
public MyConfig myConfig() { ... }

특정 리소스 파일 존재 시.

커스텀 Conditional

public class OnLinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }
}

@Bean
@Conditional(OnLinuxCondition.class)
public LinuxService linuxService() { ... }

@ConfigurationProperties

타입 안전 외부 설정.

기본

@ConfigurationProperties(prefix = "app")
public record AppProperties(
    String name,
    Database database,
    int timeout,
    List<String> tags,
    Map<String, String> features
) {
    public record Database(
        String url,
        String username,
        String password,
        int poolSize
    ) { }
}
app:
  name: myapp
  timeout: 30
  tags:
    - production
    - east
  features:
    new-ui: "true"
    legacy-api: "false"
  database:
    url: jdbc:postgresql://localhost/myapp
    username: app
    password: secret
    pool-size: 10

활성화

@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class App { }

// 또는 @ConfigurationPropertiesScan
@ConfigurationPropertiesScan("com.example.config")

사용

@Service
public class MyService {
    private final AppProperties props;

    public MyService(AppProperties props) {
        this.props = props;
    }

    public void doIt() {
        if (props.features().get("new-ui").equals("true")) {
            ...
        }
    }
}

검증

@ConfigurationProperties(prefix = "app")
@Validated
public record AppProperties(
    @NotBlank String name,
    @Min(0) @Max(600) int timeout,
    @Valid Database database
) {
    public record Database(
        @NotBlank String url,
        @Min(1) int poolSize
    ) { }
}

시작 시 검증 실패 → 시작 실패. 환경 잘못 발견 즉시.

관계형 properties

app:
  servers:
    - host: server1.example.com
      port: 8080
    - host: server2.example.com
      port: 8081
@ConfigurationProperties(prefix = "app")
public record AppProperties(List<Server> servers) {
    public record Server(String host, int port) { }
}

meta-data

IDE 자동완성을 위해 additional-spring-configuration-metadata.json 또는 annotation processor:

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

자동으로 META-INF/spring-configuration-metadata.json 생성. IntelliJ에서 yaml 자동완성.

자주 보는 패턴

Feature flag

@ConfigurationProperties("app.features")
public record FeatureFlags(
    boolean newDashboard,
    boolean aiSuggestions,
    boolean betaPayments
) { }
app:
  features:
    new-dashboard: true
    ai-suggestions: false
    beta-payments: true
@Service
public class DashboardService {
    private final FeatureFlags features;

    public Dashboard render() {
        return features.newDashboard() ? renderNew() : renderClassic();
    }
}

런타임 변경은 별도 (Flagsmith, Unleash, LaunchDarkly).

환경별 Bean

@Configuration
@Profile("prod")
public class ProductionConfig {
    @Bean public ExternalApi externalApi() { ... }
}

@Configuration
@Profile({"dev", "test"})
public class MockConfig {
    @Bean public ExternalApi mockApi() { ... }
}

@ConfigurationProperties Bean

@ConfigurationProperties("aws")
public class AwsProperties {
    private String accessKey;
    private String secretKey;
    private String region;
    // setter 필요 (lombok @Setter)
}

@SpringBootApplication
@EnableConfigurationProperties(AwsProperties.class)
public class App { }

record (3.0+)가 더 간결.

함정

1. @Value vs @ConfigurationProperties

@Value("${app.name}")
private String name;

vs

@ConfigurationProperties("app")
public record AppProperties(String name) { }

10개 이상 또는 nested → properties. 1-2개면 @Value도 OK.

2. property naming

public record AppProperties(String apiKey) { }    // Java camelCase
app:
  api-key: ...    # kebab-case 권장
  apiKey: ...     # camelCase OK
  api_key: ...    # snake_case OK (relaxed binding)

여러 표기 자동 매핑. yaml은 kebab-case가 convention.

3. Conditional 평가 시점

@ConditionalOnProperty는 시작 시 한 번 평가. 런타임 property 변경엔 영향 X.

4. @ConfigurationProperties + immutable

public record AppProperties(String name) { }    // record OK
public class AppProperties {    // mutable
    @Setter @Getter String name;
}

record는 setter 없어 @ConfigurationProperties 사용 시 컨스트럭터 주입.

5. 시크릿을 properties에

app:
  api:
    key: ${API_KEY}        # ENV 참조 → OK
    key: hardcoded         # 위험 (코드에 비밀)

비밀은 항상 환경변수 또는 secrets manager 통해.

Auto-configuration 만들기

@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyProperties props) {
        return new MyService(props.url());
    }
}
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration

라이브러리 사용자가 dependency 추가만으로 자동 활성화.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기