Spring 조건부 빈 등록 심화

Spring @ConditionalOnProperty란?

Spring Boot 자동 설정의 핵심은 조건부 빈 등록이다. @ConditionalOnProperty는 설정 파일의 특정 프로퍼티 값에 따라 빈을 등록하거나 건너뛴다. 이를 활용하면 코드 변경 없이 설정만으로 기능을 켜고 끌 수 있다.

# application.yml
app:
  cache:
    enabled: true
  notification:
    channel: slack
// cache.enabled=true일 때만 빈 등록
@Configuration
@ConditionalOnProperty(
    prefix = "app.cache",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = false  // 프로퍼티 없으면 빈 미등록
)
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager();
    }
}

핵심 속성 4가지

속성 설명 예시
prefix 프로퍼티 접두사 "app.cache"
name 프로퍼티 이름 (prefix와 결합) "enabled" → app.cache.enabled
havingValue 매칭할 값 (생략 시 존재만 확인) "true", "slack"
matchIfMissing 프로퍼티 없을 때 매칭 여부 true = 기본 활성화

실전 패턴: Feature Toggle

기능 플래그를 프로퍼티로 관리하는 가장 실용적인 패턴이다:

# application.yml
features:
  new-pricing: true
  beta-search: false
  export-pdf: true
// 새 가격 정책 활성화 시에만 등록
@Service
@ConditionalOnProperty(name = "features.new-pricing", havingValue = "true")
public class NewPricingService implements PricingService {
    public BigDecimal calculate(Order order) {
        // 새 가격 로직
    }
}

// 비활성화 시 기존 서비스 사용
@Service
@ConditionalOnProperty(name = "features.new-pricing", havingValue = "false",
                       matchIfMissing = true)
public class LegacyPricingService implements PricingService {
    public BigDecimal calculate(Order order) {
        // 기존 가격 로직
    }
}

핵심: 같은 인터페이스의 두 구현체를 @ConditionalOnProperty로 토글한다. 설정만 바꾸면 코드 배포 없이 기능을 전환할 수 있다.

알림 채널 전환

public interface NotificationSender {
    void send(String userId, String message);
}

@Service
@ConditionalOnProperty(name = "app.notification.channel", havingValue = "slack")
public class SlackNotificationSender implements NotificationSender {
    public void send(String userId, String message) {
        // Slack API 호출
    }
}

@Service
@ConditionalOnProperty(name = "app.notification.channel", havingValue = "email")
public class EmailNotificationSender implements NotificationSender {
    public void send(String userId, String message) {
        // 이메일 발송
    }
}

@Service
@ConditionalOnProperty(name = "app.notification.channel", havingValue = "noop",
                       matchIfMissing = true)
public class NoopNotificationSender implements NotificationSender {
    public void send(String userId, String message) {
        // 아무것도 안 함 (개발/테스트 환경)
    }
}

다른 @Conditional 어노테이션들

Spring Boot는 다양한 조건부 어노테이션을 제공한다:

// 클래스패스에 특정 클래스가 있을 때만
@ConditionalOnClass(RedisConnectionFactory.class)
public class RedisConfig { ... }

// 특정 빈이 없을 때만 (기본 구현 제공)
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager defaultCacheManager() {
    return new ConcurrentMapCacheManager();
}

// 특정 빈이 존재할 때만
@Bean
@ConditionalOnBean(DataSource.class)
public JdbcTemplate jdbcTemplate(DataSource ds) {
    return new JdbcTemplate(ds);
}

// 특정 프로파일일 때만
@Configuration
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "prod")
// 또는 더 단순하게:
@Profile("prod")
public class ProdConfig { ... }

조건 조합

// 여러 조건 AND 결합 (모두 만족해야 등록)
@Configuration
@ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true")
@ConditionalOnClass(RedisConnectionFactory.class)
@ConditionalOnBean(RedisConnectionFactory.class)
public class RedisCacheConfig {
    // cache 활성화 + Redis 클래스 존재 + Redis 빈 존재 시에만
}

Custom Starter에서의 활용

Custom Starter의 자동 설정에서 @ConditionalOnProperty는 필수다:

@AutoConfiguration
@EnableConfigurationProperties(MyLibProperties.class)
@ConditionalOnProperty(
    prefix = "mylib",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = true  // 기본 활성화
)
public class MyLibAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // 사용자가 직접 정의하면 스킵
    public MyService myService(MyLibProperties props) {
        return new DefaultMyService(props);
    }

    @Bean
    @ConditionalOnProperty(name = "mylib.metrics.enabled", havingValue = "true")
    public MyMetricsCollector metricsCollector() {
        return new MyMetricsCollector();
    }
}

matchIfMissing = true@ConditionalOnMissingBean의 조합이 핵심이다. 라이브러리는 기본적으로 동작하되, 사용자가 커스터마이즈할 수 있는 유연한 구조를 제공한다.

커스텀 Condition 작성

기본 제공 어노테이션으로 부족하면, Condition 인터페이스를 직접 구현한다:

// 환경 변수 기반 조건
public class OnKubernetesCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context,
                          AnnotatedTypeMetadata metadata) {
        return System.getenv("KUBERNETES_SERVICE_HOST") != null;
    }
}

@Configuration
@Conditional(OnKubernetesCondition.class)
public class K8sConfig {
    // K8s 환경에서만 등록
}

테스트에서 조건부 빈 검증

@SpringBootTest(properties = "features.new-pricing=true")
class NewPricingTest {
    @Autowired
    private PricingService pricingService;

    @Test
    void shouldUseNewPricing() {
        assertThat(pricingService).isInstanceOf(NewPricingService.class);
    }
}

@SpringBootTest(properties = "features.new-pricing=false")
class LegacyPricingTest {
    @Autowired
    private PricingService pricingService;

    @Test
    void shouldUseLegacyPricing() {
        assertThat(pricingService).isInstanceOf(LegacyPricingService.class);
    }
}

// ApplicationContextRunner로 가벼운 테스트
@Test
void shouldRegisterCacheWhenEnabled() {
    new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(CacheConfig.class))
        .withPropertyValues("app.cache.enabled=true")
        .run(context -> {
            assertThat(context).hasSingleBean(CacheManager.class);
        });
}

환경별 설정 전략

Spring Profiles@ConditionalOnProperty를 조합하면 환경별로 다른 빈을 깔끔하게 관리할 수 있다:

# application-dev.yml
app:
  storage:
    type: local
  notification:
    channel: noop

# application-prod.yml
app:
  storage:
    type: s3
  notification:
    channel: slack

개발 환경에서는 로컬 스토리지와 Noop 알림, 프로덕션에서는 S3와 Slack을 자동으로 사용한다.

정리

@ConditionalOnProperty는 Spring Boot의 설정 기반 빈 등록 핵심이다. Feature Toggle, 알림 채널 전환, 환경별 구현체 교체 등을 코드 변경 없이 설정만으로 제어할 수 있다. @ConditionalOnMissingBean, @ConditionalOnClass와 조합하면 Custom Starter의 유연한 자동 설정까지 구현할 수 있다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux