IT 기술 관련/DevOps / / 2024. 7. 26. 11:31

Redis? Caffeine? 적절한 캐시 활용

DB를 잘 설계하고 쿼리를 튜닝하면 고가용성 SW를 만들수 있습니다. 그런 노력을 해도 어느날인가는 그 한계에 도달하게 됩니다. 서비스가 잘되서 일수도 있고 미쳐 튜닝이 안된 요소가 있을수 있고... 개발자 입장에서는 자신이 만든걸 많이 쓴다는 점에서 기분이 좋기도 합니다만, 장애가 걱정되기도 하지요. 그때 좋은 해결사가 캐시 입니다.

요즘은 캐시하면 대부분은 redis 를 생각하게 됩니다. 상당히 신뢰성 있는 SW이기도 합니다만, 별도 메모리 빵빵한 장비를 준비해야 하고 네트워크로 통신도 해야 합니다. 이중화 하게되면 최소 3대이상(클러스터 구성시는 기본 6대)이니 어떤 경우에는 서비스하는 AP 서버보다 더 많아지는 배보다 배꼽이 커지는 경우도 생깁니다. redis 필수로 아키텍처를 고려하게 되면 DB 다음의 week point가 생기기 때문에 장애 상황도 고려해야 합니다. 

저는 캐시를 써야 되는 상황에 맞춰서 케이스를 나눠 적용합니다. 그 내용을 정리해 봤습니다.

 

Server remote cache

Redis, KeyDB, Valkey 같은 것들입니다. 요즘은 대부분 웹 어플리케이션을 다중화 합니다. 다중화를 할 경우 여러 서버에서 동일한 데이터를 제공하기 위해 고려해야 할 점들이 발생합니다. 대표적으로 이미지 파일을 저장하고 나서 다시 그 이미지를 요청할때 웹 어플리케이션 서버(또는 도커 컨테이너)가 달라도 이미지를 제공할 수 있게 NAS 또는 S3 같은것을 이용하는 경우가 대표적인 예입니다. 

데이터도 마찬가지 입니다. 각각의 웹 어플리케이션 프로세스가 동일 데이터, 반드시 일치해야 하는 데이터를 제공해야 하고 또한 한쪽에서 변경을 했을때 다른쪽에도 동일하게 전파되어야 하는 경우에 사용됩니다.

보통은 DB에 저장된 데이터를 캐싱하는데 사용되기 때문에 복잡한 구조의 데이터, 예를 들면 각각의 테이블에 사용자 기본 정보, 결제수단 정보등이 분산되어 있을 경우 이를 묶어서 한개의 객체로 만들어 두고 제공할때 효과적이고 휘발성 데이터, 인증키 같이 DB에 저장하고 매번 꺼내쓰기 부담스럽거나 일정시간 동안만 쓰다가 버릴 데이터를 관리하기 좋습니다. 또한 채팅 서버 같은걸 만들었을때 동일 방에 있는 사용자들이 각각 다른 서버에 붙어 있는 경우 이들을 묶어서 대화를 유지시키는 등에 효과적입니다.  

 

Server local cache

Ehcache, Caffeine 같은 것들입니다. 이건 간단하게 생각하면 예전(20년 전쯤)에 hashmap(또는 hashtable)같은 걸로 저장하던것을 랩핑해서 제공하는 SW라고 보시면 됩니다. 서두에서 언급한 것처럼 Redis 같은 것들은 장비 운용 부담이 있으나 로컬 캐시는 현재 구현한 웹 어플리케이션에 포함시켜서 구동되기 때문에 비교적 가볍습니다. 또한 중앙집중식으로 관리되어야 하는 Redis는 죽었을때 전체 웹 어플리케이션으로 장애가 전파되는 것과 다르게 각각의 프로세스 내에서 동작하기 때문에 한 프로세스에서 문제가 발생해도 해당 프로세스만 장애 또는 성능 저하가 발생하지 전체 서비스로 전파되지는 않습니다. 대신 각 웹 어플리케이션 프로세스간에 데이터 동기화는 보장이 되지 않기 때문에 쓰는 분야가 제한됩니다. 전체적으로 각 프로세스가 별도 메모리를 쓰기 때문에 전체적인 메모리 사용량은 리모트 캐시에 비해 늘어나게 됩니다.

