Skip to content

Commit 0f8f193

Browse files
committed
test: 이벤트 기반 아키텍처 통합 테스트 추가
Phase 1 - 핵심 플로우 검증: OrderPaymentEventIntegrationTest (4개 테스트): - 결제 성공 시 주문 완료 플로우 - 결제 실패 시 주문 취소 및 재고 복구 - 중복 이벤트 발행 시 멱등성 보장 - 비동기 이벤트 처리 후 트랜잭션 커밋 확인 LikeEventIntegrationTest (5개 테스트): - 좋아요 생성 시 Redis 캐시 업데이트 - 좋아요 취소 시 Redis 캐시 업데이트 - 여러 회원 좋아요 시 Redis 카운트 정확성 - Redis-DB 간 최종 일관성 검증 - 중복 좋아요 시 멱등성 보장 테스트 유틸리티: - AsyncTestHelper: 비동기 작업 완료 대기 헬퍼 - RedisTestHelper: Redis 데이터 정리 및 연결 확인 의존성: - awaitility 4.2.0 추가 (비동기 테스트용)
1 parent b028cdf commit 0f8f193

5 files changed

Lines changed: 658 additions & 0 deletions

File tree

apps/commerce-api/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ dependencies {
1717
implementation("io.github.resilience4j:resilience4j-micrometer:2.2.0")
1818
implementation("org.springframework.boot:spring-boot-starter-aop")
1919

20+
// spring retry
21+
implementation("org.springframework.retry:spring-retry")
22+
implementation("org.springframework:spring-aspects")
23+
2024
// querydsl
2125
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
2226
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
@@ -25,4 +29,7 @@ dependencies {
2529
// test-fixtures
2630
testImplementation(testFixtures(project(":modules:jpa")))
2731
testImplementation(testFixtures(project(":modules:redis")))
32+
33+
// awaitility for async testing
34+
testImplementation("org.awaitility:awaitility:4.2.0")
2835
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package com.loopers.application.event.integration;
2+
3+
import com.loopers.application.event.like.ProductLikedEvent;
4+
import com.loopers.application.event.like.ProductUnlikedEvent;
5+
import com.loopers.domain.common.vo.Money;
6+
import com.loopers.domain.like.Like;
7+
import com.loopers.domain.like.repository.LikeRepository;
8+
import com.loopers.domain.like.service.LikeService;
9+
import com.loopers.domain.members.Member;
10+
import com.loopers.domain.members.enums.Gender;
11+
import com.loopers.domain.members.repository.MemberRepository;
12+
import com.loopers.domain.product.Product;
13+
import com.loopers.domain.product.repository.ProductRepository;
14+
import com.loopers.domain.product.vo.Stock;
15+
import com.loopers.infrastructure.cache.MemberLikesCache;
16+
import com.loopers.infrastructure.cache.ProductLikeCountCache;
17+
import com.loopers.utils.DatabaseCleanUp;
18+
import jakarta.persistence.EntityManager;
19+
import org.junit.jupiter.api.AfterEach;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.DisplayName;
22+
import org.junit.jupiter.api.Test;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.boot.test.context.SpringBootTest;
25+
import org.springframework.data.redis.core.RedisTemplate;
26+
import org.springframework.test.context.event.ApplicationEvents;
27+
import org.springframework.test.context.event.RecordApplicationEvents;
28+
29+
import java.util.concurrent.TimeUnit;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.awaitility.Awaitility.await;
33+
34+
/**
35+
* 좋아요 이벤트 플로우 통합 테스트
36+
*
37+
* 검증 항목:
38+
* 1. 좋아요 생성 시 이벤트 발행 및 Redis 캐시 업데이트
39+
* 2. 좋아요 취소 시 이벤트 발행 및 Redis 캐시 업데이트
40+
* 3. Redis와 DB의 최종 일관성 (eventual consistency)
41+
* 4. 여러 좋아요/취소 작업 후 데이터 일치 확인
42+
*/
43+
@SpringBootTest
44+
@RecordApplicationEvents
45+
@DisplayName("좋아요 이벤트 통합 테스트")
46+
class LikeEventIntegrationTest {
47+
48+
@Autowired
49+
private LikeService likeService;
50+
51+
@Autowired
52+
private LikeRepository likeRepository;
53+
54+
@Autowired
55+
private ProductRepository productRepository;
56+
57+
@Autowired
58+
private MemberRepository memberRepository;
59+
60+
@Autowired
61+
private ProductLikeCountCache productLikeCountCache;
62+
63+
@Autowired
64+
private MemberLikesCache memberLikesCache;
65+
66+
@Autowired
67+
private RedisTemplate<String, String> redisTemplate;
68+
69+
@Autowired
70+
private ApplicationEvents applicationEvents;
71+
72+
@Autowired
73+
private EntityManager entityManager;
74+
75+
@Autowired
76+
private DatabaseCleanUp databaseCleanUp;
77+
78+
private Member testMember;
79+
private Product testProduct;
80+
81+
@BeforeEach
82+
void setUp() {
83+
// Redis 초기화
84+
redisTemplate.getConnectionFactory().getConnection().flushDb();
85+
86+
// 테스트 회원 생성
87+
testMember = new Member("like-user", "like@example.com", "password123", "1995-05-05", Gender.FEMALE);
88+
testMember = memberRepository.save(testMember);
89+
90+
// 테스트 상품 생성
91+
testProduct = new Product(10L, "인기 상품", "좋아요 테스트용", Money.of(50000), Stock.of(50));
92+
testProduct = productRepository.save(testProduct);
93+
94+
entityManager.flush();
95+
entityManager.clear();
96+
}
97+
98+
@AfterEach
99+
void tearDown() {
100+
redisTemplate.getConnectionFactory().getConnection().flushDb();
101+
databaseCleanUp.truncateAllTables();
102+
}
103+
104+
@Test
105+
@DisplayName("좋아요 생성 시 ProductLikedEvent 발행되고 Redis 캐시가 업데이트된다")
106+
void 좋아요_생성_이벤트_및_Redis_업데이트() {
107+
// given
108+
Long memberId = testMember.getId();
109+
Long productId = testProduct.getId();
110+
111+
// when
112+
likeService.like(memberId, productId);
113+
114+
// then - DB에 Like 레코드 저장 확인
115+
entityManager.clear();
116+
Like savedLike = likeRepository.findByMemberIdAndProductId(memberId, productId).orElse(null);
117+
assertThat(savedLike).isNotNull();
118+
assertThat(savedLike.getMemberId()).isEqualTo(memberId);
119+
assertThat(savedLike.getProductId()).isEqualTo(productId);
120+
121+
// then - ProductLikedEvent 발행 확인
122+
long likedEventCount = applicationEvents.stream(ProductLikedEvent.class)
123+
.filter(event -> event.memberId().equals(memberId) && event.productId().equals(productId))
124+
.count();
125+
assertThat(likedEventCount).isGreaterThan(0);
126+
127+
// then - 비동기 이벤트 처리 후 Redis 업데이트 확인
128+
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
129+
// Redis 좋아요 카운트 증가 확인
130+
Long redisCount = productLikeCountCache.get(productId);
131+
assertThat(redisCount).isEqualTo(1L);
132+
133+
// Redis 회원 좋아요 목록 추가 확인
134+
boolean isMemberLiked = memberLikesCache.isMember(memberId, productId);
135+
assertThat(isMemberLiked).isTrue();
136+
});
137+
}
138+
139+
@Test
140+
@DisplayName("좋아요 취소 시 ProductUnlikedEvent 발행되고 Redis 캐시가 업데이트된다")
141+
void 좋아요_취소_이벤트_및_Redis_업데이트() {
142+
// given - 먼저 좋아요 생성
143+
Long memberId = testMember.getId();
144+
Long productId = testProduct.getId();
145+
likeService.like(memberId, productId);
146+
147+
// 비동기 처리 대기
148+
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
149+
assertThat(productLikeCountCache.get(productId)).isEqualTo(1L);
150+
});
151+
152+
entityManager.clear();
153+
154+
// when - 좋아요 취소
155+
likeService.unlike(memberId, productId);
156+
157+
// then - DB에서 Like 레코드 삭제 확인
158+
entityManager.clear();
159+
Like deletedLike = likeRepository.findByMemberIdAndProductId(memberId, productId).orElse(null);
160+
assertThat(deletedLike).isNull();
161+
162+
// then - ProductUnlikedEvent 발행 확인
163+
long unlikedEventCount = applicationEvents.stream(ProductUnlikedEvent.class)
164+
.filter(event -> event.memberId().equals(memberId) && event.productId().equals(productId))
165+
.count();
166+
assertThat(unlikedEventCount).isGreaterThan(0);
167+
168+
// then - 비동기 이벤트 처리 후 Redis 업데이트 확인
169+
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
170+
// Redis 좋아요 카운트 감소 확인
171+
Long redisCount = productLikeCountCache.get(productId);
172+
assertThat(redisCount).isEqualTo(0L);
173+
174+
// Redis 회원 좋아요 목록 제거 확인
175+
boolean isMemberLiked = memberLikesCache.isMember(memberId, productId);
176+
assertThat(isMemberLiked).isFalse();
177+
});
178+
}
179+
180+
@Test
181+
@DisplayName("여러 회원이 동일 상품에 좋아요 시 Redis 카운트가 정확히 증가한다")
182+
void 여러_회원_좋아요_Redis_카운트_증가() {
183+
// given - 추가 회원 생성
184+
Member member2 = new Member("user2", "user2@example.com", "password123", "1992-02-02", Gender.MALE);
185+
member2 = memberRepository.save(member2);
186+
187+
Member member3 = new Member("user3", "user3@example.com", "password123", "1993-03-03", Gender.FEMALE);
188+
member3 = memberRepository.save(member3);
189+
190+
Long productId = testProduct.getId();
191+
192+
// when - 3명이 좋아요
193+
likeService.like(testMember.getId(), productId);
194+
likeService.like(member2.getId(), productId);
195+
likeService.like(member3.getId(), productId);
196+
197+
// then - DB에 3개 레코드 저장 확인
198+
entityManager.clear();
199+
long dbLikeCount = likeRepository.countByProductId(productId);
200+
assertThat(dbLikeCount).isEqualTo(3L);
201+
202+
// then - 비동기 처리 후 Redis 카운트 3 확인
203+
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
204+
Long redisCount = productLikeCountCache.get(productId);
205+
assertThat(redisCount).isEqualTo(3L);
206+
});
207+
}
208+
209+
@Test
210+
@DisplayName("Redis와 DB의 최종 일관성 확인 - 여러 좋아요/취소 작업 후")
211+
void Redis_DB_최종_일관성_확인() {
212+
// given - 추가 회원들 생성
213+
Member member2 = memberRepository.save(new Member("u2", "u2@test.com", "pw", "1990-01-01", Gender.MALE));
214+
Member member3 = memberRepository.save(new Member("u3", "u3@test.com", "pw", "1990-01-01", Gender.FEMALE));
215+
Member member4 = memberRepository.save(new Member("u4", "u4@test.com", "pw", "1990-01-01", Gender.MALE));
216+
217+
Long productId = testProduct.getId();
218+
219+
// when - 복잡한 좋아요/취소 시나리오
220+
likeService.like(testMember.getId(), productId); // +1 = 1
221+
likeService.like(member2.getId(), productId); // +1 = 2
222+
likeService.like(member3.getId(), productId); // +1 = 3
223+
likeService.unlike(member2.getId(), productId); // -1 = 2
224+
likeService.like(member4.getId(), productId); // +1 = 3
225+
likeService.unlike(testMember.getId(), productId); // -1 = 2
226+
227+
// 비동기 처리 완료 대기
228+
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
229+
entityManager.clear();
230+
231+
// then - DB 카운트 확인 (member3, member4만 남음 = 2)
232+
long dbCount = likeRepository.countByProductId(productId);
233+
assertThat(dbCount).isEqualTo(2L);
234+
235+
// then - Redis 카운트 확인 (DB와 일치)
236+
Long redisCount = productLikeCountCache.get(productId);
237+
assertThat(redisCount).isEqualTo(2L);
238+
239+
// then - 회원별 좋아요 여부 확인
240+
assertThat(memberLikesCache.isMember(testMember.getId(), productId)).isFalse();
241+
assertThat(memberLikesCache.isMember(member2.getId(), productId)).isFalse();
242+
assertThat(memberLikesCache.isMember(member3.getId(), productId)).isTrue();
243+
assertThat(memberLikesCache.isMember(member4.getId(), productId)).isTrue();
244+
});
245+
}
246+
247+
@Test
248+
@DisplayName("중복 좋아요 시도 시 DB는 중복 저장 안 되고 이벤트도 1회만 발행")
249+
void 중복_좋아요_멱등성() {
250+
// given
251+
Long memberId = testMember.getId();
252+
Long productId = testProduct.getId();
253+
254+
// when - 동일 회원이 같은 상품에 3번 좋아요
255+
likeService.like(memberId, productId);
256+
likeService.like(memberId, productId);
257+
likeService.like(memberId, productId);
258+
259+
// then - DB에는 1개만 저장
260+
entityManager.clear();
261+
boolean exists = likeRepository.existsByMemberIdAndProductId(memberId, productId);
262+
assertThat(exists).isTrue();
263+
264+
// then - 비동기 처리 후 Redis 카운트도 1
265+
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
266+
Long redisCount = productLikeCountCache.get(productId);
267+
assertThat(redisCount).isEqualTo(1L);
268+
});
269+
}
270+
}

0 commit comments

Comments
 (0)