Skip to content

NOTE

Bun 의 Redis 클라이언트는 Redis 서버 버전 7.2 이상을 지원합니다.

Bun 은 현대적인 Promise 기반 API 를 사용하여 Redis 데이터베이스를 작업하기 위한 네이티브 바인딩을 제공합니다. 이 인터페이스는 간단하고 성능이 뛰어나도록 설계되었으며, 내장 연결 관리, 완전히 타입 지정된 응답, TLS 지원을 제공합니다.

ts
import { redis } from "bun";

// 키 설정
await redis.set("greeting", "Hello from Bun!");

// 키 가져오기
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"

// 카운터 증가
await redis.set("counter", 0);
await redis.incr("counter");

// 키 존재 여부 확인
const exists = await redis.exists("greeting");

// 키 삭제
await redis.del("greeting");

시작하기

Redis 클라이언트를 사용하려면 먼저 연결을 생성해야 합니다:

ts
import { redis, RedisClient } from "bun";

// 기본 클라이언트 사용 (환경에서 연결 정보 읽기)
// 기본적으로 process.env.REDIS_URL 가 사용됨
await redis.set("hello", "world");
const result = await redis.get("hello");

// 커스텀 클라이언트 생성
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");

기본적으로 클라이언트는 다음 환경 변수에서 연결 정보를 읽습니다 (우선순위 순):

  • REDIS_URL
  • VALKEY_URL
  • 설정되지 않은 경우 기본적으로 "redis://localhost:6379"로 설정됨

연결 라이프사이클

Redis 클라이언트는 백그라운드에서 연결을 자동으로 처리합니다:

ts
// 명령이 실행될 때까지 연결되지 않음
const client = new RedisClient();

// 첫 명령으로 연결 시작
await client.set("key", "value");

// 후속 명령을 위해 연결 유지
await client.get("key");

// 완료 시 명시적으로 연결 닫기
client.close();

연결 라이프사이클을 수동으로 제어할 수도 있습니다:

ts
const client = new RedisClient();

// 명시적으로 연결
await client.connect();

// 명령 실행
await client.set("key", "value");

// 완료 시 연결 끊기
client.close();

기본 연산

문자열 연산

ts
// 키 설정
await redis.set("user:1:name", "Alice");

// 키 가져오기
const name = await redis.get("user:1:name");

// Uint8Array 로 키 가져오기
const buffer = await redis.getBuffer("user:1:name");

// 키 삭제
await redis.del("user:1:name");

// 키 존재 여부 확인
const exists = await redis.exists("user:1:name");

// 만료 설정 (초 단위)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // 1 시간 후 만료

// 잔여 수명 가져오기 (초 단위)
const ttl = await redis.ttl("session:123");

숫자 연산

ts
// 초기값 설정
await redis.set("counter", "0");

// 1 증가
await redis.incr("counter");

// 1 감소
await redis.decr("counter");

해시 연산

ts
// 해시에 여러 필드 설정
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com", "active", "true"]);

// 해시에서 여러 필드 가져오기
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]

// 해시에서 단일 필드 가져오기 (값 직접 반환, 없으면 null)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"

// 해시에서 숫자 필드 증가
await redis.hincrby("user:123", "visits", 1);

// 해시에서 float 필드 증가
await redis.hincrbyfloat("user:123", "score", 1.5);

집합 연산

ts
// 집합에 멤버 추가
await redis.sadd("tags", "javascript");

// 집합에서 멤버 제거
await redis.srem("tags", "javascript");

// 집합에 멤버 존재 여부 확인
const isMember = await redis.sismember("tags", "javascript");

// 집합의 모든 멤버 가져오기
const allTags = await redis.smembers("tags");

// 랜덤 멤버 가져오기
const randomTag = await redis.srandmember("tags");

// 랜덤 멤버 팝 (제거 및 반환)
const poppedTag = await redis.spop("tags");

Pub/Sub

Bun 은 Redis Pub/Sub 프로토콜에 대한 네이티브 바인딩을 제공합니다. Bun 1.2.23 에서 새로 추가됨

기본 사용법

메시지 게시를 시작하려면 publisher.ts 에서 게시자를 설정할 수 있습니다:

typescript
import { RedisClient } from "bun";

const writer = new RedisClient("redis://localhost:6739");
await writer.connect();

writer.publish("general", "Hello everyone!");

writer.close();

다른 파일에서 subscriber.ts 에 구독자를 생성합니다:

typescript
import { RedisClient } from "bun";

const listener = new RedisClient("redis://localhost:6739");
await listener.connect();

await listener.subscribe("general", (message, channel) => {
  console.log(`Received: ${message}`);
});

한 셸에서 구독자를 실행합니다:

bash
bun run subscriber.ts

그리고 다른 셸에서 게시자를 실행합니다:

bash
bun run publisher.ts

NOTE

구독 모드는 `RedisClient` 연결을 차지합니다. 구독이 있는 클라이언트는 `RedisClient.prototype.subscribe()` 만 호출할 수 있습니다. 즉, Redis 에 메시지를 보내야 하는 애플리케이션은 별도의 연결이 필요하며, `.duplicate()` 를 통해 얻을 수 있습니다:
ts
import { RedisClient } from "bun";

const redis = new RedisClient("redis://localhost:6379");
await redis.connect();
const subscriber = await redis.duplicate(); 

await subscriber.subscribe("foo", () => {});
await redis.set("bar", "baz");

게시

메시지 게시는 publish() 메서드를 통해 수행됩니다:

typescript
await client.publish(channelName, message);

구독

Bun RedisClient.subscribe() 메서드를 통해 채널을 구독할 수 있습니다:

typescript
await client.subscribe(channel, (message, channel) => {});

.unsubscribe() 메서드를 통해 구독을 해제할 수 있습니다:

typescript
await client.unsubscribe(); // 모든 채널에서 구독 해제.
await client.unsubscribe(channel); // 특정 채널 구독 해제.
await client.unsubscribe(channel, listener); // 특정 리스너 구독 해제.

고급 사용법

명령 실행 및 파이프라이닝

클라이언트는 자동으로 명령을 파이프라이닝하여 여러 명령을 배치로 보내고 응답이 도착하는 대로 처리하여 성능을 향상시킵니다.

ts
// 명령은 기본적으로 자동으로 파이프라이닝됨
const [infoResult, listResult] = await Promise.all([redis.get("user:1:name"), redis.get("user:2:email")]);

자동 파이프라이닝을 비활성화하려면 enableAutoPipelining 옵션을 false 로 설정할 수 있습니다:

ts
const client = new RedisClient("redis://localhost:6379", {
  enableAutoPipelining: false, 
});

원시 명령

편의 메서드가 없는 명령을 사용해야 할 때는 send 메서드를 사용할 수 있습니다:

ts
// 모든 Redis 명령 실행
const info = await redis.send("INFO", []);

// 리스트에 LPUSH
await redis.send("LPUSH", ["mylist", "value1", "value2"]);

// 리스트 범위 가져오기
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);

send 메서드를 사용하면 클라이언트에 전용 메서드가 없는 명령을 포함한 모든 Redis 명령을 사용할 수 있습니다. 첫 번째 인수는 명령 이름이고 두 번째 인수는 문자열 인수 배열입니다.

연결 이벤트

연결 이벤트에 대한 핸들러를 등록할 수 있습니다:

ts
const client = new RedisClient();

// Redis 서버에 성공적으로 연결되면 호출됨
client.onconnect = () => {
  console.log("Redis 서버에 연결됨");
};

// Redis 서버에서 연결이 끊기면 호출됨
client.onclose = error => {
  console.error("Redis 서버에서 연결 끊김:", error);
};

// 수동으로 연결/연결 끊기
await client.connect();
client.close();

연결 상태 및 모니터링

ts
// 연결 여부 확인
console.log(client.connected); // 연결 상태를 나타내는 부울

// 버퍼링된 데이터 양 확인 (바이트 단위)
console.log(client.bufferedAmount);

타입 변환

Redis 클라이언트는 Redis 응답에 대한 자동 타입 변환을 처리합니다:

  • 정수 응답은 JavaScript 숫자로 반환됨
  • 벌크 문자열은 JavaScript 문자열로 반환됨
  • 심플 문자열은 JavaScript 문자열로 반환됨
  • null 벌크 문자열은 null 로 반환됨
  • 배열 응답은 JavaScript 배열로 반환됨
  • 오류 응답은 적절한 오류 코드로 JavaScript 오류를 throw 함
  • 부울 응답 (RESP3) 은 JavaScript 부울로 반환됨
  • 맵 응답 (RESP3) 은 JavaScript 객체로 반환됨
  • 집합 응답 (RESP3) 은 JavaScript 배열로 반환됨

특정 명령에 대한 특별 처리:

  • EXISTS 는 숫자 대신 부울을 반환함 (1 은 true, 0 은 false)
  • SISMEMBER 는 부울을 반환함 (1 은 true, 0 은 false)

다음 명령은 자동 파이프라이닝을 비활성화합니다:

  • AUTH
  • INFO
  • QUIT
  • EXEC
  • MULTI
  • WATCH
  • SCRIPT
  • SELECT
  • CLUSTER
  • DISCARD
  • UNWATCH
  • PIPELINE
  • SUBSCRIBE
  • UNSUBSCRIBE
  • UNPSUBSCRIBE

연결 옵션

클라이언트를 생성할 때 연결을 구성하기 위한 다양한 옵션을 전달할 수 있습니다:

ts
const client = new RedisClient("redis://localhost:6379", {
  // 밀리초 단위의 연결 타임아웃 (기본값: 10000)
  connectionTimeout: 5000,

  // 밀리초 단위의 유휴 타임아웃 (기본값: 0 = 타임아웃 없음)
  idleTimeout: 30000,

  // 연결 끊김 시 자동으로 재연결 여부 (기본값: true)
  autoReconnect: true,

  // 최대 재연결 시도 횟수 (기본값: 10)
  maxRetries: 10,

  // 연결 끊김 시 명령 큐잉 여부 (기본값: true)
  enableOfflineQueue: true,

  // 명령을 자동으로 파이프라이닝 여부 (기본값: true)
  enableAutoPipelining: true,

  // TLS 옵션 (기본값: false)
  tls: true,
  // 또는 커스텀 TLS 구성 제공:
  // tls: {
  //   rejectUnauthorized: true,
  //   ca: "path/to/ca.pem",
  //   cert: "path/to/cert.pem",
  //   key: "path/to/key.pem",
  // }
});