보통은 자주 변경되지 않는 데이터를 관리할때 사용하면 좋습니다. 예를 들면 상태가 자주 바뀌는 데이터 보다는 SW의 설정값 또는 뉴스 컨텐츠같이 한번 생산되면 왠만하면 바뀌지 않는 데이터를 DB에 저장하고 그걸 꺼내 쓰는 형태로 구현한 경우나 이중화를 고려하지 않은 솔루션 패키지를 개발했을때 사용하기도 합니다.

Browser cache (Web cache) 

이것도 캐시냐? 라고 하실텐데 캐시 맞습니다. 특히 별도 웹서버 없이 spring boot 등으로 내장 WAS를 사용할 경우 html/css/js/image의 요청도 내장 WAS 가 처리해야 합니다. 결과적으론 내장 WAS가 일을 더 많이 해야 하기 때문에 실제 목적인 데이터 처리를 덜할수 밖에 없는것이고 네트워크 트래픽도 많아집니다. html/css/js/image등 정적 리소스라고 불리는 것들은 브라우저에서 캐싱되서 사용할수 있도록 구성하는게 좋습니다. 앞단에 nginx같은 웹서버를 두거나 Spring boot 면 application.yml에 cache 설정을 추가하는게 좋습니다. 이건 웹 페이지 성능 튜닝때 자세히 언급하겠습니다.

Client cache (Local storage)

사용자 PoC 주요 수단이 웹 브라우저인 웹 어플리케이션이라면  로컬 스토리지를 이용하는것도 좋은 방법입니다. 특히 쿠키로 뭘 처리하려는 분들에게는 적극 추천 합니다. 쿠키의 용량 한계가 4kb이고 이건 매번 네트워크를 타고 전송했다가 전송받는것이 트래픽 낭비를 유발합니다. 저는 주로 사용자 UI가 비어있다가 갑자기 채워지는 경우 이질감을 방지할때나 한번 받은 데이터가 Server local cache처럼 불변의 가능성이 높은 경우에 사용합니다. 예전에 뉴스 제공하는 서비스에 적용했었는데 클라이언트 로컬에 저장하고 그걸 UI에 표시하게 구현하였더니 그 당시 오전 일과시간에 네트워크 장애가 났었는데 사용자들이 아무도 인지를 못했었습니다. ㅎㅎ

 

캐싱 전략 

캐시는 DB와 데이터 일치가 된다는 신뢰성이 확보가 필요함으로 DB에 넣을때나 꺼낼때 캐시를 같이 갱신하는 경우가 많습니다. 그런데 캐시라는 것도 결국은 메모리를 쓰다보니 캐싱 대상 컨텐츠가 많아지면 그만큼 메모리를 많이 쓰게 됩니다. 그 경우에 제가 개발자에게 '이거 캐시 히트율은 높은거야?' 라고 질문 합니다만 대부분 모릅니다. 모르기 때문에 그냥 일단 넣고 보는거지요. 넣어두면 빠르니까... 저는 이건 낭비라고 봅니다. 아울러 넣고 나서 lifetime을 지정하지 않고 주구장창 보관하는 것도 마찬가지 입니다. 하드는 비싸지만 메모리는 더 비싸다. 이건 IT 쟁이들 대부분이 아는 상황인데 그 비싼 메모리를 가지고 낭비를 하다니..

제가 구현시 고려(가정?)하는 점은 이렇습니다. 

  • 해당 데이터를 사용자가 어떻게 쓸지 고려하자
    • 사용자 정보/인증 데이터는 데이터는 단기간내에 재요청할 가능성이 높으며 일정 기간 요청이 안들어오는 경우는 그 사용자가 이탈했을 가능성이 높다(로그아웃 또는 브라우저/앱을 닫음)
    • 뉴스 성격 데이터(공지사항 등)는 전면에 배치하는 것들은 요청 가능성이 높다
    • 업무용 인트라넷의 컨텐츠 데이터는 목록 1페이지 또는 금일 등록 건의 상세 정보 요청 가능성이 높다
    • FAQ는 생각보다 요청이 안들어온다(차라리 DB나 json파일로 만들어 정적으로 쓰는게 좋다)
    • 설정 데이터는 Server local cache를 쓰고 lifetime을 길게 가져간다(설정 조회 부분에서 장애가 발생해도 장시간 버틸수 있음)
    • 목록 자체를 캐싱하는것은 컨텐츠가 자주 생산되는 서비스에서는 불리하다. 목록보다는 상세쪽에 적용하는것이 좋다.
  • 요청이 들어오기 전까지는 일단 메모리에 넣지 말자
  • 캐시에 없다고 DB에도 없는건 아니다
  • 오랫동안 요청이 안들어온 데이터는 앞으로도 안 쓴다
  • DB와 캐시는 일치해야 한다. 그러나 그걸 매번 체크할순 없으니 불일치의 갭을 줄여야 한다
  • 전략 적용후 hit ratio를 체크한다(로그 또는 redis-cli 사용)

