Skip to content

Commit 3174797

Browse files
committed
test: Kafka 이벤트 파이프라인 테스트 추가
- KafkaConnectionTest: Kafka 연결 테스트 - MetricsAggregationServiceIdempotencyTest: 멱등성 검증 - DlqPublisherTest: DLQ 발행 테스트 - RetryTrackerTest: 재시도 추적 테스트 - 테스트 설정 파일 추가
1 parent 092eb59 commit 3174797

File tree

6 files changed

+593
-0
lines changed

6 files changed

+593
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.loopers.infrastructure.kafka;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.kafka.core.KafkaTemplate;
8+
import org.springframework.kafka.support.SendResult;
9+
import org.springframework.test.context.ActiveProfiles;
10+
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.concurrent.TimeUnit;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
@SpringBootTest(
17+
classes = {com.loopers.confg.kafka.KafkaConfig.class},
18+
properties = {
19+
"spring.kafka.bootstrap-servers=localhost:19092",
20+
"spring.kafka.producer.acks=all",
21+
"spring.kafka.producer.properties.enable.idempotence=true",
22+
"spring.kafka.producer.retries=3",
23+
"spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer",
24+
"spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer"
25+
}
26+
)
27+
@ActiveProfiles("test")
28+
class KafkaConnectionTest {
29+
30+
@Autowired
31+
private KafkaTemplate<String, Object> kafkaTemplate;
32+
33+
@Test
34+
@DisplayName("Kafka Producer 연결 테스트")
35+
void kafkaProducerConnectionTest() throws Exception {
36+
// given
37+
String topic = "demo.internal.topic-v1";
38+
String key = "test-key";
39+
String message = "Hello Kafka!";
40+
41+
// when
42+
CompletableFuture<SendResult<String, Object>> future =
43+
kafkaTemplate.send(topic, key, message);
44+
45+
// then
46+
SendResult<String, Object> result = future.get(10, TimeUnit.SECONDS);
47+
assertThat(result).isNotNull();
48+
assertThat(result.getRecordMetadata().topic()).isEqualTo(topic);
49+
50+
System.out.println("✅ Kafka Producer 연결 성공!");
51+
System.out.println("Topic: " + result.getRecordMetadata().topic());
52+
System.out.println("Partition: " + result.getRecordMetadata().partition());
53+
System.out.println("Offset: " + result.getRecordMetadata().offset());
54+
}
55+
56+
@Test
57+
@DisplayName("Kafka Producer acks=all 설정 확인")
58+
void kafkaProducerAcksConfigTest() {
59+
// given
60+
var producerFactory = kafkaTemplate.getProducerFactory();
61+
var configs = producerFactory.getConfigurationProperties();
62+
63+
// then
64+
assertThat(configs.get("acks")).isEqualTo("all");
65+
assertThat(configs.get("enable.idempotence")).isEqualTo(true);
66+
67+
System.out.println("✅ Producer 설정 확인:");
68+
System.out.println("acks: " + configs.get("acks"));
69+
System.out.println("enable.idempotence: " + configs.get("enable.idempotence"));
70+
System.out.println("retries: " + configs.get("retries"));
71+
}
72+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package com.loopers.application.metrics;
2+
3+
import com.loopers.application.event.like.ProductLikedEvent;
4+
import com.loopers.application.event.like.ProductUnlikedEvent;
5+
import com.loopers.application.event.order.OrderCompletedEvent;
6+
import com.loopers.application.event.product.ProductViewedEvent;
7+
import com.loopers.config.TestConfig;
8+
import com.loopers.domain.event.EventHandledRepository;
9+
import com.loopers.domain.metrics.ProductMetrics;
10+
import com.loopers.domain.metrics.ProductMetricsRepository;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
16+
import org.springframework.context.annotation.Import;
17+
import org.springframework.test.context.ActiveProfiles;
18+
19+
import java.math.BigDecimal;
20+
import java.time.LocalDateTime;
21+
import java.util.List;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* 메트릭 집계 서비스 멱등성 테스트
27+
* - 중복 이벤트 처리 방지 검증
28+
* - event_handled 테이블 기반 멱등성 보장
29+
*/
30+
@DataJpaTest
31+
@Import({MetricsAggregationService.class})
32+
@ActiveProfiles("test")
33+
class MetricsAggregationServiceIdempotencyTest {
34+
35+
@Autowired
36+
private MetricsAggregationService aggregationService;
37+
38+
@Autowired
39+
private ProductMetricsRepository metricsRepository;
40+
41+
@Autowired
42+
private EventHandledRepository eventHandledRepository;
43+
44+
private static final Long PRODUCT_ID = 100L;
45+
private static final Long MEMBER_ID = 1L;
46+
private static final Long BRAND_ID = 10L;
47+
48+
@BeforeEach
49+
void setUp() {
50+
// 테스트용 ProductMetrics 초기화
51+
ProductMetrics metrics = ProductMetrics.create(PRODUCT_ID);
52+
metricsRepository.save(metrics);
53+
}
54+
55+
@Test
56+
@DisplayName("좋아요 이벤트 중복 처리 방지 - 동일한 eventId로 두 번 호출 시 한 번만 처리됨")
57+
void shouldIgnoreDuplicateProductLikedEvent() {
58+
// given
59+
String eventId = "event-liked-001";
60+
ProductLikedEvent event = new ProductLikedEvent(
61+
MEMBER_ID,
62+
PRODUCT_ID,
63+
BRAND_ID,
64+
LocalDateTime.now()
65+
);
66+
67+
// when - 첫 번째 이벤트 처리
68+
aggregationService.handleProductLiked(eventId, event);
69+
70+
// then - 좋아요 수가 1로 증가
71+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
72+
assertThat(metrics.getLikeCount()).isEqualTo(1);
73+
assertThat(eventHandledRepository.existsById(eventId)).isTrue();
74+
75+
// when - 동일한 eventId로 두 번째 이벤트 처리 (중복)
76+
aggregationService.handleProductLiked(eventId, event);
77+
78+
// then - 좋아요 수가 여전히 1 (증가하지 않음)
79+
metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
80+
assertThat(metrics.getLikeCount()).isEqualTo(1);
81+
}
82+
83+
@Test
84+
@DisplayName("좋아요 취소 이벤트 중복 처리 방지")
85+
void shouldIgnoreDuplicateProductUnlikedEvent() {
86+
// given - 좋아요 수를 먼저 1로 설정
87+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
88+
metrics.incrementLikeCount(LocalDateTime.now().atZone(java.time.ZoneId.of("Asia/Seoul")));
89+
metricsRepository.save(metrics);
90+
91+
String eventId = "event-unliked-001";
92+
ProductUnlikedEvent event = new ProductUnlikedEvent(
93+
MEMBER_ID,
94+
PRODUCT_ID,
95+
BRAND_ID,
96+
LocalDateTime.now()
97+
);
98+
99+
// when - 첫 번째 이벤트 처리
100+
aggregationService.handleProductUnliked(eventId, event);
101+
102+
// then - 좋아요 수가 0으로 감소
103+
metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
104+
assertThat(metrics.getLikeCount()).isEqualTo(0);
105+
assertThat(eventHandledRepository.existsById(eventId)).isTrue();
106+
107+
// when - 동일한 eventId로 두 번째 이벤트 처리 (중복)
108+
aggregationService.handleProductUnliked(eventId, event);
109+
110+
// then - 좋아요 수가 여전히 0 (감소하지 않음)
111+
metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
112+
assertThat(metrics.getLikeCount()).isEqualTo(0);
113+
}
114+
115+
@Test
116+
@DisplayName("주문 완료 이벤트 중복 처리 방지")
117+
void shouldIgnoreDuplicateOrderCompletedEvent() {
118+
// given
119+
String eventId = "event-order-001";
120+
OrderCompletedEvent event = new OrderCompletedEvent(
121+
"ORDER-001",
122+
MEMBER_ID,
123+
BigDecimal.valueOf(50000),
124+
List.of(
125+
new OrderCompletedEvent.OrderItemInfo(
126+
PRODUCT_ID,
127+
2,
128+
BigDecimal.valueOf(25000)
129+
)
130+
),
131+
LocalDateTime.now()
132+
);
133+
134+
// when - 첫 번째 이벤트 처리
135+
aggregationService.handleOrderCompleted(eventId, event);
136+
137+
// then - 판매량 증가
138+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
139+
assertThat(metrics.getSalesCount()).isEqualTo(2);
140+
assertThat(metrics.getSalesAmount()).isEqualTo(50000);
141+
assertThat(eventHandledRepository.existsById(eventId)).isTrue();
142+
143+
// when - 동일한 eventId로 두 번째 이벤트 처리 (중복)
144+
aggregationService.handleOrderCompleted(eventId, event);
145+
146+
// then - 판매량이 여전히 2 (증가하지 않음)
147+
metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
148+
assertThat(metrics.getSalesCount()).isEqualTo(2);
149+
assertThat(metrics.getSalesAmount()).isEqualTo(50000);
150+
}
151+
152+
@Test
153+
@DisplayName("서로 다른 eventId는 각각 처리됨")
154+
void shouldProcessDifferentEventIds() {
155+
// given
156+
String eventId1 = "event-liked-001";
157+
String eventId2 = "event-liked-002";
158+
ProductLikedEvent event1 = new ProductLikedEvent(
159+
MEMBER_ID,
160+
PRODUCT_ID,
161+
BRAND_ID,
162+
LocalDateTime.now()
163+
);
164+
ProductLikedEvent event2 = new ProductLikedEvent(
165+
MEMBER_ID + 1,
166+
PRODUCT_ID,
167+
BRAND_ID,
168+
LocalDateTime.now()
169+
);
170+
171+
// when - 서로 다른 eventId로 두 번 호출
172+
aggregationService.handleProductLiked(eventId1, event1);
173+
aggregationService.handleProductLiked(eventId2, event2);
174+
175+
// then - 좋아요 수가 2로 증가
176+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
177+
assertThat(metrics.getLikeCount()).isEqualTo(2);
178+
assertThat(eventHandledRepository.existsById(eventId1)).isTrue();
179+
assertThat(eventHandledRepository.existsById(eventId2)).isTrue();
180+
}
181+
182+
@Test
183+
@DisplayName("상품 조회 이벤트 중복 처리 방지 - 동일한 eventId로 두 번 호출 시 한 번만 처리됨")
184+
void shouldIgnoreDuplicateProductViewedEvent() {
185+
// given
186+
String eventId = "event-viewed-001";
187+
ProductViewedEvent event = new ProductViewedEvent(
188+
MEMBER_ID,
189+
PRODUCT_ID,
190+
BRAND_ID,
191+
LocalDateTime.now()
192+
);
193+
194+
// when - 첫 번째 이벤트 처리
195+
aggregationService.handleProductViewed(eventId, event);
196+
197+
// then - 조회 수가 1로 증가
198+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
199+
assertThat(metrics.getViewCount()).isEqualTo(1);
200+
assertThat(eventHandledRepository.existsById(eventId)).isTrue();
201+
202+
// when - 동일한 eventId로 두 번째 이벤트 처리 (중복)
203+
aggregationService.handleProductViewed(eventId, event);
204+
205+
// then - 조회 수가 여전히 1 (증가하지 않음)
206+
metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
207+
assertThat(metrics.getViewCount()).isEqualTo(1);
208+
}
209+
210+
@Test
211+
@DisplayName("비로그인 사용자 조회도 집계됨")
212+
void shouldAggregateViewFromAnonymousUser() {
213+
// given
214+
String eventId = "event-viewed-002";
215+
ProductViewedEvent event = new ProductViewedEvent(
216+
null, // 비로그인 사용자
217+
PRODUCT_ID,
218+
BRAND_ID,
219+
LocalDateTime.now()
220+
);
221+
222+
// when
223+
aggregationService.handleProductViewed(eventId, event);
224+
225+
// then
226+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
227+
assertThat(metrics.getViewCount()).isEqualTo(1);
228+
}
229+
230+
@Test
231+
@DisplayName("여러 조회 이벤트가 누적됨")
232+
void shouldAccumulateMultipleViewEvents() {
233+
// given
234+
String eventId1 = "event-viewed-003";
235+
String eventId2 = "event-viewed-004";
236+
String eventId3 = "event-viewed-005";
237+
238+
ProductViewedEvent event1 = new ProductViewedEvent(MEMBER_ID, PRODUCT_ID, BRAND_ID, LocalDateTime.now());
239+
ProductViewedEvent event2 = new ProductViewedEvent(MEMBER_ID + 1, PRODUCT_ID, BRAND_ID, LocalDateTime.now());
240+
ProductViewedEvent event3 = new ProductViewedEvent(null, PRODUCT_ID, BRAND_ID, LocalDateTime.now());
241+
242+
// when
243+
aggregationService.handleProductViewed(eventId1, event1);
244+
aggregationService.handleProductViewed(eventId2, event2);
245+
aggregationService.handleProductViewed(eventId3, event3);
246+
247+
// then
248+
ProductMetrics metrics = metricsRepository.findById(PRODUCT_ID).orElseThrow();
249+
assertThat(metrics.getViewCount()).isEqualTo(3);
250+
}
251+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.loopers.config;
2+
3+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4+
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
5+
import org.springframework.boot.test.context.TestConfiguration;
6+
import org.springframework.context.annotation.ComponentScan;
7+
8+
/**
9+
* 테스트 환경 설정
10+
* - Kafka 자동 설정 비활성화
11+
* - JPA, Redis만 활성화
12+
*/
13+
@TestConfiguration
14+
@EnableAutoConfiguration(exclude = {KafkaAutoConfiguration.class})
15+
@ComponentScan(basePackages = "com.loopers")
16+
public class TestConfig {
17+
}

0 commit comments

Comments
 (0)