Spring Boot는 DataSource, Security, Redis 등 수십 개의 자동 설정을 가지고 있는데, 어떻게 필요한 것만 골라서 적용하는 걸까요?

조건부 어노테이션이란

Spring Boot의 자동 설정(AutoConfiguration)은 조건부 어노테이션 으로 활성화 여부가 결정됩니다. 특정 클래스가 클래스패스에 있는지, 프로퍼티가 설정되어 있는지, 빈이 이미 등록되어 있는지 등의 조건을 평가하여 설정을 적용하거나 건너뜁니다.

주요 조건부 어노테이션

@ConditionalOnClass / @ConditionalOnMissingClass

클래스패스에 특정 클래스가 있는지/없는지 확인합니다.

JAVA
@AutoConfiguration
@ConditionalOnClass(DataSource.class)  // DataSource 클래스가 있을 때만
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // 사용자가 직접 정의하지 않았을 때만
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

spring-boot-starter-jdbc를 의존성에 추가하면 DataSource 클래스가 클래스패스에 존재하게 되어 이 자동 설정이 활성화됩니다. 의존성을 제거하면 비활성화됩니다.

@ConditionalOnBean / @ConditionalOnMissingBean

특정 빈이 이미 등록되어 있는지/없는지 확인합니다.

JAVA
@AutoConfiguration
public class JacksonAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // ObjectMapper 빈이 없을 때만 생성
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        return builder.build();
    }
}

// 사용자가 직접 정의하면 자동 설정은 건너뜀
@Configuration
public class MyJacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return mapper;  // 이 빈이 우선
    }
}

@ConditionalOnProperty

프로퍼티 값에 따라 활성화합니다.

JAVA
@Configuration
@ConditionalOnProperty(
    name = "app.feature.notification.enabled",
    havingValue = "true",
    matchIfMissing = true  // 프로퍼티가 없어도 기본 활성화
)
public class NotificationAutoConfiguration {

    @Bean
    public NotificationService notificationService() {
        return new EmailNotificationService();
    }
}
YAML
# 명시적으로 끄기
app:
  feature:
    notification:
      enabled: false

@ConditionalOnWebApplication / @ConditionalOnNotWebApplication

JAVA
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
public class WebMvcConfiguration {
    // 서블릿 웹 환경에서만 적용
}

@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
public class WebFluxConfiguration {
    // 리액티브 웹 환경에서만 적용
}

@ConditionalOnExpression

SpEL 표현식으로 복잡한 조건을 표현합니다.

JAVA
@Configuration
@ConditionalOnExpression(
    "${app.cache.enabled:true} and '${app.cache.type}' == 'redis'"
)
public class RedisCacheConfig {
    // app.cache.enabled=true이고 app.cache.type=redis일 때만
}

조건 평가 순서

조건부 어노테이션은 평가 순서가 있습니다.

PLAINTEXT
1. @ConditionalOnClass / @ConditionalOnMissingClass  ← 가장 먼저 (빠른 필터링)
2. @ConditionalOnBean / @ConditionalOnMissingBean
3. @ConditionalOnProperty
4. @ConditionalOnResource
5. @ConditionalOnExpression
6. 기타 커스텀 Condition

클래스 레벨의 @ConditionalOnClass가 실패하면 내부의 @Bean 메서드 조건은 평가하지도 않습니다. 빠른 실패를 위해 가장 비용이 적은 조건을 먼저 배치합니다.

동작 원리 — Condition 인터페이스

모든 조건부 어노테이션은 내부적으로 Condition 인터페이스를 구현합니다.

JAVA
@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

Spring Boot는 SpringBootCondition을 확장하여 각 @Conditional 어노테이션의 로직을 구현합니다.

JAVA
// @ConditionalOnClass의 내부 구현 (간략화)
class OnClassCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(
            ConditionContext context, AnnotatedTypeMetadata metadata) {

        // ASM으로 바이트코드 수준에서 클래스 존재 여부 확인
        // Class.forName()을 쓰지 않으므로 클래스 로딩 없이 검사 가능
        List<String> missing = filter(candidates, ClassNameFilter.MISSING,
            context.getClassLoader());

        if (!missing.isEmpty()) {
            return ConditionOutcome.noMatch("클래스 없음: " + missing);
        }
        return ConditionOutcome.match();
    }
}

커스텀 Condition 만들기

JAVA
// 특정 OS에서만 동작하는 조건
public class OnLinuxCondition implements Condition {

    @Override
    public boolean matches(
            ConditionContext context, AnnotatedTypeMetadata metadata) {
        String os = context.getEnvironment()
            .getProperty("os.name", "").toLowerCase();
        return os.contains("linux");
    }
}

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

JAVA
// 사용
@Configuration
@Conditional(OnLinuxCondition.class)
public class LinuxSpecificConfig {
    @Bean
    public FileWatcher linuxFileWatcher() {
        return new InotifyFileWatcher();
    }
}

어노테이션으로 래핑

