diff --git a/src/main/java/com/beachcheck/outbox/config/OutboxFirebaseConfig.java b/src/main/java/com/beachcheck/outbox/config/OutboxFirebaseConfig.java
index e4f59fd3..d5c81d5d 100644
--- a/src/main/java/com/beachcheck/outbox/config/OutboxFirebaseConfig.java
+++ b/src/main/java/com/beachcheck/outbox/config/OutboxFirebaseConfig.java
@@ -7,22 +7,26 @@
import com.beachcheck.outbox.tracing.OutboxTracing;
import com.google.firebase.messaging.FirebaseMessaging;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
- * Why: FirebaseMessaging 빈이 있을 때만 Outbox 전송 관련 빈을 한곳에서 조건부 등록하기 위해.
+ * Why: Firebase가 활성화됐을 때만 Outbox 전송 관련 빈을 한곳에서 조건부 등록하기 위해.
*
*
Policy:
*
*
- * - FirebaseMessaging 빈이 없으면 dispatcher/publisher를 등록하지 않는다.
+ *
- app.firebase.enabled=false면 dispatcher/publisher를 등록하지 않는다.
*
- 조건부 생성 정책은 서비스 클래스가 아니라 configuration 레이어에서 관리한다.
*
*/
@Configuration(proxyBeanMethods = false)
-@ConditionalOnBean(FirebaseMessaging.class)
+@ConditionalOnProperty(
+ prefix = "app.firebase",
+ name = "enabled",
+ havingValue = "true",
+ matchIfMissing = false)
public class OutboxFirebaseConfig {
@Bean
diff --git a/src/main/java/com/beachcheck/outbox/config/OutboxSchedulingConfig.java b/src/main/java/com/beachcheck/outbox/config/OutboxSchedulingConfig.java
index 0770c4fd..1058b210 100644
--- a/src/main/java/com/beachcheck/outbox/config/OutboxSchedulingConfig.java
+++ b/src/main/java/com/beachcheck/outbox/config/OutboxSchedulingConfig.java
@@ -3,7 +3,6 @@
import com.beachcheck.outbox.service.OutboxPublisher;
import java.util.UUID;
import org.slf4j.MDC;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
@@ -15,15 +14,14 @@
* Policy:
*
*
- * - app.outbox.polling.enabled=true 일 때만 빈 등록
+ *
- app.firebase.enabled=true이고 app.outbox.polling.enabled=true일 때만 빈 등록
*
- fixedDelay: app.outbox.polling.fixed-delay 값 사용 (기본 1초)
*
*/
@Configuration
-@ConditionalOnBean(OutboxPublisher.class)
@ConditionalOnProperty(
- prefix = "app.outbox.polling",
- name = "enabled",
+ prefix = "app",
+ name = {"firebase.enabled", "outbox.polling.enabled"},
havingValue = "true",
matchIfMissing = false)
public class OutboxSchedulingConfig {
diff --git a/src/test/java/com/beachcheck/outbox/config/OutboxFirebaseConditionContextTest.java b/src/test/java/com/beachcheck/outbox/config/OutboxFirebaseConditionContextTest.java
index 1f1193d2..4589dc38 100644
--- a/src/test/java/com/beachcheck/outbox/config/OutboxFirebaseConditionContextTest.java
+++ b/src/test/java/com/beachcheck/outbox/config/OutboxFirebaseConditionContextTest.java
@@ -1,16 +1,24 @@
package com.beachcheck.outbox.config;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import com.beachcheck.global.config.FirebaseConfig;
import com.beachcheck.notification.repository.NotificationRepository;
import com.beachcheck.outbox.repository.OutboxEventRepository;
import com.beachcheck.outbox.service.OutboxEventDispatcher;
import com.beachcheck.outbox.service.OutboxPublisher;
import com.beachcheck.outbox.tracing.OutboxTracing;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
+import java.io.InputStream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -20,28 +28,32 @@ class OutboxFirebaseConditionContextTest {
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
- .withUserConfiguration(OutboxBeansConfig.class)
.withPropertyValues("app.outbox.polling.enabled=true", "app.outbox.polling.batch-size=10")
.withBean(OutboxEventRepository.class, () -> mock(OutboxEventRepository.class))
.withBean(NotificationRepository.class, () -> mock(NotificationRepository.class))
.withBean(OutboxTracing.class, () -> mock(OutboxTracing.class));
@Test
- @DisplayName("FirebaseMessaging 빈이 없으면 Outbox 관련 빈 없이도 컨텍스트가 정상 기동된다")
- void whenFirebaseMessagingMissing_thenContextLoadsWithoutOutboxBeans() {
- contextRunner.run(
- context -> {
- assertThat(context).hasNotFailed();
- assertThat(context).doesNotHaveBean(OutboxEventDispatcher.class);
- assertThat(context).doesNotHaveBean(OutboxPublisher.class);
- assertThat(context).doesNotHaveBean(OutboxSchedulingConfig.class);
- });
+ @DisplayName("Firebase가 비활성화되면 Outbox 관련 빈 없이도 컨텍스트가 정상 기동된다")
+ void whenFirebaseDisabled_thenContextLoadsWithoutOutboxBeans() {
+ contextRunner
+ .withUserConfiguration(OutboxBeansConfig.class)
+ .withPropertyValues("app.firebase.enabled=false")
+ .run(
+ context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).doesNotHaveBean(OutboxEventDispatcher.class);
+ assertThat(context).doesNotHaveBean(OutboxPublisher.class);
+ assertThat(context).doesNotHaveBean(OutboxSchedulingConfig.class);
+ });
}
@Test
- @DisplayName("FirebaseMessaging 빈이 있으면 Outbox 관련 빈을 함께 등록한다")
- void whenFirebaseMessagingPresent_thenRegisterOutboxBeans() {
+ @DisplayName("Firebase가 활성화되고 FirebaseMessaging 의존성이 있으면 Outbox 관련 빈을 등록한다")
+ void whenFirebaseEnabledWithMessaging_thenRegisterOutboxBeans() {
contextRunner
+ .withUserConfiguration(OutboxBeansConfig.class)
+ .withPropertyValues("app.firebase.enabled=true")
.withBean(FirebaseMessaging.class, () -> mock(FirebaseMessaging.class))
.run(
context -> {
@@ -52,7 +64,65 @@ void whenFirebaseMessagingPresent_thenRegisterOutboxBeans() {
});
}
+ @Test
+ @DisplayName("Outbox 폴링이 비활성화되면 전송 빈은 유지하고 스케줄러만 등록하지 않는다")
+ void whenOutboxPollingDisabled_thenRegisterDispatchBeansWithoutScheduler() {
+ contextRunner
+ .withUserConfiguration(OutboxBeansConfig.class)
+ .withPropertyValues("app.firebase.enabled=true", "app.outbox.polling.enabled=false")
+ .withBean(FirebaseMessaging.class, () -> mock(FirebaseMessaging.class))
+ .run(
+ context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).hasSingleBean(OutboxEventDispatcher.class);
+ assertThat(context).hasSingleBean(OutboxPublisher.class);
+ assertThat(context).doesNotHaveBean(OutboxSchedulingConfig.class);
+ });
+ }
+
+ @Test
+ @DisplayName("운영 Firebase와 Outbox 설정을 함께 로드하면 처리 순서와 관계없이 관련 빈을 등록한다")
+ void whenProductionConfigurationsLoad_thenRegisterFirebaseAndOutboxBeans() {
+ FirebaseApp firebaseApp = mock(FirebaseApp.class);
+ FirebaseMessaging firebaseMessaging = mock(FirebaseMessaging.class);
+
+ try (MockedStatic googleCredentials = mockStatic(GoogleCredentials.class);
+ MockedStatic firebaseApps = mockStatic(FirebaseApp.class);
+ MockedStatic firebaseMessagingInstances =
+ mockStatic(FirebaseMessaging.class)) {
+ googleCredentials
+ .when(() -> GoogleCredentials.fromStream(any(InputStream.class)))
+ .thenReturn(mock(GoogleCredentials.class));
+ firebaseApps.when(FirebaseApp::getApps).thenReturn(java.util.List.of());
+ firebaseApps
+ .when(() -> FirebaseApp.initializeApp(any(FirebaseOptions.class)))
+ .thenReturn(firebaseApp);
+ firebaseMessagingInstances
+ .when(() -> FirebaseMessaging.getInstance(firebaseApp))
+ .thenReturn(firebaseMessaging);
+
+ contextRunner
+ .withInitializer(context -> context.getEnvironment().setActiveProfiles("test"))
+ .withUserConfiguration(ProductionBeansConfig.class)
+ .withPropertyValues(
+ "app.firebase.enabled=true", "app.firebase.credentials-json-base64=e30=")
+ .run(
+ context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).hasSingleBean(FirebaseApp.class);
+ assertThat(context).hasSingleBean(FirebaseMessaging.class);
+ assertThat(context).hasSingleBean(OutboxEventDispatcher.class);
+ assertThat(context).hasSingleBean(OutboxPublisher.class);
+ assertThat(context).hasSingleBean(OutboxSchedulingConfig.class);
+ });
+ }
+ }
+
@Configuration(proxyBeanMethods = false)
- @Import({OutboxFirebaseConfig.class, OutboxSchedulingConfig.class})
+ @Import({OutboxSchedulingConfig.class, OutboxFirebaseConfig.class})
static class OutboxBeansConfig {}
+
+ @Configuration(proxyBeanMethods = false)
+ @Import({OutboxSchedulingConfig.class, OutboxFirebaseConfig.class, FirebaseConfig.class})
+ static class ProductionBeansConfig {}
}
diff --git a/src/test/java/com/beachcheck/outbox/integration/OutboxPublisherIntegrationTest.java b/src/test/java/com/beachcheck/outbox/integration/OutboxPublisherIntegrationTest.java
index 631c6b33..d7202956 100644
--- a/src/test/java/com/beachcheck/outbox/integration/OutboxPublisherIntegrationTest.java
+++ b/src/test/java/com/beachcheck/outbox/integration/OutboxPublisherIntegrationTest.java
@@ -19,7 +19,9 @@
import com.beachcheck.notification.repository.NotificationRepository;
import com.beachcheck.outbox.domain.OutboxEvent;
import com.beachcheck.outbox.repository.OutboxEventRepository;
+import com.beachcheck.outbox.service.OutboxEventDispatcher;
import com.beachcheck.outbox.service.OutboxPublisher;
+import com.beachcheck.outbox.tracing.OutboxTracing;
import com.beachcheck.support.base.IntegrationTest;
import com.beachcheck.support.tracing.RecordingSpanExporter;
import com.beachcheck.support.tracing.TracingTestConfiguration;
@@ -39,6 +41,9 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Propagation;
@@ -62,7 +67,7 @@
Propagation
.NOT_SUPPORTED) // OutboxPublisher의 REQUIRES_NEW 트랜잭션과 격리하여 테스트 간 트랜잭션 롤백이 영향을 주지 않도록 함.
// 대신 @BeforeEach에서 deleteAll()로 DB 상태 초기화
-@Import(TracingTestConfiguration.class)
+@Import({TracingTestConfiguration.class, OutboxPublisherIntegrationTest.OutboxTestConfig.class})
@TestPropertySource(properties = "management.tracing.sampling.probability=1.0")
class OutboxPublisherIntegrationTest extends IntegrationTest {
@@ -85,6 +90,29 @@ class OutboxPublisherIntegrationTest extends IntegrationTest {
@Autowired private SdkTracerProvider tracerProvider;
+ @TestConfiguration(proxyBeanMethods = false)
+ static class OutboxTestConfig {
+
+ @Bean
+ OutboxEventDispatcher outboxEventDispatcher(
+ OutboxEventRepository outboxEventRepository,
+ NotificationRepository notificationRepository,
+ FirebaseMessaging firebaseMessaging) {
+ return new OutboxEventDispatcher(
+ outboxEventRepository, notificationRepository, firebaseMessaging);
+ }
+
+ @Bean
+ OutboxPublisher outboxPublisher(
+ OutboxEventRepository outboxEventRepository,
+ OutboxEventDispatcher outboxEventDispatcher,
+ OutboxTracing outboxTracing,
+ @Value("${app.outbox.polling.batch-size:10}") int batchSize) {
+ return new OutboxPublisher(
+ outboxEventRepository, outboxEventDispatcher, outboxTracing, batchSize);
+ }
+ }
+
@BeforeEach
void setUp() throws FirebaseMessagingException {
flushSpans(tracerProvider);
diff --git a/src/test/java/com/beachcheck/support/config/FirebaseTestConfig.java b/src/test/java/com/beachcheck/support/config/FirebaseTestConfig.java
index 6725ca85..f856bc76 100644
--- a/src/test/java/com/beachcheck/support/config/FirebaseTestConfig.java
+++ b/src/test/java/com/beachcheck/support/config/FirebaseTestConfig.java
@@ -16,8 +16,8 @@
*
*
* - @TestConfiguration으로 테스트 환경에만 적용
- *
- @Primary로 FirebaseConfig의 실제 빈보다 우선순위 높게 설정 (실제 FirebaseConfig는 test 환경에서 비활성화되어 있지만 명시적
- * 우선순위 지정)
+ *
- 기본 test 프로필의 app.firebase.enabled=false로 실제 FirebaseConfig를 비활성화하고 Mock 빈을 사용
+ *
- @Primary로 테스트용 FirebaseMessaging 의존성을 명확하게 선택
*
- ApiTest, IntegrationTest 베이스 클래스에 @Import로 자동 적용
*
*
@@ -36,9 +36,8 @@ public class FirebaseTestConfig {
*
*
* - Mockito.mock()으로 Mock 객체 생성
- *
- @Primary로 실제 FirebaseConfig보다 우선순위 높게 설정 (test 환경에서 FirebaseConfig는 비활성화되어 있지만 명시적 우선순위
- * 지정)
- *
- 개별 테스트에서 @MockBean으로 추가 설정 가능 (예: OutboxPublisherIntegrationTest)
+ *
- 기본 test 프로필에서는 app.firebase.enabled=false이므로 실제 FirebaseConfig를 등록하지 않음
+ *
- 개별 테스트에서 반환값과 예외 등 Mock 동작을 추가 설정 가능
*
*/
@Bean
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 9aed543d..15f47825 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -70,7 +70,9 @@ app:
outbox:
polling:
- enabled: false # Outbox 폴링 스케줄러 비활성화 (수동 호출로 테스트)
+ # @SpringBootTest 기반 Outbox 통합 테스트에서 처리 시점을 직접 제어하기 위해 스케줄러 자동 실행을 비활성화한다.
+ # Outbox 통합 테스트는 FirebaseMessaging Mock을 사용하고 OutboxPublisher를 직접 호출한다.
+ enabled: false
logging:
level: