Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Redis 글로벌 캐싱으로 SQL 데이터베이스 성능 극대화하기

소프트웨어 애플리케이션의 가치는 고객의 요구를 얼마나 잘 충족하느냐에 달려 있습니다. 고객의 니즈를 살펴보면 가장 먼저 마주치는 요구사항은 바로 애플리케이션의 속도데이터 신뢰성입니다. 그러나 애플리케이션이 성장하고 전 세계로 확장될수록, SQL 데이터베이스는 쿼리량 증가, 높아지는 지연 시간, 지역적으로 분산된 사용자 기반 등으로 인해 성능 병목 지점이 되곤 합니다.

애플리케이션이 커질 때 이러한 문제를 해결하는 첫 번째 방법 중 하나가 바로 캐싱입니다. 캐싱은 반복적인 쿼리로 인한 기본 데이터베이스의 부하를 줄이고, 사용자가 쿼리를 보낼 때 발생하는 지연 시간을 단축해 줍니다. 캐시라고 하면 모두에게 공통적으로 떠오르는 도구가 있죠? 네, 바로 Redis입니다. Redis는 데이터를 캐싱하여 부하를 줄이고 애플리케이션을 더 빠르게 만들어 주는 완벽한 도구입니다. 특히 Upstash는 전 세계에 분산된 Redis 복제(replication) 기능을 제공하여 일반적인 캐시보다 훨씬 빠른 애플리케이션 구현을 가능하게 합니다.

이번 글에서는 글로벌 Redis와 SQL 데이터베이스를 통합했을 때 얻을 수 있는 기술적 장점을 살펴보고, 지연 시간과 확장성에 미치는 영향을 논의하며, PostgreSQL과 MySQL과 함께 글로벌 Redis를 사용하는 실전 예제까지 소개합니다.

캐싱의 이점

먼저 왜 캐싱을 사용해야 하는지 알아보겠습니다.

캐싱에는 크게 두 가지 핵심 이점이 있습니다. 바로 데이터베이스 부하 감소사용자 대상 지연 시간 감소입니다.

데이터베이스 부하 감소

SQL 데이터베이스는 구조화된 데이터와 복잡한 쿼리 처리에 탁월하지만, 부하가 심해지면 병목 현상이 발생할 수 있습니다. 상품 상세 정보, 사용자 프로필, 자주 조회되는 설정값처럼 동일한 데이터에 대한 반복 쿼리가 많아지면 CPU와 I/O 자원이 상당히 소모됩니다. 이런 결과값들을 캐싱하면 데이터베이스에 도달하는 쿼리 수를 획기적으로 줄일 수 있고, 데이터베이스는 트랜잭션 처리나 업데이트 같은 더 중요한 작업에 집중할 수 있게 됩니다.

예를 들면 다음과 같습니다.

  • 캐싱 없이: 웹사이트의 인기 기능 하나가 하루에 수백만 건의 동일한 데이터베이스 쿼리를 생성하여 다른 작업의 성능을 저하시킵니다.
  • 캐싱 적용 시: 자주 접근되는 쿼리 결과를 Redis 같은 고속 캐시에 저장하여 데이터베이스 쿼리 비율을 90% 이상 줄일 수 있습니다.

지연 시간 감소

데이터베이스 부하를 줄이는 것은 데이터 신뢰성을 보장하기 위한 시스템 안정화 차원의 이점입니다. 캐싱은 또한 사용자 요청과 쿼리 처리에 걸리는 시간도 단축시켜 줍니다.

애플리케이션이 동일한 데이터를 반복해서 데이터베이스에 질의하면 데이터 조회에 항상 오랜 시간이 걸립니다. 특히 이러한 쿼리들이 데이터베이스에 큰 부하를 만들 경우 지연은 더욱 심해집니다. 이는 실시간 데이터 접근이 필요하거나 대량의 쿼리를 처리하는 애플리케이션에서 특히 문제가 됩니다. 작은 지연이라도 성능에 큰 영향을 미칠 수 있기 때문입니다.

캐싱은 자주 접근되는 데이터를 메모리에 저장함으로써 이 문제를 해결합니다. 메모리는 데이터베이스를 조회하는 것보다 훨씬 빠릅니다. 반복 요청에 대한 데이터베이스 쿼리 의존도를 줄임으로써 네트워크 이동 거리를 최소화하고 복잡한 쿼리 실행에 따른 연산 오버헤드도 피할 수 있습니다. 그 결과 응답 시간이 크게 개선되어, 애플리케이션이 무거운 부하나 분산 환경에서도 빠르고 일관된 성능을 제공할 수 있습니다.

주요 캐싱 전략

애플리케이션 성능 최적화에서 사용되는 두 가지 대표적인 캐싱 전략, 즉 캐시 어사이드(Cache-aside)라이트 스루(Write-through)를 이해하는 것도 중요합니다. 각 방식은 애플리케이션의 요구사항에 따라 고유한 사용 사례와 트레이드오프를 가지고 있습니다.

캐시 어사이드는 가장 널리 사용되는 캐싱 기법입니다. 이 방식에서는 애플리케이션이 먼저 캐시에서 데이터를 확인합니다. 데이터가 캐시에 없다면(캐시 미스) 데이터베이스에서 데이터를 가져와 향후 사용을 위해 캐시에 저장합니다.

아래는 캐시 어사이드의 동작 방식을 보여주는 간단한 다이어그램으로, 이전에 어딘가에서 이미 보신 적이 있을 수도 있습니다.

Redis 글로벌 캐싱으로 SQL 데이터베이스 성능 극대화하기

이 캐싱 방식의 장점은 캐시 크기가 최적화되고, 필요할 때 사용자 요청에 따라 캐시 데이터가 다시 로드된다는 점입니다. 반면 단점은 TTL(Time To Live)이 만료되어 캐시가 삭제되면 해당 데이터를 즉시 사용할 수 없다는 것입니다. 이때 사용자가 데이터를 요청하면 캐시가 다시 로드되는데, 이 경우 해당 사용자는 쿼리가 완료될 때까지 기다려야 합니다. 물론 이후의 요청들은 다시 캐시에서 데이터를 받을 수 있습니다.

라이트 스루 전략에서는 데이터베이스에 대한 모든 쓰기 작업이 즉시 캐시에도 기록됩니다. 이를 통해 캐시는 항상 데이터베이스의 최신 데이터와 동기화 상태를 유지합니다.

아래는 라이트 스루 캐싱의 간단한 다이어그램입니다.

Redis 글로벌 캐싱으로 SQL 데이터베이스 성능 극대화하기

이 전략은 캐시와 데이터베이스 간의 데이터 일관성을 보장하며, 데이터가 이미 준비되어 있어 사용자 요청을 기다릴 필요가 없으므로 어떤 요청도 추가 지연을 경험하지 않습니다. 다만 단점은 필요 여부와 관계없이 모든 데이터가 캐시에 저장된다는 점입니다. 또한 모든 쓰기 작업마다 캐시에도 데이터를 기록해야 하므로 쓰기 지연 시간이 늘어난다는 점도 고려해야 합니다.

글로벌 Redis란? 글로벌 Redis의 이점

이제 SQL 데이터베이스의 성능을 한층 더 끌어올리는 방법을 살펴보겠습니다.

지연 시간의 또 다른 원인은 데이터베이스의 위치입니다. 대부분의 경우 기본(primary) 데이터베이스는 특정 리전에 위치해 있습니다. 하지만 데이터 저장소까지의 거리로 인한 지연을 최소화하려면 데이터에 가장 가까운 위치에서 접근할 수 있어야 합니다.

이 문제는 Upstash가 제공하는 글로벌 분산 Redis로 해결할 수 있습니다.

글로벌 Redis는 여러 지리적 위치에 데이터를 복제하여, 전 세계에 분산된 애플리케이션에도 낮은 지연 시간으로 접근할 수 있게 해주는 분산 캐싱 솔루션입니다.

글로벌 Redis를 만드는 과정을 간단히 살펴보겠습니다. 먼저 Upstash 콘솔에 로그인합니다.

로그인 후 이곳에서 Redis 데이터베이스를 생성할 수 있습니다. Upstash는 읽기 복제본(read replica)을 배치할 수 있는 다양한 리전을 제공합니다.

Redis 글로벌 캐싱으로 SQL 데이터베이스 성능 극대화하기

읽기 위치를 선택한 후 플랜을 선택하고 최종적으로 Redis 데이터베이스를 생성하면 끝입니다!

또한 콘솔에서는 데이터베이스 생성 이후에도 리전을 추가하거나 제거할 수 있습니다.

Redis 글로벌 캐싱으로 SQL 데이터베이스 성능 극대화하기

글로벌 Redis 데이터베이스는 주로 전 세계에 분산 배포된 애플리케이션이나 엣지(edge)에서 실행되는 애플리케이션에서 활용됩니다.

글로벌 분산 애플리케이션을 위한 저지연

글로벌 분산 시스템에서는 사용자와 중앙 데이터베이스 또는 캐시 사이의 물리적 거리 때문에 지연 시간이 병목이 되는 경우가 많습니다. 글로벌 Redis는 여러 지역에 분산된 노드에 데이터를 복제함으로써 이 문제를 해결합니다.

사용자가 데이터를 요청하면 가장 가까운 캐시 노드가 요청을 처리하여 네트워크 이동 시간을 획기적으로 줄입니다. 이러한 로컬화된 접근은 사용자의 위치와 관계없이 빠른 응답 시간과 일관된 사용자 경험을 보장합니다.

예를 들어, 사용자가 도쿄에 있지만 데이터베이스가 더블린에 있다면 물리적 거리로 인해 응답이 지연됩니다. 하지만 유럽에 Upstash Redis 읽기 복제본이 있다면, 요청은 가장 가까운 읽기 복제본, 즉 이 경우 유럽의 복제본으로 라우팅될 수 있습니다.

엣지 런타임(Cloudflare Workers 등)을 위한 저지연 데이터

엣지 런타임은 최종 사용자와 가까운 네트워크 엣지에서 코드를 실행하도록 설계된 환경입니다. 엣지 런타임은 애플리케이션 로직을 전 세계 여러 엣지 위치에 분산 배포합니다. 이 아키텍처는 사용자와 요청 실행 지점 사이의 물리적 거리를 최소화하여 지연 시간을 크게 줄이고 성능을 향상시킵니다.

엣지 런타임이 연산을 사용자 가까이 가져오더라도, 대부분의 작업에서는 사용자별 정보, 세션 토큰, 설정값 등의 데이터 접근이 여전히 필요합니다. 캐싱 계층이 없다면 각 요청마다 중앙 데이터베이스로의 왕복 통신이 발생하여 지연 시간 개선 효과가 상당 부분 상쇄됩니다. 바로 이 지점에서 글로벌 Redis가 중요한 역할을 합니다. 자주 사용되는 데이터를 엣지로 복제하여 낮은 지연 시간으로 접근할 수 있게 해주기 때문입니다.

예제 코드 1: Node.js와 PostgreSQL

글로벌 Redis를 활용한 캐싱은 정말 강력합니다. 이제 Upstash Redis와 PostgreSQL 데이터베이스로 캐시 어사이드 전략을 구현한 코드 샘플을 살펴보겠습니다.

먼저 데이터 스토어에 연결하기 위해 사용할 SDK를 설치해야 합니다.

npm install pg upstash/redis

의존성 설치가 완료되면 데이터 스토어인 Upstash Redis와 Postgres에 연결할 수 있습니다.

const { Redis } = require('@upstash/redis'); // Upstash Redis SDK
const { Client } = require('pg');
 
const redis = new Redis({
 url: <UPSTASH_REDIS_REST_URL>,
 token: <UPSTASH_REDIS_REST_TOKEN>,
})
 
const client = new Client({
 user: 'username',
 password: 'password',
 host: 'host',
 port: 'port_number',
 database: 'database_name',
});
 
client.connect();

이제 데이터 액세스 계층에 함수를 작성해 보겠습니다. 이 함수는 필요에 따라 자유롭게 수정할 수 있습니다.

예를 들어 웹사이트에서 userId로 사용자 정보를 표시하고 싶다고 가정해 봅시다. 이 경우 userId를 매개변수로 받는 함수가 필요합니다.

async function getUserData(userId) {
 // Check cache first
 const cachedData = await redis.get(userId);
 if (cachedData) {
 console.log('Cache hit');
 return JSON.parse(cachedData);
 }
 // Fallback to database
 console.log('Cache miss');
 const query = 'SELECT * FROM users WHERE id = $1';
 const { rows } = await client.query(query, [userId]);
 await redis.set(userId, JSON.stringify(rows), { EX: 300 }); // Cache for 5 minutes
 return rows;
}

완성되었습니다! 이제 이 함수는 먼저 요청자의 리전에 가장 가까운 Redis 데이터베이스에 사용자 정보가 있는지 확인합니다. 정보가 없다면 PostgreSQL 데이터베이스에서 요청된 데이터를 조회하고, 반환된 데이터를 Upstash Redis 기본 리전에 기록합니다. 기본 리전에 기록된 데이터는 모든 읽기 복제본에 자동으로 복제됩니다.

예제 코드 2: Python과 MySQL

이번에는 또 다른 예제를 살펴보겠습니다. 이번에도 동일한 캐시를 구현하지만, 메인 데이터베이스는 MySQL입니다. 그리고 파이썬 기반 애플리케이션에서 어떻게 동작하는지 보여드리기 위해 이번에는 Python으로 함수를 작성하겠습니다.

평소처럼 데이터베이스 연결에 사용할 의존성 패키지부터 설치합니다.

pip install upstash-redis upstash-redis

이제 클라이언트를 초기화하고 연결할 수 있습니다.

import upstash_redis
import mysql.connector
import json
 
# Initialize Upstash Redis client
redis_client = upstash_redis.Redis(
 url='<your-upstash-redis-url>',
 token='<your-upstash-token>'
)
 
# Initialize MySQL client
db = mysql.connector.connect(
 host="<your-mysql-host>",
 user="<your-mysql-user>",
 password="<your-mysql-password>",
 database="<your-database-name>"
)
 
cursor = db.cursor(dictionary=True)

연결이 준비되었습니다. 이제 앞서 구현한 것과 유사한 함수를 작성해 보겠습니다.

def get_user_data(userId):
 # Check the cache for the data
 cache_data = redis_client.get(key)
 if cache_data:
 print("Cache hit")
 return json.loads(cache_data)
 
 # If cache miss, query the MySQL database
 print("Cache miss")
 cursor.execute("SELECT * FROM users WHERE key = %s", (userId))
 result = cursor.fetchone()
 if result:
 # Store the data in the cache with a TTL of 1 hour
 redis_client.set(key, json.dumps(result), ex=3600)
 return result

결론

아키텍처에 글로벌 Redis를 통합하면 SQL 기반 애플리케이션, 특히 전 세계에 분산된 환경에서 상당한 성능 향상을 얻을 수 있습니다. 낮은 지연 시간, 감소된 데이터베이스 부하, 엣지 런타임과의 호환성 덕분에 글로벌 Redis는 SQL 데이터베이스를 사용하는 애플리케이션의 성능 문제를 효과적으로 해결할 수 있습니다.

이번 포스트에서는 글로벌 Redis를 활용한 캐싱의 이점을 살펴보고 몇 가지 예제도 함께 확인했습니다. 소개드린 내용은 기본적인 예제일 뿐이며, 필요에 따라 더욱 확장할 수 있습니다.

이 글이 글로벌 Redis의 강력한 기능을 활용하기 위한 좋은 출발점이 되기를 바랍니다.