[항해플러스 백엔드후기] 7주차 회고 - 오쪼쪼.. 나의 임시 보관함..캐싱
[개요]
지난 주 고비를 넘고나니 상대적으로 수월해진 느낌이다
여전히 하루하루 수면부족에 시달리고는 있지만, 고지가 눈앞에 보이니 해낼 수 있다라는 생각으로
포기하지말기. 한주 한주 과제를 제출만 하자라는 생각으로 천천히 해내어 보기
[과제]
7주차의 과제는 다음과 같았다
- 조회가 오래 걸리는 쿼리에 대한 캐싱, 혹은 Redis 를 이용한 로직 이관을 통해 성능 개선할 수 있는 로직을
분석하고 합리적인 이유와 함께 정리한 문서 제출
- 선착순 쿠폰 발급 기능에 대해 Redis 기반의 설계를 진행하고 적절하게 동작할 수 있도록 쿠폰 발급 로직 개선
이번에는 캐싱과 Redis에 대해 다루게 되었다.
캐싱의 중요성에 대해서는 잘 알고 있었지만 실무에서는 사용하지 않고, Redis는 사용하지만 이미 모듈로 구현된걸 가져다가 써봤을 뿐 따로 Redis 기반의 설계 자체를 해본 적은 없어 자세한 공부가 필요했다.
[과제 해결 과정]
1. 캐시 ? 캐싱 ?
- 캐시 : 자주 사용하는 데이터를 더 빠르게 접근할 수 있도록 메모리(RAM) 등에 저장하는 기술
- 캐싱 : 데이터의 원래 소스보다 더 빠르고 효율적으로 액세스 할 수 있도록 임시 데이터 저장소
- 캐시를 사용하는 이유
- 동일한 데이터에 반복적으로 접근하는 상황이 많을 때 사용하는 것이 효과적
- 잘 변하지 않는 데이터일수록 캐시를 사용하는 것이 효과적
- 데이터에 접근 시 복잡한 로직이 필요한 경우 사용하면 효과적
2. 캐시 <> Redis
캐싱에는 2가지 level 이있는데 메모리 캐시와 별도의 캐시 서비스가 있다.
- application level 메모리 캐시
- 애플리케이션의 메모리에 데이터를 저장해두고 요청에 대해 데이터를 빠르게 접근해 변환하여 성능 향상 효과를 얻는 방
- external level 별도의 캐시 서비스
- 별도의 캐시 저장소 또는 이를 담당하는 API 서버를 통해 캐시 환경을 제공하는 방법 ex) Redis, Nginx 캐시, CDN
흔히 캐싱 = Redis 라고 이야길 하길래 헷갈렸는데 그게 아니고 캐싱을 구현하는 서비스 중의 하나로 Redis가 있는 것이었다.
캐싱 전략에는 4가지가 있다.
전략 | 설명 | 장점 | 단점 | 사용 사례 |
Read-Through | 캐시를 먼저 조회하고 없으면 DB에서 가져와 캐시에 저장 | - 캐시가 자동으로 최신 데이터를 유지 - 애플리케이션이 캐시 로직을 직접 관리할 필요 없음 |
- 캐시 미스 시 응답 속도 저하 - 실시간 업데이트가 어려울 수 있음 |
- 제품 상세 정보 - 사용자 프로필 조회 |
Write-Through | 데이터를 DB에 저장하면서 동시에 캐시에도 저장 | - 캐시와 DB 간 데이터 일관성 유지 - 자주 사용되는 데이터의 캐시 적중률 증가 |
- 쓰기 성능이 저하될 수 있음 - 불필요한 캐시 저장 가능 |
- 사용자 정보 저장 - 주문 상태 관리 |
Write-Around | 데이터를 캐시에 저장하지 않고 직접 DB에 저장, 조회 시 Read-Through 방식으로 캐싱 | - 불필요한 캐시 저장을 방지 - 캐시 낭비 최소화 |
- 첫 조회 시 캐시 미스로 인해 성능 저하 - 자주 조회되는 데이터가 아니면 캐싱 효과 미비 |
- 변경이 적고 자주 조회되지 않는 데이터 |
Write-Back (Write-Behind) | 데이터를 먼저 캐시에 저장하고, 일정 주기마다 DB로 반영 | - 쓰기 성능이 가장 빠름 - 자주 변경되는 데이터에 유리 |
- 캐시 장애 발생 시 데이터 유실 가능 - DB와의 일관성 유지가 어려울 수 있음 |
- 실시간 로그 저장 - IoT 센서 데이터 |
참고 : https://www.youtube.com/watch?v=92NizoBL4uA
3. 캐싱 그거 그래서 어떻게 구현하는건데 ..
- 조회가 오래 걸리는 쿼리로는 현재 시나리오 상 제일 복잡한 쿼리를 가지고 있는 "3일간 주문 상위 5 위 조회 " 기능에 캐싱을 활용하기로 하였다.
SELECT *
FROM OrderItem oi
LEFT JOIN Order o ON oi.order.id = o.id
WHERE oi.createdAt >= :threeDaysAgo AND o.status = 'COMPLETED'
GROUP BY oi.product.id
ORDER BY SUM(oi.quantity) DESC
- 캐싱 적용 이유
- 해당 데이터를 참조하기 위해서는 복잡한 로직이 필요
- 해당 데이터는 모든 사용자에게 공통적으로 보여지는 데이터 이므로 다른 데이터보다 상대적으로 조회가 빈번하다고 판단
- 3일간 주문 상위 5개의 통계는 사용자에게 실시간으로 정확하게 보여야 하는 데이터가 아님
- 캐시 만료 시간만큼 동일한 통계 데이터를 사용자에게 제공해도 무방
- 캐시 TTL (Time To Live) 설정
- 설정 시간: 24시간
- 이유: 과거의 판매내역을 기준으로 상품 집계를 하는 것이므로 매일 00시에 새로운 데이터로 갱신해도 무방하기 때문에 하루를 기준으로 캐시 만료 시간을 설정
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory());
// Key Serializer 설정
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public CacheManager cacheManager(LettuceConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(24)) // 캐시 만료 시간: 24시간
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
- 스프링의 캐시매니저를 활용하여
@Scheduled(cron = "0 0 0 * * ?") // 매일 00:00 실행
@CachePut(value = "topProducts", key = "'top_5_products'") // 캐시 갱신
public List<TopProductVO> updateTopProductsCache() {
log.info(" [스케줄러 실행] 3일간 최다 판매 상품을 Redis에 저장합니다.");
// 1 최근 3일간 최다 판매 상품 조회
List<TopProductVO> topProducts = productApplicationService.getTopProducts();
if (topProducts == null || topProducts.isEmpty()) {
log.error("최근 3일간 판매된 상품이 없습니다.");
return new ArrayList<>();
}
log.info(" [스케줄러 완료] 최다 판매 상품 5개가 Redis에 저장되었습니다.");
return topProducts;
}
- Scheduler로 매일 00시에 캐싱되도록 구현하였다
- 따로 eviction 하지 않고 같은 키값으로 CachePut 하도록 설정하였다
- 굳이 다른 키값으로 로그를 남겨둘만한 정보도 아니고 00시에 바로 업데이트 되므로 캐시 업데이트로 해주는 게 낫다고 생각하였다
4. 선착순 쿠폰 기능의 Redis 활용
기존의 선착순 쿠폰 기능은 DB락을 활용하여 동시성 제어를 하는 방식으로 구현되어 있었는데 이 부분을 Redis를 활용하여 비동기로 구현하는 것이 목표였다.
1. 선착순 쿠폰 요청이 들어옴
2. 해당 요청을 Redis에 저장
3. 스케줄러로 해당 요청값들을 Redis에서 꺼내옴
4. 선착순 쿠폰 재고 에 맞게 쿠폰 발급 처리
5. 선착순에 들지 못한 요청은 실패 큐에 따로 저장
6. 선착순에 들지 못한 요청을 요청 큐에서 삭제 시에 실패 큐에 저장된 값들을 꺼내 비동기로 사용자에게 실패 알림
-> 실패 큐에 저장된 값들 또한 비동기로 처리해야 하는데, 이또한 스케줄러에서 처리하는 과정에서 어떤 시간 간격으로 두어야 할지 고민하다가 그냥 발급 요청시에 같이 처리하는 걸로 구현해 버렸는데 이렇게 해버리니 굳이 나눈 의미가 없이 다시 기능간 의존도가 생겨서 좋지 않은 방식이라고 피드백을 받았다
-> 조금더 고민해보고 리팩토링을 해봐야겠다
[알게된 점]
캐싱에 대해서 자세히 공부하고 직접 다뤄볼 수 있어서 좋았다. 우연한 타이밍이었지만 해당 과제를 제출하고 난 다음날에 바로 실무에서도 바로 캐싱을 해야 하는 이슈가 생겨 바로 써먹은걸 적용해 볼 수 있었다. 이렇게 항해에서 공부해보지 않았더라면 분명히 캐싱에 대해서 제대로 공부해보지도 못하고 급하게 구현해서 올리기에만 바빴을 텐데, 바로 배운걸 적용해 볼 수 있어서 뿌듯했다.
무엇이든 책으로만, 영상으로만 공부하지 말고 직접 내가 구현해봐야 나에게 체득된다는 걸 다시한번 느꼈다.
이번주 KPT 회고
Keep : [유지해야 할 좋은 점]
다소 걱정되었는데 다행히도 둘다 pass를 받았다. 비동기로 구현하면서 여러 멘토링들을 청강하면서 내가 구현한 방향은 비슷하지만
Problem : [개선이 필요한 점]
Redis를 구현하면서 레이어와 패키징에 대한 고민을 많이 했는데 역시 피드백을 받았다.
부끄럽지만 이전에는 구조에 대해서 큰 고민을 하지 않고 개발하곤 했는데 (이미 구현된 프로젝트에 유지보수만 하는 경우가 많으므로)
이제는 기능하나를 새로 개발하더라도 현재 구조보다 더 나은 구조를 고민해보고, 어떤 구조를 가져가야 할지 , 어떤 레이어에서 어떤 게 이루어져야 하는지 먼저 생각해보고 구현하는 습관이 생겼다. 다만 아직은 많이 부족해서 디자인 패턴과 클린 아키텍처에 대한 공부를 더 해보아야겠다는 열정이 생겼다
Try : [새롭게 시도할 점]
보고서를 쓰는 게 참 어렵다. 글을 잘쓰려면 글을 많이 읽고 많이 쓰는 수 밖에 없는데 점점 퇴화하고 있는 것만 같다
다른 잘 작성된 글을 보면서 연습을 많이 해보아야 겠다
🎉 수료생 추천 할인 혜택 안내 🎉
항해 플러스에 합류하고 싶은데 수강료가 부담되시나요? 🤔
수료생 추천 할인 혜택으로 20만 원 할인을 받을 수 있다는 사실, 알고 계셨나요? 💡
💳 할인 적용 방법
1️⃣ 결제 페이지로 이동
2️⃣ 할인 코드 입력란에 zzy6ui 입력
3️⃣ 추가 할인 20만 원 바로 적용!
놓치지 말고 특별한 혜택 꼭 챙겨가세요! 🚀🌟