재연결 동작

연결이 손실되면 클라이언트는 지수 백오프로 자동으로 재연결을 시도합니다:

  1. 클라이언트는 작은 지연 (50ms) 으로 시작하여 각 시도마다 두 배로 증가시킵니다
  2. 재연결 지연은 2000ms(2 초) 로 제한됨
  3. 클라이언트는 최대 maxRetries 번 (기본값: 10) 재연결을 시도합니다
  4. 연결 끊김 중 실행된 명령은:
    • enableOfflineQueue 가 true 이면 큐에 추가됨 (기본값)
    • enableOfflineQueue 가 false 이면 즉시 거부됨

지원되는 URL 형식

Redis 클라이언트는 다양한 URL 형식을 지원합니다:

ts
// 표준 Redis URL
new RedisClient("redis://localhost:6379");
new RedisClient("redis://localhost:6379");

// 인증 포함
new RedisClient("redis://username:password@localhost:6379");

// 데이터베이스 번호 포함
new RedisClient("redis://localhost:6379/0");

// TLS 연결
new RedisClient("rediss://localhost:6379");
new RedisClient("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
new RedisClient("redis+tls://localhost:6379");

// Unix 소켓 연결
new RedisClient("redis+unix:///path/to/socket");
new RedisClient("redis+unix:///path/to/socket");

// Unix 소켓 위의 TLS
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");

오류 처리

Redis 클라이언트는 다양한 시나리오에 대해 타입 지정된 오류를 throw 합니다:

ts
try {
  await redis.get("non-existent-key");
} catch (error) {
  if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
    console.error("Redis 서버와의 연결이 닫혔습니다");
  } else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
    console.error("인증에 실패했습니다");
  } else {
    console.error("예상치 못한 오류:", error);
  }
}

일반적인 오류 코드:

  • ERR_REDIS_CONNECTION_CLOSED - 서버와의 연결이 닫혔음
  • ERR_REDIS_AUTHENTICATION_FAILED - 서버 인증에 실패함
  • ERR_REDIS_INVALID_RESPONSE - 서버로부터 잘못된 응답을 받음

예제 사용 사례

캐싱

ts
async function getUserWithCache(userId) {
  const cacheKey = `user:${userId}`;

  // 먼저 캐시에서 가져오기 시도
  const cachedUser = await redis.get(cacheKey);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }

  // 캐시에 없음, 데이터베이스에서 가져오기
  const user = await database.getUser(userId);

  // 1 시간 동안 캐시에 저장
  await redis.set(cacheKey, JSON.stringify(user));
  await redis.expire(cacheKey, 3600);

  return user;
}

속도 제한

ts
async function rateLimit(ip, limit = 100, windowSecs = 3600) {
  const key = `ratelimit:${ip}`;

  // 카운터 증가
  const count = await redis.incr(key);

  // 이 창의 첫 요청이면 만료 설정
  if (count === 1) {
    await redis.expire(key, windowSecs);
  }

  // 제한 초과 여부 확인
  return {
    limited: count > limit,
    remaining: Math.max(0, limit - count),
  };
}

세션 스토리지

ts
async function createSession(userId, data) {
  const sessionId = crypto.randomUUID();
  const key = `session:${sessionId}`;

  // 만료와 함께 세션 저장
  await redis.hmset(key, ["userId", userId.toString(), "created", Date.now().toString(), "data", JSON.stringify(data)]);
  await redis.expire(key, 86400); // 24 시간

  return sessionId;
}

async function getSession(sessionId) {
  const key = `session:${sessionId}`;

  // 세션 데이터 가져오기
  const exists = await redis.exists(key);
  if (!exists) return null;

  const [userId, created, data] = await redis.hmget(key, ["userId", "created", "data"]);

  return {
    userId: Number(userId),
    created: Number(created),
    data: JSON.parse(data),
  };
}

구현 노트

Bun 의 Redis 클라이언트는 Zig 로 구현되었으며 Redis Serialization Protocol (RESP3) 을 사용합니다. 연결을 효율적으로 관리하며 지수 백오프로 자동 재연결을 제공합니다.

클라이언트는 명령 파이프라이닝을 지원하므로 이전 명령의 응답을 기다리지 않고 여러 명령을 보낼 수 있습니다. 이는 연속으로 여러 명령을 보낼 때 성능을 크게 향상시킵니다.

제한 사항 및 향후 계획

향후 버전에서 해결할 계획인 Redis 클라이언트의 현재 제한 사항:

  • 트랜잭션 (MULTI/EXEC) 은 현재 원시 명령을 통해 수행해야 함

지원되지 않는 기능:

  • Redis Sentinel
  • Redis Cluster

Bun by www.bunjs.com.cn 편집