API 성능개선 : Redis Cache를 적용하여 Read API 기능을 개선해보자


이 글은 당근마켓을 모티브로 한 프로젝트 Secondhand 구현시 이슈 사항을 정리한 글입니다.

사실 처음 시작은 조회수 구현을 어떻게 효율적으로 할 수 있을까에서 출발하였습니다. [조회수 관련해서 참고한 블로그 글] 세션별로 중복되지 않게 일정 시간동안 1회씩 카운팅하고 싶은 욕심이었습니다.

그런데 학습을 하다보니 레디스 Cache를 읽기 API에서도 사용해보고 싶더라고요🔥 API 성능을 개선해본 뒤 차근차근 조회수도 개선해보도록 하겠습니다.

Cache에 대하여

캐시 Cache는 자주 사용하는 연산에 대하여 속도가 빠른 임시 공간에 저장해두어 애플리케이션 연산 속도를 높일 수 있습니다.

Cache는 나중에 요청을 위해 결과를 미리 저장해두었다가 빠르게 서비스 해주는 것을 의미합니다.
- 우아한레디스 (강대명)

애플리케이션이 Database(Disk)에 접속하여 데이터를 읽어오는 것보다는 자주 읽는 데이터를 애플리케이션이 빠르게 접근할 수 있는 메모리에 올려두고 빠르게 가져와 사용하는 것이지요. 창고에 있는 데이터를 책상에 올려두는 것과 같습니다.

비단 읽는 것뿐만 아니라 쓰기작업 혹은 연산 작업에도 적용됩니다. 다이나믹 프로그래밍을 생각해보세요. 중간 연산 결과를 계속 캐싱하여 다음 연산을 실행한다면 매번 처음부터 연산을 하는 것보다 훨씬 빠를 겁니다.

Lightbox

이 글에서는 Redis를 이용해서 cache를 구현합니다. 하지만 캐시 구현에 가장 손쉬운 방법은 ConcurrentHashMap 과 같은 자료구조를 이용해 로컬 인메모리로 구현하는 것일 수 있습니다.

Cache 와 Buffer 뭐가 다르지?

대부분 두 용어의 차이점을 정확하게 알지 못하기 때문에 종종 이 용어를 횬용해서 쓰기도 해서 간단하게 짚고 넘어가겠습니다. (제 얘기)

사실 엄밀히 말하자면 다른 의미를 갖고 있습니다.

공통점

캐시와 버퍼는 둘 다 데이터를 임시로 저장하는 데 사용됩니다.

차이점

Buffer는 전통적으로 빠른 속도의 장치와 느린 속도의 장치 사이에서 데이터를 일시적으로 보관하는 데 사용합니다. 빠른 속도의 장치에서 느린 속도의 장치로 데이터를 보낼 때 데이터의 유실, 손실 상황을 막기 위함입니다. 느린 속도의 장치가 이 버퍼 데이터를 받을 때는 한번에 받도록 하여 이 속도 차이를 완화시킵니다. 그러다보니 데이터는 버퍼에서 한 번만 쓰거나 읽을 수 있습니다.

예를 들어 컴퓨터에서 입력장치인 키보드의 데이터를 CPU가 받는다고 할 때, 키보드 버퍼에 데이터를 잠시 저장하거나 네트워킹에서 다른 장치로 데이터가 이동할 때 잠시 저장해두는 용도로 사용할 수 있습니다.

Cache 의 경우, 성능 향상에 목적이 있습니다. 캐시는 자주 사용하는 정보, 데이터(값비싼 연산결과나 자주 참조되는 데이터)를 메모리에 올려두고 사용할 수 있는 저장소입니다.

디스크 I/O보다 캐시 메모리에 올라간 데이터를 읽는 것이 훨씬 빠르기 때문입니다.

버퍼와 달리 데이터가 한번 읽고 쓰는 것으로 끝나지 않고, 일정 시간동안 계속 있으면서 반복적인 작업에 사용됩니다.

마치 책상 위에 자주 쓰는 필기구와 노트를 가져다 놓는 것처럼요. 서랍에 있으면 매번 꺼내쓰는 데 번거롭겠죠?

pen near black lined paper and eyeglasses
Photo by Jess Bailey / Unsplash

언제 사용해야 좋을까요?

캐시는 애플리케이션 전반에서 사용할 수 있습니다.

만약 캐시가 없는 애플리케이션 서버가 동일한 API 요청을 N번 받으면 매번 DB 조회 후에 모든 로직을 거쳐 반환하는 과정을 겪어야 겠지요.

대표적인 사용 예시

캐시의 장점은 데이터베이스에 직접 조회하는 것보다 빠른 것뿐만 아니라 데이터베이스의 부하를 줄일 수 있다는 점도 있습니다. 같은 API로직을 10번 요청받았을 때, 최초 1회만 DB에 접근하​면 되니까요.

캐시에 유리한 데이터

캐시에 들어갈 데이터는 자주 사용되지만 변경은 자주 일어나지 않는 것이 유리합니다.

예를 들어 채팅로그와 같은 데이터는 자주 바뀌기 때문에 사실 캐싱하기에는 별로 좋지 않은 데이터이지요. 물론 어떻게 구현하느냐, 어떤 로직이 필요하느냐에 따라 선택의 결과는 다를 수 있습니다.

아무것도 모를 때 채팅을 캐싱해보겠다며 팀 메이트와 그려본 플로우 차트 👀

다만 조회는 잦으면서 변함이 별로 없는 캐시라면 TTL을 길게해서 메모리에 올려두면 더 좋겠죠?

캐시를 사용하기 전에 선택할 것들

이처럼 캐시를 이용하여 DB 커넥션을 줄이고 서비스 로직 실행도 생략할 수가 있어서 DB와 애플리케이션 성능에 크게 도움이 되는데요. 캐시를 도입하기 전에 선택해야 하는 문제들이 몇가지 있습니다.

Local cache와 Global Cache

캐시를 WAS(Web Application Server) 에 저장하는 방식인 Local cache와 별도 캐시 서버를 구축하는 Global Cache 방식 두가지가 있는데요. 👉 Local cache 참고링크

이번 프로젝트의 경우에는 Scale out 가능성이 있다는 가정을 하고 있으며, 이미 Redis pub/sub으로 채팅 서비스를 구현한 상태이기 때문에 자연스럽게 Redis cache를 이용하여 Global Cache 를 사용하기로 하였습니다.

Local cache의 경우, WAS 인스턴스 메모리에 데이터를 저장하므로 속도가 매우 빠르고 별도 인프라를 구축할 필요가 없어서 선택하는 경우도 있습니다.

캐시 읽기/쓰기 전략과 TTL

캐시 읽기 전략과 쓰기 전략에는 여러가지 종류가 있었는데요. 데이터베이스와 캐시에서 어떻게 데이터를 가져올지에 대한 전략입니다.

캐싱할 API의 성격과 환경에 따라 선택해야 했습니다. 해당 전략에 대해서는 이 링크를 참고하였습니다.

우선적으로 캐싱 할 API로는 다음의 세가지를 염두에 두고 있었는데요.

  1. 상품판매 목록 조회
    1. 첫 페이지이므로 조회 빈도가 높은 편
    2. 여러 데이터를 조회해야하므로 쿼리가 복잡한 편
    3. 다만 데이터의 수정이 잦은 편이기 때문에 TTL을 짧게 해두는 것이 좋을 것 같음
  2. 지역 목록 조회
    1. 키워드 검색 결과 (key를 키워드를 붙여서 적용)
    2. 데이터가 변함이 없는 편
    3. TTL을 조금 길게 해두어도 좋을 것 같음

읽기 API에 적용해보자

우선적으로 캐싱 할 API로는 다음을 염두에 두고 있었는데요. 이유는 아래와 같습니다.

1. 상품판매 목록 조회

    • 첫 페이지이므로 조회 빈도가 높은 편
    • 여러 데이터를 조회해야하므로 쿼리가 복잡한 편
    • 다만 데이터의 수정이 잦은 편이기 때문에 TTL을 짧게 해두는 것이 좋을 것 같음

2. 지역 목록 조회

    • 키워드 검색 결과 (key를 키워드를 붙여서 적용)
    • 데이터가 변함이 없는 편
    • TTL을 조금 길게 해두어도 좋을 것 같음

3. 채팅 로그와 채팅방 메타 데이터

    • 속도가 중요한 도메인
    • 채팅 로그의 경우, 데이터가 계속 생성되어 쌓이는 특징을 가지고 있으므로 수정, 삭제를 신경쓰지 않아도됨
    • 쓰기 버퍼의 역할도 할 수 있을 것 같음.

막상 적용하려고 자료를 찾아보니 Spring Framework는 캐시 추상화가 되어있기 때문에 어노테이션을 기반으로 손쉽게 구현할 수 있었습니다. 이에 대한 내용은 다음 글을 통해 작성해보도록 하겠습니다.


Refs.