JAVA
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnLinux {
}

// 사용
@Configuration
@ConditionalOnLinux
public class LinuxSpecificConfig { }

자동 설정 디버깅 — --debug

BASH
java -jar myapp.jar --debug
# 또는 application.yml에서
# debug: true

출력 예:

PLAINTEXT
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:  (적용된 자동 설정)
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes
        'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
      - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory) did not find any beans

   HikariDataSource matched:
      - @ConditionalOnClass found required class 'com.zaxxer.hikari.HikariDataSource'
      - @ConditionalOnMissingBean did not find any beans of type 'javax.sql.DataSource'

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

PLAINTEXT
Negative matches:  (건너뛴 자동 설정)
-----------------
   MongoAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'com.mongodb.client.MongoClient'

   RedisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'org.springframework.data.redis.core.RedisOperations'

이 보고서를 통해 어떤 자동 설정이 왜 적용되었는지/건너뛰어졌는지 정확히 파악할 수 있습니다.

자동 설정 제외

특정 자동 설정을 강제로 비활성화하려면:

JAVA
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    SecurityAutoConfiguration.class
})
public class MyApp { }
YAML
# 또는 application.yml
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

실무 활용 패턴

Feature Toggle

JAVA
@Configuration
@ConditionalOnProperty(
    prefix = "feature",
    name = "new-payment-flow",
    havingValue = "true"
)
public class NewPaymentFlowConfig {
    @Bean
    public PaymentProcessor paymentProcessor() {
        return new NewPaymentProcessor();
    }
}

이어서 @Configuration을 적용한 나머지 구현부입니다.

JAVA
@Configuration
@ConditionalOnProperty(
    prefix = "feature",
    name = "new-payment-flow",
    havingValue = "false",
    matchIfMissing = true  // 기본: 이전 플로우
)
public class LegacyPaymentFlowConfig {
    @Bean
    public PaymentProcessor paymentProcessor() {
        return new LegacyPaymentProcessor();
    }
}

환경별 구현체 전환

JAVA
@Configuration
public class StorageConfig {

    @Bean
    @ConditionalOnProperty(name = "storage.type", havingValue = "s3")
    public StorageService s3Storage() {
        return new S3StorageService();
    }

    @Bean
    @ConditionalOnProperty(name = "storage.type", havingValue = "local",
        matchIfMissing = true)
    public StorageService localStorage() {
        return new LocalStorageService();
    }
}

주의할 점

1. @ConditionalOnMissingBean의 타입이 정확히 일치하지 않으면 빈이 중복 등록된다

자동 설정이 @ConditionalOnMissingBean(ObjectMapper.class)으로 체크하는데, 사용자가 ObjectMapper의 서브클래스를 빈으로 등록하면 조건이 "빈 없음"으로 평가되어 자동 설정 빈도 함께 등록됩니다. 같은 타입의 빈이 2개가 되어 NoUniqueBeanDefinitionException이 발생합니다. 커스텀 빈을 등록할 때는 자동 설정이 체크하는 정확한 타입을 확인하세요.

2. @ConditionalOnProperty의 matchIfMissing 기본값이 false라서 프로퍼티를 누락하면 기능이 꺼진다

@ConditionalOnProperty(name = "feature.enabled", havingValue = "true")에서 matchIfMissing을 지정하지 않으면 기본값이 false입니다. 프로퍼티를 설정하지 않으면 해당 기능이 비활성화됩니다. "기본으로 켜져 있을 줄 알았는데 안 됩니다"라는 문의가 이 원인인 경우가 많습니다. 기본 활성화가 의도라면 matchIfMissing = true를 명시하세요.

3. 조건부 어노테이션의 평가 순서를 모르면 디버깅이 어렵다

@ConditionalOnClass는 빈 등록 전에 평가되고, @ConditionalOnBean은 다른 빈이 등록된 후에 평가됩니다. 자동 설정 클래스 간의 로딩 순서에 따라 @ConditionalOnBean이 의도와 다르게 동작할 수 있습니다. --debug 플래그로 CONDITIONS EVALUATION REPORT를 확인하면 어떤 조건이 왜 실패했는지 정확히 파악할 수 있습니다.

정리

  • 조건부 어노테이션 은 Spring Boot 자동 설정의 핵심입니다. 클래스패스, 빈, 프로퍼티 등의 조건을 평가하여 설정 적용 여부를 결정합니다.
  • @ConditionalOnClass는 의존성 존재 여부를, @ConditionalOnMissingBean은 사용자 정의 빈 여부를 확인합니다.
  • @ConditionalOnProperty로 프로퍼티 기반 기능 토글을 구현할 수 있습니다.
  • --debug 플래그로 CONDITIONS EVALUATION REPORT 를 확인하여 어떤 자동 설정이 왜 적용되었는지 디버깅합니다.
  • Condition 인터페이스를 구현하여 커스텀 조건부 어노테이션 을 만들 수 있습니다.
댓글 로딩 중...