수요일

Redis WRONGTYPE 에러: 데이터 타입 불일치 해결

🔍 검색 키워드: Redis WRONGTYPE 에러, Redis 데이터 타입, Redis 명령어 호환성, INCR 문자열, Redis 키 재사용

Redis WRONGTYPE 에러: 데이터 타입 불일치 해결

Redis는 강타입 데이터베이스입니다. 같은 키에 다른 타입의 데이터를 저장하려고 하면 WRONGTYPE 에러가 발생합니다. 흔히 개발 중에 같은 키명을 여러 목적으로 재사용할 때 마주치는 실수입니다. 이 글에서는 에러 원인과 해결 방법을 실제 상황으로 설명하겠습니다.

1. 증상: WRONGTYPE 에러 메시지

ERR WRONGTYPE Operation against a key holding the wrong kind of value
  at Object.writeError (/app/node_modules/redis/lib/reply.js:71:15)
  at Socket.onreply (/app/node_modules/redis/lib/index.js:304:17)

또는 Redis CLI에서:

> set user:1 "{name: john}"
  OK
  > lpush user:1 "new_value"
  (error) WRONGTYPE Operation against a key holding the wrong kind of value

2. 원인 분석

Redis는 5가지 기본 데이터 타입을 가집니다:

  • String: set, get, incr
  • List: lpush, rpush, lrange
  • Set: sadd, scard, smembers
  • Hash: hset, hget, hgetall
  • Sorted Set: zadd, zscore, zrange

원인 1: 같은 키를 다른 타입으로 사용

// 기존 코드: user:1을 String으로 사용
  await redis.set('user:1', JSON.stringify({ name: 'john' }));
  
  // 신규 코드: 같은 키를 List로 변경
  await redis.lpush('user:1', 'new_value');
  // ❌ WRONGTYPE 에러!

원인 2: 개발 중 타입 변경

// 처음에는 값 저장 (String)
  redis.set('counter', '10');
  
  // 나중에 증가시키려고 시도 (INCR은 String 전용)
  redis.incr('counter');  // 기존 값이 JSON이면 에러
  
  // 또는 반대로
  redis.lpush('counter', 1);  // 이제 List
  redis.incr('counter');       // WRONGTYPE!

원인 3: TTL 만료 후 타입 재사용

redis.set('session:abc', 'token123', 'EX', 3600);  // String으로 저장
  
  // TTL이 지나 자동 삭제됨
  // 몇 시간 후...
  
  redis.hset('session:abc', 'user_id', '123');  // Hash로 저장 시도
  // 만약 키가 남아있으면 WRONGTYPE!

3. 해결 방법

방법 1: 기존 키 삭제 후 재생성

// 안전한 재설정
  await redis.del('user:1');
  await redis.lpush('user:1', 'value1', 'value2');

방법 2: 다른 키명 사용

// AS-IS: 충돌하는 키
  const key = 'user:1';
  
  // TO-BE: 타입 명시하는 키명
  const userKey = 'user:1:string';      // String용
  const userListKey = 'user:1:list';    // List용
  const userHashKey = 'user:1:hash';    // Hash용
  
  // 또는 버전 붙이기
  const userKeyV2 = 'user:1:v2';

방법 3: 타입 확인 후 처리

// Node.js Redis 클라이언트
  async function safeSet(key, value, type = 'string') {
    const existingType = await redis.type(key);
    
      if (existingType !== 'none' && existingType !== type) {
          console.warn(`키 '${key}'의 기존 타입: ${existingType}, 요청 타입: ${type}`);
              await redis.del(key);  // 기존 데이터 삭제
                }
                
                  if (type === 'string') {
                      await redis.set(key, value);
                        } else if (type === 'list') {
                            await redis.lpush(key, value);
                              } else if (type === 'hash') {
                                  await redis.hset(key, ...Object.entries(value).flat());
                                    }
                                    }
                                    
                                    // 사용
                                    await safeSet('user:1', { name: 'john' }, 'hash');

방법 4: Redis에서 타입 조회

# 기존 모든 키와 타입 확인
  $ redis-cli KEYS '*' | while read key; do echo -n "$key: "; redis-cli TYPE "$key"; done
  
  # 특정 패턴 확인
  $ redis-cli SCAN 0 MATCH "user:*" | xargs -I {} redis-cli TYPE {}

방법 5: 마이그레이션 스크립트

// 전체 키를 새 구조로 마이그레이션
  const redis = require('redis');
  const client = redis.createClient();
  
  async function migrateKeys() {
    const keys = await new Promise((resolve, reject) => {
        let allKeys = [];
            const stream = client.scanStream();
                stream.on('data', (keys) => allKeys.push(...keys));
                    stream.on('end', () => resolve(allKeys));
                        stream.on('error', reject);
                          });
                          
                            for (const key of keys) {
                                if (key.startsWith('user:')) {
                                      const type = await client.type(key);
                                      
                                            if (type === 'string') {
                                                    const value = await client.get(key);
                                                            await client.del(key);
                                                                    await client.hset(`${key}:v2`, 'data', value);
                                                                          }
                                                                                // 다른 타입도 처리...
                                                                                    }
                                                                                      }
                                                                                      
                                                                                        console.log('마이그레이션 완료');
                                                                                        }
                                                                                        
                                                                                        await migrateKeys();

4. 타입별 명령어 정리

명령어 대상 타입 예제
set / get String set key value
lpush / lrange List lpush list_key value
sadd / smembers Set sadd set_key member
hset / hget Hash hset hash_key field value
zadd / zrange Sorted Set zadd zset_key 1 member
incr String만 incr counter
lpop List만 lpop list_key

5. 예방 가이드

개발 단계:

  • 키 설계 시 타입 명시: {domain}:{id}:{type}
  • 예: user:123:string, session:456:hash, queue:789:list

테스트 단계:

  • Redis 모의 객체로 타입 검증
  • 개발/스테이징 Redis 정기 초기화

배포 단계:

  • 마이그레이션 스크립트로 기존 키 처리
  • TTL 만료 후 재사용 금지 정책 수립

WRONGTYPE 에러는 Redis를 처음 다룰 때 흔한 실수입니다. 핵심은 같은 키에는 하나의 타입만 유지하는 것. 타입을 바꿔야 한다면 새 키를 사용하거나, 기존 데이터를 명시적으로 삭제하세요. 이를 통해 안정적인 Redis 운영이 가능합니다.

댓글 없음:

댓글 쓰기