๐ฟ ๊ธ ์ฃผ์ ๊ฐ๋จ ์๊ฐ
์ด์ปค๋จธ์ค ํ๋ก์ ํธ์์ Redis๋ฅผ ํ์ฉํ ์ธ๊ธฐ ์ํ ์กฐํ & ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ์ต์ ํ
์ด ๊ธ์์๋ Redis๋ฅผ ํ์ฉํ์ฌ ์ธ๊ธฐ ์ํ ์กฐํ ๋ฐ ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ ์ต์ ํํ ๊ณผ์ ์ ์ ๋ฆฌํฉ๋๋ค.
๊ธฐ์กด ์์คํ
์ ๋ฌธ์ ์ ์ ๋ถ์ํ๊ณ , Redis๋ฅผ ์ ์ฉํ ์ด์ ๋ฐ ๊ธฐ๋ ํจ๊ณผ๋ฅผ ์ค๋ช
ํฉ๋๋ค.
๐ฟ๊ธฐ์กด ๋ฌธ์ ์ ๋ฐ ํ๊ณ
๐ ์ธ๊ธฐ ์ํ ์กฐํ์ ๋ฌธ์ ์
์ด์ปค๋จธ์ค์์ ์ต๊ทผ 3์ผ ๋์ ๊ฐ์ฅ ๋ง์ด ํ๋ฆฐ ์ํ์ ์กฐํํ๋ API๋
๋งค ์์ฒญ๋ง๋ค DB์์ ์ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ๊ธฐ ๋๋ฌธ์, ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
- ํธ๋ํฝ ์ฆ๊ฐ ์ DB ๋ถํ ์ฌํ
- ์ธ๊ธฐ ์ํ ์กฐํ๋ ์ ์ ๋ค์ ์ฌ์ฉ ๋น๋๊ฐ ๋์ API
- ์กฐํ ์๋ง๋ค DB์์ ์ฐ์ฐ์ ์ํํด์ผ ํ๋ฏ๋ก, ํธ๋ํฝ์ด ์ฆ๊ฐํ๋ฉด DB ๋ถํ๊ฐ ์ฌ๊ฐํด์ง
- ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ฃผ๊ธฐ๊ฐ ๊ธธ์ง๋ง, ์บ์ฑ์ด ์์์
- ์ธ๊ธฐ ์ํ ์ ๋ณด๋ ์ค์๊ฐ์ผ๋ก ๋ฐ๋์ง ์์ (์งํํ๊ณ ์๋ ํ๋ก์ ํธ์์๋ ํ๋ฃจ๋์์ ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ)
- ๋ถํ์ํ DB ์กฐํ๋ฅผ ์ค์ด๊ธฐ ์ํด ์บ์ฑ์ด ํ์ํจ
๐ ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ ๋ฌธ์ ์
์ด์ปค๋จธ์ค์์ ํน์ ์ด๋ฒคํธ(์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ) ์ ๋์ ์ ์์๊ฐ ๊ธ์ฆํ ์ ์์ต๋๋ค.
ํ์ง๋ง ๊ธฐ์กด์๋ ์ฟ ํฐ ๋ฐ๊ธ ์์ฒญ์ RDB(MySQL)์ ์ง์ ์ ์ฅํ๋ ๋ฐฉ์์ด์๊ธฐ ๋๋ฌธ์, ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- ๋์์ฑ ์ด์
- ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์์ฒญํ๋ฉด ๊ฒฝํฉ ์กฐ๊ฑด(Race Condition) ๋ฐ์
- ์ฟ ํฐ ์ด๊ณผ ๋ฐ๊ธ ๊ฐ๋ฅ์ฑ (ํธ๋์ญ์ ์ด ์์ ํ ์ฒ๋ฆฌ๋๊ธฐ ์ ์ ๋ค๋ฅธ ์์ฒญ์ด ์ฒ๋ฆฌ๋ ๊ฐ๋ฅ์ฑ)
- ์ฑ๋ฅ ์ ํ
- ์งง์ ์๊ฐ์ ๋๊ท๋ชจ ํธ๋ํฝ์ด ๋ฐ์ํ๋ฉด DB Connection ๋ถํ๊ฐ ๋นํจ์จ์ ์ผ ์ ์์.
- ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ์ ํธ๋์ญ์ ์ถฉ๋์ ๋ฐฉ์งํ๊ธฐ ์ํด DB ๋ฝ์ด ๊ฑธ๋ฆด ๊ฐ๋ฅ์ฑ
- ํธ๋์ญ์ ์ถฉ๋์ด ๋ฐ์ํ๋ฉด DB ์ฑ๋ฅ์ด ์ ํ๋๊ณ , ๋ค๋ฅธ ์์ฒญ๊น์ง ๋๊ธฐํด์ผ ํจ
๐ฟ Redis ๋์ ๋ฐฐ๊ฒฝ
๐ Redis๋?
Redis๋ ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ Key-Value ๋ฐ์ดํฐ ์ ์ฅ์๋ก,
๋น ๋ฅธ ์ฝ๊ธฐ/์ฐ๊ธฐ ์๋์ ๋ค์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ(Set, SortedSet, List ๋ฑ)๋ฅผ ์ง์ํ๋ ๊ฒ์ด ํน์ง์
๋๋ค.
๋ํ, ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ ์์ฐจ์ ์์ฒญ์ ํน์ง์ SortedSet์ผ๋ก ๊ฐ๋จํ ํด๊ฒฐํ ์ ์์ต๋๋ค.
์ด์ปค๋จธ์ค์์ Redis๋ฅผ ํ์ฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ฅ์ ์ด ์์ต๋๋ค.