위의 사항과 맞물려서 개발자분들에게 권고하는 로직은 다음과 같습니다. Server remote cache 적용 기준입니다.

  • 요청이 들어왔을때 캐시를 확인한다
  • 캐시에 있으면 데이터를 응답해주고 lifetime 갱신(옵션), 없으면 DB에서 조회해서 데이터 구성 후 캐시에 넣은뒤 응답한다
  • 변동사항이 발생(admin site등에서 수정 발생)하면 DB에 반영하고 캐시에서 삭제한다 (이때 캐시에 재반영은 없음)
  • 캐시에 등록되는 데이터는 lifetime(TTL, Expire)을 적용한다

이렇게 구현할 경우 언제 요청이 올지 모르는 데이터를 무조건 캐시에 넣는것을 방지하기 때문에 메모리를 효과적으로 사용할수 있습니다. 또한 lifetime을 적용해 뒀으니 혹시 모를 버그로 인한 데이터 불일치도 lifetime 이후에 재보정이 되니 개발자/운영자가 개입해서 조치할 필요도 없습니다. 간혹 lifetime마다 데이터를 DB에서 꺼내와야 하는 점에 부담감을 가지는 분들이 있던데 매번 DB 쿼리를 하는것보다는 비약적으로 쿼리 횟수가 줄어들기 때문에 DB에 부담이 상당히 감소하게 됩니다. 캐시서버 장애 발생시에도 try catch로 예외를 걸어주면 DB를 쓰기 때문에 사용자 입장에서는 체감 시간이 늘어나겠지만 서비스의 장애로 전파되지는 않는 장점도 있습니다. Server local cache를 적용할때는 외부에서의 변동사항을 감지할수 없기 때문에 lifetime에 의존적일수 밖에 없습니다. 그 부분은 운영적인 측면으로 풀어야 합니다. 예를 들면 히든 API등으로 명령을 날려서 캐시를 삭제하게끔 처리하는 방안이 있습니다.

마무리

캐시는 상당히 유용한 해법이기는 합니다. 그러나 개인적으로 생각했을때 캐시 SW에 너무 의존적이게 되면 백엔드 개발자 입장에서 고가용성 SW를 만드는데 중요한 요소인 DB설계나 튜닝에 대해 둔감하게 됩니다.

저는 캐시를 보조제로 생각하는 사람입니다. 그 이유가 있는데, 10년도 더 된 일입니다만 제가 있던 조직에서 만든 4년마다 열리는 전세계적으로 유명한 스포츠 대회관련 서비스가 개막식날 장애가 발생했습니다. 그걸 해결하려고 이틀동안 사무실에서 살았는데 검수단계에서 수행한 성능 테스트 결과를 알고 있었기 때문에 장애 발생이 이해가 되지 않았었습니다. 후에 개발자와 면담을 해보니 사용자의 관심 종목/선수/국가 정보를 디비에서 한번 읽고 그걸 캐시에 넣어서 그 캐시 내용으로 성능 테스트를 한거였습니다. 전세계인구에 육박하는 사용자를 다 캐시로 담을수 없으니 그 이후 로직에서부터 DB 쿼리를 호출하기 시작했는데 튜닝안된 쿼리로 동작하니 성능 저하가 발생하는 원인이라는걸 발견하고 이후 성능 테스트부터는 캐시를 끄고 테스트하게 했었습니다. 

서비스의 내용에 따라 적절한 설계가 있습니다. 그 다음에 쿼리 튜닝은 필수로 진행되어야 하고 그래도 발생하는 한계를 보조하는게 캐시라고 생각합니다. 캐시를 도입하려는 개발자분들이 이글을 보시고 도움이 되길 바랍니다. 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유