개발

전시 도메인 캐싱에 대한 잡다한 생각들

Debin 2025. 2. 5.
반응형

이번 포스팅에서는 화면 레이아웃, 팝업, 배너, 카테고리와 같은 전시 도메인,

전시 영역에 대한 캐싱을 적용하면서 본인이 생각한 중요한 포인트 및 자잘한 생각들을 정리해보려고 한다.

 

프로젝트(이커머스)를 코프링으로 진행했으므로 아마 스프링과 JVM 관점에서 설명하는 내용이 존재할 것이다.

인프라는 AWS이며, 데이터베이스로는 SQL(Oracle)과 Redis를 사용 중이다. 

 

캐싱 적용 이유 및 놓친 부분

 

캐싱 적용은 6개월 또는 분기마다 트래픽이 높아지는 이벤트가 있는데 이에 도움이 되고자 진행한 작업이다.

사실 이커머스에서 유저가 몰릴 때 부하가 제일 많이 생기는 곳은 상품 도메인이다.


그래도 팀에서 전시 도메인 특징 상 자주 수정이 되지 않고 조회는 많으므로 캐싱 적용이 알맞고 도움이 될 것이라고 판단했다.  

 

다른 얘기지만.. 수정이 드물고 조회는 많은 전시 도메인은 MongoDB와 같은 NoSQL이 잘 어울린다고 생각한다.

하지만 현재 프로젝트는 경제적(?)인 관점으로 보았을 때 SQL(Oracle)을 NoSQL(MongoDB)로 마이그레이션하고,

새로운 기술을 프로젝트에 도입해 개선하는 건 마이너스다. 

 

아무튼.. 잠시 얘기가 샜지만 프로젝트에서는 Redis를 사용한 캐싱을 도입하기로 결정했다.

웹 애플리케이션(Tomcat) 로컬 캐시 적용에 대해 생각해봤는데..
결국 전시 도메인 데이터를 수정하면 다중 서버 로컬 캐시에 반영하기 힘드므로 로컬 캐시는 적절하지 않은 것 같다. 

 

스프링에서는 캐시를 잘 추상화해 놓았으므로 @Cacheable과 @CacheEvict를 사용해서 열심히 캐시를 적용하고 무효화도 구현했다.

 

캐시 전략 또한 Look Aside 패턴을 활용해 Redis에 없으면 Oracle에서 조회하고 값을 Redis에 담았다.

 

열심히 캐시를 적용하면서 중간에 문제를 발견했다.

스프링에서 추상화해놓은 캐시 기능(스프링 aop 기반)들만 사용하면 TTL을 설정할 수는 있지만 캐시 만료 기간을 지정할 수는 없다.

이는 직접 RedisTemplate 같은 코드로 만료 기간 (expired date)을 지정해줘야 한다.

 

예를 들어 캐시 적용 유지 시간이 10분이라고 가정해보자. A 배너는 12시부터 조회가 되면 안된다. 

그러나 캐시가 11시 59분에 적용되면 12시 09분까지 A 배너는 조회가 가능하다.

 

작성해 놓은 코드는 전부 수정해야 한다..!

 

가장 깔끔한 해결책은 무엇일까

 

해결 방법으로 온갖 상상의 나래를 펼쳐보았다.

생성, 수정, 삭제 관련 API는 @CacheEvict를 적용하면 될 것 같은데 조회 부분이 고민이었다.

 

첫 번째 방법: 복잡한 로직을 더한 캐시 적용

 

데이터가 캐시에 없고 DB에서 조회되면, 아래 조건이 참이면 캐싱을 하는 것이다.

 

현재 시간 + 캐시 시간 <= 만료 날짜 

 

기각이다. 복잡해질 뿐만 아니라 캐싱이 되지 않는 텀이 길게 존재할 수 있다.

 

두 번째 방법:  배치와 비즈니스 지식(?) 활용

 

일단 기존 방식대로 @Cacheable을 사용해 TTL을 전부 건다.

 

그리고 전시 담당자들과 하나 약속을 정한다.

예를 들어 "배너 마감은 1시 10분, 3시 20분, 3시 30분과 같이 10분 단위로 진행되어야 해요!!"

그리고 10분마다 캐시를 날리는 배치를 작성한다.

 

해당 방식은 캐싱 무효화 지점을 파악하기 힘들 때 유용할 것 같다.

그러나 비즈니스 지식이 추가되고, 수행하지 않아도 배치가 일단 돌아야한다는 단점이 존재한다.

이건 나중에 요건에 따라서 배치 잡이 늘어날수도......

 

세 번째 방법: 캐싱 코드를 직접 구현

 

아래는 직접 캐시 조회 코드와 캐시 저장 코드를 구현한 코드다.

 

fun findBanner(): BannerListResponse {

    //레디스에 존재하는지
    val cacheResponse = bannerCacheStore.findResponse()
    
    //레디스에 존재하면 리턴
    if(cacheResponse != null) {
        return cacheResponse
    }
	
    //화면에 필요한 배너를 모두 조회
    val banners = BannerRepository.findAll() 
    
    //response에는 만료시간 필드가 존재
    val response = BannerListResponse.of(banners) 
    
    //이를 캐시에 직접 저장, save 로직에는 만료 시간이 되면 사라지게 하는 코드가 담김
    bannerCacheStore.save(response)

    return response
}

//참고로 redisTempalte에서 만료 시간을 지정하는 코드는 다음과 같다.
//redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutInSeconds));

 

다른 이야기지만.. 클래스 이름을 BannerCacheStore라고 짓기 까지 꽤 많은 고민을 했다.

 

처음에는 BannerCacheRepository로 클래스 이름을 지었다.

그러나 Repository 패턴은 도메인 모델을 영속화시키기 위해 나온 패턴인데, BannerListResponse는 도메인 모델이 아니다.

따라서 Repository의 의미가 혼동될 수 있어서 BannerCacheStore라고 지었다.

 

이 방법은 ..? 무난해보인다.

 

네 번째 방법:  CDN 적용

 

ALB 응답 JSON에도 CDN을 적용할 수 있다.

현재 프로젝트에서는 드물지만 cloud front를 사용하고 있다.

 

aws sdk를 사용하면 cloud front 무효화도 손쉽게 가능하다.

또한 개인화되지 않은 영역이라면 CDN은 훌륭한 성능 개선 방법일 수 있다.

 

중요한 것은 결국 비용, 돈이다!!!

CloudFront를 적극적으로 사용하면 API 요청은 줄겠지만,

그 효과로 인해 ec2(누군가에게는 ecs, k8s) 스펙을 내리거나 여기서 돈을 아낄 수 있을까??

 

이 질문은.. 역시 계산기를 두드려봐야 알 수 있다. 

 

참고로 응답 JSON을 CDN을 사용하며 캐싱을 진행하면 다른 유저의 응답과 공유되지 않게 각별히 신경을 써야한다.

 

결론

 

필자는 3번과 4번 방법이 제일 좋아보이지만 아직 정해진 것은 없다!

역시 컴퓨터 공학에서 제일 어려운 것은 캐시 무효화와 변수명 짓기다.

 

이상으로 포스팅을 마칩니다. 감사합니다.

반응형

댓글