@Condition, @Conditional 그리고 Fallback 패턴

JY·2025년 11월 3일

Spring

목록 보기
7/7
post-thumbnail

🧩 문제의 시작 — 환경에 따라 깨지는 Bean 주입

“Ansible이 설치되지 않은 환경에서 앱이 바로 죽는다.”

프로젝트를 진행하던 중 AnsibleProperties라는 설정 Bean이 여러 서비스에 주입되어 있었다.
문제는 Ansible이 설치되어 있지 않거나 설정 값이 비어 있을 경우, Spring이 애플리케이션을 띄우는 도중 컨텍스트 로딩 에러를 발생시키며 바로 종료된다는 점이었다.

Spring 프로젝트를 하다 보면 .properties, .yml, .env 등의 설정 파일에 환경별 민감 정보나 외부 연결 값을 저장하는 경우가 많다.
이러한 설정 주입은 배포 환경이나 운영 환경이 달라질 때 필수적이지만, 특정 환경에서 값이 누락되면 예상치 못한 런타임 오류로 이어질 수 있다.
특히 MSA 구조에서는 하나의 설정 주입이 실패하면 그 설정을 의존하는 모든 서비스가 연쇄적으로 장애를 일으킬 수도 있다.

이 문제는 설정 파일에만 국한되지 않는다. 예를 들어,

  • 외부 API Key가 없는 경우
  • S3 / Redis / Kafka / Elasticsearch 등의 연결 정보가 비어 있는 경우
  • 사내망 전용 모듈을 로컬 환경에서 띄우는 경우

이런 상황에서도 동일한 문제가 반복된다.

결국 “내가 필요한 기능은 일부일 뿐인데, 하나의 Bean이 전체 서비스를 멈추게 된다.”
이것이 바로 환경 의존적인 Bean 주입 구조의 대표적인 함정이다.


⚙️ 원인 — Spring의 기본 Bean 로딩 메커니즘

Spring은 @Component, @Service, @ConfigurationProperties 등 애노테이션이 붙은 클래스라면
조건과 상관없이 무조건 Bean으로 등록한다.

그런데 이 과정에서 바인딩할 설정 값이 없거나, 초기화 시 외부 시스템에 접근하는 코드가 있다면?
➡️ 컨텍스트 초기화 단계에서 예외 발생앱 전체 기동 실패

이는 “Fail-fast” 구조로 빠른 에러 감지는 가능하지만,
모듈성이 중요한 시스템에서는 “필요 없는 환경에서까지 Bean을 강제 등록”해버리는 문제가 된다.


🧩 해결책 1 — @Conditional & @Condition

“필요할 때만 Bean을 등록하자.”

Spring은 @Conditional 애노테이션을 통해 Bean 등록 여부를 제어할 수 있다.
직접 Condition 인터페이스를 구현하면 된다.

public class ApiKeyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
        String key = ctx.getEnvironment().getProperty("external.api.key");
        return key != null && !key.isBlank();
    }
}
@Bean
@Conditional(ApiKeyCondition.class)
public ExternalApiClient realClient() {
    return new RealExternalApiClient();
}

이제 external.api.key가 존재할 때만 Bean이 등록된다.
없다면 Bean 자체가 생성되지 않는다.


🧩 해결책 2 — Fallback 패턴 (대체 전략)

“Real이 없을 땐 안전한 No-Op로 대체하자.”

@Configuration
public class ApiClientConfig {

    @Bean
    @Conditional(ApiKeyCondition.class)
    public ExternalApiClient realClient() {
        return new RealExternalApiClient();
    }

    @Bean
    @ConditionalOnMissingBean(ExternalApiClient.class)
    @Primary
    public ExternalApiClient noOpClient() {
        return () -> log.info("API Key 미설정 → 요청 스킵");
    }
}

주입받는 쪽에서는 전혀 신경 쓸 필요가 없다.

@Service
@RequiredArgsConstructor
public class ReportService {
    private final ExternalApiClient apiClient;

    public void sendReport() {
        apiClient.send(); // Real 또는 No-Op 중 하나가 자동 실행
    }
}

환경에 따라 실제 호출이든, 아무 동작도 안 하든,
애플리케이션은 절대 멈추지 않는다.


🧠 Fallback은 “대체 Bean”만이 아니다

실무에서는 Fallback을 다양한 복원 전략으로 확장할 수 있다.

유형설명예시
🧩 No-Op아무 동작도 안 함, Null-safeAnsible 미설치 시 스킵
⏱️ Timeout일정 시간 초과 시 기본 응답 반환Resilience4j, Hystrix
💾 Cache / Graceful Degrade외부 시스템 불가 시 캐시 데이터 반환Redis → InMemory
🧪 Stub / Mock개발 환경에서 실제 호출 대신 모의 응답Dev용 Mock API
🔁 HybridNo-Op → Cache → Queue 순서로 점진적 복구장애 복원형 설계

Fallback의 핵심은 “기능 축소는 허용하되, 시스템 중단은 금지”다.
Fail-fast보다 Fail-safe한 시스템을 지향해야 한다.


✅ 환경이 달라져도 시스템은 살아 있어야 한다

Spring의 @Conditional은 “언제 Bean을 등록할지”를 결정하고,
Fallback 패턴은 “등록되지 않았을 때 어떻게 대응할지”를 책임진다.

Ansible처럼 환경 의존성이 강한 시스템은 물론,
외부 API, 클라우드, 캐시, 메시징 등 다양한 영역에서도
이 두 가지를 조합하면 죽지 않는 애플리케이션을 만들 수 있다.

장애는 피할 수 없지만, 죽지 않는 구조는 설계할 수 있다.

0개의 댓글