
1. 개요
현대적인 백엔드 시스템에서 Redis는 선택이 아닌 필수가 되었습니다. 고성능 캐싱, 세션 저장소, 메시지 브로커 등 다양한 역할을 수행하기 때문입니다. 스프링(Spring) 생태계에서는 전통적으로 Lettuce나 Jedis 드라이버를 기반으로 한 RedisTemplate을 주로 사용해왔습니다. 이는 강력하지만, 로우 레벨(Low-level)의 바이트(Byte) 조작이나 직렬화(Serialization) 설정을 개발자가 직접 신경 써야 한다는 부담이 있습니다.
이번 글에서는 이러한 복잡성을 추상화하여, 마치 "자바 컬렉션(List, Map)을 다루듯이 레디스를 사용하는" Redisson 라이브러리에 대해 알아봅니다. 특히 캐시 구현 관점에서 기존 방식과 어떤 차이가 있는지, 왜 Redisson이 '객체 지향적인 Redis 클라이언트'라고 불리는지 대조적인 코드를 통해 분석해 보겠습니다.
2. 간단한 설명
기존 방식: Lettuce와 RedisTemplate
스프링 부트(Spring Boot)의 기본 Redis 클라이언트입니다. 성능이 우수하고 비동기 처리를 지원하지만, Redis의 명령어(Command)를 거의 그대로 사용하는 방식입니다. 데이터를 저장하려면 키(Key)와 값(Value)을 바이트 배열이나 문자열로 변환(Serializer)하는 과정이 명시적으로 필요합니다. 마치 SQL을 직접 작성하여 DB에 질의하는 것과 유사한 느낌을 줍니다.
새로운 제안: Redisson
Redisson은 네티(Netty) 기반의 비동기 Redis 클라이언트입니다. 가장 큰 특징은 In-Memory Data Grid 개념을 도입하여, Redis의 자료구조를 자바의 표준 인터페이스(java.util.Map, java.util.List, java.util.concurrent.node.Lock 등)로 매핑해준다는 점입니다.
개발자는 네트워크 패킷이나 Redis 명령어를 몰라도 됩니다. 그저 Map에 put()을 하면 Redisson이 알아서 직렬화하여 Redis에 저장합니다. 분산 락(Distributed Lock)이나 로컬 캐싱(Local Caching) 같은 고급 기능도 매우 손쉽게 사용할 수 있습니다.
3. 심층 분석: "Netty 기반"이라는 것은 무엇을 의미할까?
Redisson이 빠르고 효율적인 이유는 내부적으로 Netty라는 자바 비동기 이벤트 기반 네트워크 프레임워크를 사용하기 때문입니다.
- 비동기 논블로킹 I/O (Asynchronous Non-blocking I/O):
전통적인 JDBC 같은 동기(Blocking) 방식은 Redis에 요청을 보내고 응답이 올 때까지 스레드가 아무것도 못 하고 대기해야 합니다(Thread Blocking). 반면 Netty는 요청만 보내놓고 스레드는 다른 일을 하러 갑니다. 응답이 왔을 때만 알림(Event/Callback)을 받아 처리하므로, 적은 수의 스레드로도 수천, 수만의 동시 요청을 거뜬히 처리할 수 있습니다. - 높은 처리량(Throuhgput):
이러한 구조 덕분에 Redisson은 적은 리소스(CPU, Memory)로도 폭발적인 트래픽을 감당해냅니다. 이는 대규모 트래픽이 몰리는 분산 락 구현이나, 빈번한 캐시 조회 환경에서 빛을 발합니다.
4. 사용 사례 (직렬화 및 캐시 운용 비교)
사용자 정보(UserProfile) 객체를 캐싱하고 조회하는 시나리오를 통해 두 방식의 차이를 비교해 보겠습니다.
Scenario 1: 표준적인 접근 (RedisTemplate)
RedisTemplate을 사용할 때는 먼저 적절한 Serializer를 설정해야 합니다. 그렇지 않으면 기본 자바 직렬화가 동작하여 Redis에 알 수 없는 이진 데이터가 저장되거나, JSON 변환을 위한 ObjectMapper 를 설정해야 합니다.
변경전: 설정부터 저장까지 손이 많이 가는 구조
// 1. Redis 설정 (Boilerplate Code)
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, UserProfile> userRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, UserProfile> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key는 String, Value는 JSON으로 직렬화 설정
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(UserProfile.class));
return template;
}
}
// 2. 비즈니스 로직
@Service
public class UserService {
private final RedisTemplate<String, UserProfile> redisTemplate;
public void cacheUser(String userId, UserProfile profile) {
// opsForValue()를 호출하여 저수준 연산 수행
redisTemplate.opsForValue().set("user:" + userId, profile);
}
}
이 방식은 코드가 장황해질 뿐만 아니라, List나 Set 같은 컬렉션을 다룰 때 Redis의 데이터 구조(Data Structure)에 대한 깊은 이해가 필요합니다.
Scenario 2: 객체 지향적인 접근 (Redisson)
Redisson의 정체성: In-Memory Data Grid
Redisson은 단순한 Redis 클라이언트가 아닙니다. Redis 서버를 자바 애플리케이션의 힙(Heap) 메모리 확장 공간처럼 쓰게 해주는 자바 인메모리 데이터 그리드(Java In-Memory Data Grid) 역할을 수행합니다.
- 장점 (Pros):
- 개발 생산성: 자바 표준 컬렉션 인터페이스(
Map,List,Set,Queue)를 100% 지원합니다. 레디스 명령어를 몰라도list.add("item")만 하면 됩니다. - 강력한 동시성 도구:
RLock(분산 락),RAtomicLong,RCountDownLatch등java.util.concurrent패키지의 분산 버전을 제공합니다.- RLock (Distributed Lock): 자바의
ReentrantLock과 사용법이 거의 동일한 분산 락입니다.- [실전 코드] 재고 차감 동시성 제어:
- RLock (Distributed Lock): 자바의
- 개발 생산성: 자바 표준 컬렉션 인터페이스(
// 1. 락 객체 가져오기 (이름으로 식별)
RLock lock = redissonClient.getLock("stock_lock:" + productId);
try {
// 2. 락 획득 시도 (waitTime: 10초 대기, leaseTime: 1초 후 자동 해제)
// 무한 대기(Deadlock)를 방지하기 위해 반드시 타임아웃을 설정해야 합니다.
if (lock.tryLock(10, 1, TimeUnit.SECONDS)) {
try {
// 3. 비즈니스 로직 수행 (Safe Zone)
stockService.decrease(productId, quantity);
} finally {
// 4. 안전한 잠금 해제
lock.unlock();
}
}
} catch (InterruptedException e) {
// 인터럽트 처리
}
- ReentrantLock vs RLock 비교:
- ReentrantLock: '재진입 가능한 락'이라는 뜻으로, 동일한 스레드가 이미 락을 획득했다면 횟수의 제한 없이 계속해서 락을 획득할 수 있는 자바 표준 동기화 도구입니다. 단, 단일 JVM(서버 1대) 내부에서만 유효하다는 치명적인 한계가 있어 서버가 여러 대인 분산 환경에서는 동시성을 제어할 수 없습니다.
- RLock: Redisson이 제공하는 분산 락 구현체입니다.
ReentrantLock의 사용법(API)을 그대로 따르면서도, 락의 상태를 Redis라는 외부 저장소에서 관리하므로 여러 대의 서버가 하나의 락을 공유할 수 있습니다. - Redis 구성별 RLock 데이터 저장 흐름 상세:
Redisson의 락은 Redis의 Hash 자료구조를 사용하여 관리됩니다. Hash Key는 '락 이름'이 되고, Field는 'UUID:ThreadID'가 됩니다.- 단일(Single) 모드: 가장 단순한 형태입니다.
- 특징: 단순하지만 SPOF(Single Point Of Failure) 위험이 있습니다.
- 클러스터(Cluster) 모드: 데이터가 여러 노드에 분산됩니다.
- 동작: Redisson이 키의 해시를 계산하여 해당 슬롯을 가진 Node B로만 명령을 전송합니다.
- 특징: 락 부하를 여러 노드로 분산할 수 있어 대규모 트래픽에 유리합니다.
- 센티널(Sentinel) 모드: 고가용성을 위해 복제본을 활용합니다.
- 취약점: 마스터 A가 락을 받고 Slave로 복제하기 직전에 죽으면, 새로 승격된 Slave B에는 락 정보가 없어 락 유실(Lock Loss)이 발생할 수 있습니다 (Redlock 알고리즘 필요성 대두).
- 단일(Single) 모드: 가장 단순한 형태입니다.
- 특히 스핀 락(Spin Lock) 방식이 아닌 Pub/Sub 기반의 락을 사용하여 Redis 부하를 획기적으로 줄여줍니다.
- 다양한 Codec 지원:
Jackson,Avro,Smile,Kryo등 고성능 직렬화 코덱을 설정 한 줄로 교체할 수 있습니다.
- 다양한 Codec 지원:
- Spin Lock(Lettuce): 락을 얻을 때까지 "나 들어가도 돼?"라고 Redis에게 끊임없이 물어보는 방식 (Redis CPU 부하 급증) - 대표적으로
Lettuce클라이언트 * Pub/Sub(Redisson): "나 락 필요해"라고 등록하고 대기하면, 락이 해제될 때 "너 이제 들어와!"라고 알림을 받는 방식 (효율적 대기) - 제한점 (Cons):
- 학습 곡선: 단순 문자열 캐싱만 하던 개발자에게는 오버엔지니어링(Over-engineering)으로 느껴질 수 있습니다.
- 서드파티 의존성:
jackson-dataformat등 직렬화 라이브러리에 대한 의존성이 생길 수 있습니다. - 유료 기능: 일부 고급 기능(Redis 프로토콜을 이용한 DB 캐싱 등)은 PRO(유료) 버전에서만 제공됩니다.
- 도입 효과 (Benefits):
- 코드량 50% 감소: 직렬화/역직렬화 및 커넥션 관리 코드가 사라져 비즈니스 로직에만 집중할 수 있습니다.
- 안전한 동시성 제어: 별도의 Zookeeper 설치 없이 Redis만으로도 신뢰할 만한 분산 락 시스템을 구축할 수 있습니다.
변경후: 자바 컬렉션 그 자체처럼 사용하는 구조
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
@Service
public class UserRedissonService {
private final RedissonClient redissonClient;
// 생성자 주입
public UserRedissonService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void cacheUserWithMap(String userId, UserProfile profile) {
// 1. "users"라는 이름의 Redis Hash를 RMap 타입으로 가져옴
RMap<String, UserProfile> userMap = redissonClient.getMap("users_cache");
// 2. 일반 자바 Map처럼 사용 (내부적으로 Redis HSET 명령어 실행 & 직렬화 자동 처리)
// Redisson은 기본적으로 Jackson이나 Kryo 같은 고성능 코덱을 사용하여 저장
userMap.put(userId, profile);
// 조회 예시
UserProfile cachedProfile = userMap.get(userId);
System.out.println("캐시 조회 성공: " + cachedProfile.getName());
}
}
[실행 결과 및 Redis 저장 데이터 예시]
코드를 실행하면 콘솔에는 자바 객체가 그대로 출력되지만, 실제 Redis에는 최적화된 JSON 혹은 바이너리 형태로 저장됩니다.
-- Console Output --
캐시 조회 성공: 홍길동
-- Redis-cli 확인 (Redisson 기본 FstCodec 사용 시) --
1) "field" -> "\xF6\x03\..." (Binary serialized data)
Redisson은 자체적인 코덱(Codec)을 통해 객체를 압축하여 저장하므로, 개발자가 JSON 변환 로직에 신경 쓸 필요가 없습니다. 또한 userMap.put(key, value, 10, TimeUnit.MINUTES)와 같이 Map의 특정 키에만 TTL(만료 시간)을 설정하는 기능(RMapCache)은 RedisTemplate으로는 구현하기 까다로운 고급 기능입니다.
Redisson 도입 시 꼭 고려해야 할 점
Redisson이 제공하는 편리함 이면에는 반드시 고려해야 할 비용과 주의사항이 존재합니다. 자바 컬렉션처럼 보인다고 해서 정말 로컬 메모리처럼 다루다가는 성능 장애(Out Of Memory, Timeout)를 유발할 수 있습니다.
1. "이건 로컬 변수가 아닙니다" (네트워크 비용 인지)
RMap이나 RList는 사용법이 자바의 HashMap, ArrayList와 똑같습니다. 하지만 map.get("key")를 호출할 때마다 네트워크 통신이 발생합니다.
- 주의: 반복문 안에서
RMap.get()을 수천 번 호출하는 코드는 엄청난 네트워크 지연을 초래합니다. 이럴 때는RMap.getAll()같은 배치(Batch) 메서드를 사용해야 합니다.
2. 무거운 연산의 위험성 (values(), entrySet())
자바 로컬 맵에서는 map.values()를 호출해 전체 데이터를 순회하는 것이 자연스럽습니다. 하지만 Redis에 저장된 데이터가 100만 건이라면?
- 위험:
RMap.values()나RMap.entrySet()을 호출하면 100만 건의 데이터를 네트워크로 다 끌어오다가 애플리케이션 서버가 OOM(Out Of Memory)으로 죽을 것입니다. - 대안: 대량의 데이터는
iterator를 사용하여 페이징 처리하듯 조금씩 가져와야 합니다.
3. 직렬화(Codec) 전략 고려
Redisson은 객체를 저장할 때 직렬화를 수행합니다. 기본 설정인 Jackson(JSON)은 가독성은 좋지만 용량을 많이 차지합니다.
[직렬화 방식 비교 (Benchmark)]
| Codec | 데이터 크기 (Size) | CPU 비용 (Throughput) | 용도 |
| Jackson (JSON) | 100% (기준) | High | 디버깅 용이, 일반적인 웹 데이터 |
| Kryo (Binary) | ~15% (7배 작음) | Low (3~7배 빠름) | 대용량 캐싱, 고성능 요구 시스템 |
- 팁: 벤치마크 결과, JSON 대신 Kryo 같은 바이너리 코덱 사용 시 데이터 크기는 최대 7배 감소하고, CPU 처리 비용도 낮아집니다. 트래픽이 많은 시스템이라면
KryoCodec설정을 강력히 권장합니다.
4. 맺음말
RedisTemplate이 Redis의 기능을 충실히 수행하는 '드라이버'라면, Redisson은 Redis를 자바 애플리케이션의 일부처럼 확장해 주는 '프레임워크'에 가깝습니다.
- 단순한 캐싱(String-String)만 필요하다면
RedisTemplate이나StringRedisTemplate이 가볍고 빠를 수 있습니다. - 복잡한 객체 저장, 분산 락, 컬렉션 사용이 빈번하다면 Redisson은 생산성을 비약적으로 높여주는 강력한 무기가 됩니다.
특히 많은 트래픽 처리에서 발생하는 동시성 이슈(Concurrency Issue)를 해결하기 위해 분산 락(RLock)이 필요하다면, Redisson은 선택이 아닌 필수입니다. 복잡한 인프라 코드를 걷어내고, 비즈니스 로직에 더 집중할 수 있는 Redisson 도입을 검토해 보시길 바랍니다.
'프로그래밍 PROGRAMMING > 자바 JAVA AND FRAMEWORKS' 카테고리의 다른 글
| [JAVA] Random vs SecureRandom vs ThreadLocalRandom: 자바 난수 생성의 모든 것 (0) | 2026.02.02 |
|---|---|
| [Spring] 스프링 배치 구조 정리: Tasklet부터 파티셔닝까지 (0) | 2026.01.31 |
| [JAVA/Cache] 이중 캐시(Two-Level Cache) 전략: 성능과 정합성 모두 만족하기 (0) | 2026.01.29 |
| [JAVA/Cache] 캐시 스탬피드(Cache Stampede): 만료된 순간 시스템이 멈추는 이유와 해결책 (2) | 2026.01.28 |
| [SPRING/JPA] JPA 2차 캐시(Second-Level Cache): 영속성 컨텍스트와 성능 최적화 (0) | 2026.01.27 |