๐ฟ Redis ์ ์ฉ ๋ฐฉ์
๐ ์ธ๊ธฐ ์ํ ์กฐํ ์บ์ฑ
โ ์ ์ฉ ๋ชฉํ
- ์ต๊ทผ 3์ผ๊ฐ ์ธ๊ธฐ ์ํ ์กฐํ ๊ฒฐ๊ณผ๋ฅผ Redis์ ์บ์ฑํ์ฌ ๋ถํ์ํ DB ์กฐํ๋ฅผ ์ค์
- ๋งค์ผ ์์ (00:00)์ ์บ์๋ฅผ ์ญ์ (Eviction)ํ์ฌ ์ต์ ๋ฐ์ดํฐ ์ ์ง
- TTL(๋ง๋ฃ ์๊ฐ) ๋์ ์ค์ผ์ค๋ง์ ํ์ฉํ์ฌ ๋งค์ผ ํน์ ์๊ฐ์๋ง ๊ฐฑ์
โ ๊ตฌํ ์ฝ๋ (Java)
//RedisCacheManager๋ฅผ ํ์ฉํ ์ฝ๋ ๊ตฌํ(์ค์ ์ ์ฌ๋ฆฌ์ง ์์ต๋๋ค.)
//์บ์๊ฐ ํ์ํ ๋ก์ง์ Redis ์บ์ ์ ์ฅ
@Service
@RequiredArgsConstructor
public class ProductService {
private final OrderRepository orderRepository;
@Cacheable(value = "popular_products", key = "'last_3_days'", cacheManager = "redisCacheManager")
public List<PopularProductDto> getPopularProduct3Days() {
LocalDateTime endDate = LocalDateTime.now();
LocalDateTime startDate = endDate.minusDays(3);
Pageable three = PageRequest.of(0, 3);
return orderRepository.findPoplarProductBeforeDays(startDate, endDate, three);
}
}
//๋งค์ผ ์์ ์ ์บ์ ์ด๊ธฐํ
@Component
@RequiredArgsConstructor
public class PopularProductCacheEvictionTask {
private final CacheManager cacheManager;
@Scheduled(cron = "0 0 0 * * *") // ๋งค์ผ ์์ (00:00) ์คํ
public void evictPopularProductCache() {
cacheManager.getCache("popular_products").clear();
}
}
โ ์บ์ฑ ๋์ ๋ฐฉ์
- API๊ฐ ํธ์ถ๋๋ฉด Redis์ "popular_products:last_3_days" ํค๊ฐ ์กด์ฌํ๋์ง ํ์ธ
- ์กด์ฌํ๋ฉด: Redis์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ๋ฐํ (DB ์กฐํ X)
- ์กด์ฌํ์ง ์์ผ๋ฉด: DB์์ ์กฐํ ํ Redis์ ์ ์ฅ
- TTL์ ์ค์ ํ์ง ์๊ณ , ๋งค์ผ ์์ (00:00)์ @Scheduled์ ์ฌ์ฉํ์ฌ ์บ์๋ฅผ ์ญ์ .
- ์บ์๋ ์์ ๋ง๋ค ์ญ์ ๋๋ฏ๋ก, ๋ค์ ํธ์ถ ์ ์ต์ ๋ฐ์ดํฐ๋ฅผ Redis์ ์ ์ฅ
๐ ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ Redis SortedSet ํ์ฉ
์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ์ Redis์ SortedSet(ZSET)์ ํ์ฉํ๋ฉด ๋์์ฑ ์ ์ด + ์ ์ฐฉ์ ์ ๋ ฌ ๊ธฐ๋ฅ์ ๋์์ ํด๊ฒฐ ๊ฐ๋ฅ!
โ ์์ ์ฝ๋
ps.) ์ ์ ํ๋ก์ ํธ ๋ก์ง์ Repository๋ฅผ ๋ฐ๋ก ๋ถ๋ฆฌํ์ฌ ์ฌ์ฉ์ค์ด๊ธฐ ๋๋ฌธ์ ๊ฐ๋จํ ์ฌ์ฉํ๋ฆ์ ๋ํ ์ค๋ช ์ ์ํ ์์์ฝ๋์ ๋๋ค.
//1.์ฟ ํฐ ๋ฐ๊ธ ์์ฒญ ์ ์ฅ
public void requestCoupon(Long couponId, String userId) {
String key = "coupon-requests:" + couponId;
redisTemplate.opsForZSet().add(key, userId, System.currentTimeMillis()); // ์์ฒญ ์๊ฐ ๊ธฐ์ค ์ ๋ ฌ
}
//2. ๊ฐ์ฅ ๋น ๋ฅธ ์์๋๋ก N๊ฐ ๊ฐ์ ธ์์ ๋ฐ๊ธ ๊ฒ์ฆ & ์ฒ๋ฆฌ
public void processCouponRequests(Long couponId, int batchSize) {
String requestKey = "coupon-requests:" + couponId;
String issuedKey = "issued-users:" + couponId;
// 1.๊ฐ์ฅ ์ค๋๋ ์์ฒญ `batchSize`๊ฐ ๊ฐ์ ธ์ค๊ธฐ
Set<String> userIds = redisTemplate.opsForZSet().range(requestKey, 0, batchSize - 1);
if (userIds == null || userIds.isEmpty()) return;
// 2.์ฟ ํฐ ์ฌ๊ณ ํ์ธ
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND));
if (coupon.getStock() < userIds.size()) {
return; // ์ฌ๊ณ ๋ถ์กฑ → ๋ ์ด์ ์งํ ์ ํจ
}
// 3.๋ฐ๊ธ ์ฒ๋ฆฌ
for (String userId : userIds) {
if (!redisTemplate.opsForSet().isMember(issuedKey, userId)) { // ์ค๋ณต ๋ฐฉ์ง
issueCoupon(userId, couponId); // ์ฟ ํฐ ๋ฐ๊ธ
redisTemplate.opsForSet().add(issuedKey, userId); // ๋ฐ๊ธ๋ ์ฌ์ฉ์ ์ ์ฅ
redisTemplate.opsForZSet().remove(requestKey, userId); // ์์ฒญ ๋ชฉ๋ก์์ ์ญ์
}
}
}
โ ๋์ ๋ฐฉ์
- ์์ฒญ์ด ์ค๋ฉด ์ฟ ํฐ ์ ๋ณด๋ฅผ ์กฐํ (Redis์ String์ ์ด์ฉํด ์ฟ ํฐ id์ ์ฟ ํฐ์๋์ ์ ์ฅ)
-> ์์ผ๋ฉด DB ์กฐํ ํ ์บ์ ์ ์ฅ
-> ์์ผ๋ฉด Redis์์ ๊ฐ์ ธ์ค๊ธฐ - ์ฟ ํฐ์ ๋ณด๋ก ๊ฐ๋ฅ ์๋ ๊ฒ์ฆ ํ ๋ฐ๊ธ์์ฒญ์ ๋ณด๋ฅผ SortedSet์ ์ ์ฅ(์์๋ณด์ฅ)
- ์ค์ผ์ฅด๋ง์ ๋๋ฉด์ ๋ฐ๊ธ์์ฒญ์ ๋ณด SortedSet์์ ๋ฝ์์ ๋ฐ๊ธ๊ฒ์ฆ ํ ๋ฐ๊ธ์ฒ๋ฆฌ
- ์ฑ๊ณต / ์คํจ ์ ๊ฐ๊ฐ์ Redis List ํค์ ์ ์ฅ ํ ์ฌ์ฉ์์๊ฒ ์๋ฆผ (๊ตฌํ์ ์ถํ์)
๐ฟ Redis ๋์ ํ ๊ธฐ๋ ํจ๊ณผ
๐ [์ธ๊ธฐ ์ํ ์กฐํ ์ต์ ํ]
- DB ๋ถํ ๊ฐ์ (ํธ๋ํฝ ์ฆ๊ฐ ์์๋ ์ฑ๋ฅ ์ ์ง)
- ์์ ๋ง๋ค ์๋์ผ๋ก ์ด๊ธฐํ ํจ์ผ๋ก์จ ์ต์ ๋ฐ์ดํฐ ๊ฐฑ์ ๊ฐ๋ฅ(๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ์ง)
- ๋น ๋ฅธ ์๋ต ์๋ (DB ์กฐํ ์์ด Redis์์ ๋ฐ์ดํฐ ์กฐํ)
๐ [์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ๊ฐ์ ]
- ๋๊ท๋ชจ ํธ๋ํฝ์ด ๋ฐ์ํ๋ฉด DB Connection ๋ถํ๋ฅผ ํจ์จ์ ์ผ๋ก ์ค์ผ ์ ์์.
- ๊ฒฝํฉ ์กฐ๊ฑด(Race Condition) ํด๊ฒฐ (SortedSet์ ์ด์ฉํ ์ ์ฐฉ์ ์ฒ๋ฆฌ)
- ํธ๋์ญ์ ์ถฉ๋ ์์ด ๋์์ฑ ์ ์ด ๊ฐ๋ฅ
- DB Lock ์์ด ๋น ๋ฅธ ์ฟ ํฐ ๋ฐ๊ธ ๊ฐ๋ฅ
๐ฟ ์์ฝ ์ ๋ฆฌ
- ์ด์ปค๋จธ์ค ํ๋ก์ ํธ์์ Redis๋ฅผ ํ์ฉํ์ฌ ์ธ๊ธฐ ์ํ ์กฐํ & ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ ์ต์ ํ!
- ์บ์๋ฅผ ์ ์ฉํ์ฌ ์ธ๊ธฐ ์ํ ์กฐํ ์ฑ๋ฅ์ ๋ํญ ํฅ์
- SortedSet์ ์ด์ฉํ์ฌ ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ ๋์์ฑ ๋ฌธ์ ์์ด ์์ ์ ์ผ๋ก ๊ตฌํ
- ์ด์ ํธ๋ํฝ์ด ์ฆ๊ฐํด๋ Redis ๋๋ถ์ ์์คํ
์ด ์์ ์ ์ผ๋ก ์ด์ ๊ฐ๋ฅ
- ์ด ๋ถ๋ถ์... ์ถํ k6๋ก์ ๋ถํํ ์คํธ๋ฅผ ์ง์ ํด๋ณด๊ณ ๋์ผ๋ก ํ์ธํด๋ณด์!!