공통 질문
1. Redis 기본 개념
Redis란 무엇인가요?
Redis는 어떤 데이터 구조를 지원하나요? (예: String, List, Set, Hash, Sorted Set)
Redis와 Memcached의 차이점은 무엇인가요?
Redis는 왜 단일 스레드로 동작하나요?
Redis는 NoSQL 데이터베이스인가요? 만약 그렇다면 왜 그렇게 분류되나요?
Redis의 주요 사용 사례는 무엇인가요? (예: 캐싱, 세션 관리, Pub/Sub 등)
Redis는 왜 데이터를 메모리에 저장하나요?
2. Redis 데이터 구조 관련 질문
Redis의 String 데이터 구조를 활용할 수 있는 예는 무엇인가요?
Redis의 List와 Set의 차이점은 무엇인가요?
Sorted Set은 어떤 경우에 사용되며, 일반 Set과의 차이점은 무엇인가요?
Redis의 Hash는 어떤 상황에서 유용하게 사용될 수 있나요?
HyperLogLog란 무엇이며, 어떤 용도로 사용되나요?
Stream 데이터 구조란 무엇이며, 어떤 시나리오에서 적합한가요?
3. Redis 캐싱
Redis를 캐싱에 사용하는 이유는 무엇인가요?
Redis 캐시의 TTL(Time To Live)을 설정하는 방법과 그 효과는 무엇인가요?
Cache Stampede(캐시 쏟아짐) 문제란 무엇이며, 이를 방지하는 방법은 무엇인가요?
Redis에서 LRU(Least Recently Used) 정책이란 무엇인가요?
Redis의 eviction 정책(제거 정책)에는 어떤 종류가 있나요? (예: allkeys-lru, volatile-lru 등)
4. Redis 성능 및 확장성
Redis는 왜 빠른 성능을 제공할 수 있나요?
Redis의 단일 스레드 구조가 성능에 어떤 영향을 미치나요?
Redis 클러스터(Redis Cluster)란 무엇이며, 왜 사용하는 것인가요?
Redis Sentinel이란 무엇이며, 어떤 역할을 하나요?
Redis는 어떤 방식으로 데이터를 샤딩(Sharding)하나요?
레디스 파이프라이닝(Pipelining)이란 무엇이며, 성능에 어떤 영향을 주나요?
Redis의 레이턴시(latency)를 줄이는 방법은 무엇인가요?
5. 데이터 영속성(Persistence)
Redis에서 영속성을 제공하는 두 가지 주요 방식은 무엇인가요? (RDB, AOF)
RDB(Snapshot)와 AOF(Append-Only File)의 차이점은 무엇인가요?
Redis에서 데이터 손실을 방지하려면 어떻게 해야 하나요?
Redis AOF 파일의 크기가 커질 때 어떻게 최적화할 수 있나요? (rewrite 옵션)
RDB 스냅샷이 언제 유용하게 사용되나요?
6. Redis 클러스터 및 분산 시스템
Redis 클러스터와 Redis Sentinel의 차이점은 무엇인가요?
Redis 클러스터에서 파티셔닝(Partitioning)은 어떻게 동작하나요?
Redis의 Consistent Hashing(일관된 해싱)이란 무엇인가요?
Redis 클러스터에서 슬록(Slot)이란 무엇이며, 어떤 역할을 하나요?
분산 환경에서 Redis의 데이터 일관성은 어떻게 유지되나요?
Redis에서 Pub/Sub과 Redis Streams의 차이점은 무엇인가요?
7. Redis 보안
Redis의 기본 인증(authentication) 방법은 무엇인가요?
Redis를 보안 위협(예: 무단 접근)으로부터 보호하려면 어떻게 해야 하나요?
Redis와 TLS/SSL을 설정하는 방법은 무엇인가요?
Redis의 ACL(Access Control List)이란 무엇인가요? 어떻게 설정하나요?
Redis에서 발생할 수 있는 보안 취약점과 이를 방지하는 방법은 무엇인가요?
8. Redis와 데이터 일관성
Redis는 CAP 이론에서 어디에 속하나요?
Redis에서 강한 일관성을 보장하려면 어떻게 해야 하나요?
Redis 클러스터에서 쓰기 연산 후 읽기 일관성을 보장하려면 어떻게 해야 하나요?
Redis에서 멱등성(Idempotency)을 구현하려면 어떤 전략을 사용할 수 있나요?
9. Redis Pub/Sub 및 Streams
Redis Pub/Sub의 작동 원리를 설명하세요.
Redis Pub/Sub은 어떤 상황에서 유용하며, 한계점은 무엇인가요?
Redis Streams란 무엇이며, Kafka와의 차이점은 무엇인가요?
Redis Streams에서 Consumer Group은 어떤 역할을 하나요?
Redis Pub/Sub과 Streams의 주요 차이점은 무엇인가요?
10. Redis의 고급 주제
Redis의 Lua 스크립트는 어떤 용도로 사용되며, 성능에 어떤 영향을 미치나요?
Redis 트랜잭션(Transaction)이란 무엇이며, MULTI와 EXEC의 역할은 무엇인가요?
레디스의 비동기 복제(Asynchronous Replication)란 무엇인가요?
Redis에서 Bloom Filter는 어떤 경우에 유용하게 사용되나요?
RedisJSON이란 무엇이며, JSON 데이터를 처리하는 방법은 무엇인가요?
Redis에서 Geo 데이터 구조는 어떤 목적으로 사용되나요?
1. Redis 기본 개념
1.1 Redis란 무엇인가요?
- Redis는 Remote Dictionary Server의 약자로, 오픈소스 인메모리 데이터 구조 저장소입니다.
- 주로 캐시, 메시지 브로커, 세션 저장소로 사용되며, 빠른 읽기/쓰기 속도를 제공합니다.
- Key-Value 저장소로 작동하며, 데이터를 디스크에 영속적으로 저장할 수도 있습니다.
1.2 Redis는 어떤 데이터 구조를 지원하나요?
- Redis는 다음의 다양한 데이터 구조를 지원합니다:
- String: 단순한 텍스트 또는 숫자.
- List: 순서가 있는 문자열의 컬렉션.
- Set: 중복이 없는 문자열의 집합.
- Hash: 필드와 값을 매핑하는 키-값 쌍.
- Sorted Set: 값에 순서를 부여하는 집합.
- Bitmap: 비트 단위로 데이터를 저장.
- HyperLogLog: 고유 값 개수를 근사 계산.
- Stream: 로그 스트림 데이터 저장.
1.3 Redis와 Memcached의 차이점은 무엇인가요?
Memcached란 무엇인가?
Memcached는 분산 메모리 캐싱 시스템으로, 데이터베이스나 애플리케이션에 자주 사용되는 데이터를 메모리(RAM)에 캐싱하여, 읽기 성능을 극대화하고 데이터베이스 부하를 줄이는 데 사용됩니다.
2003년, LiveJournal의 Brad Fitzpatrick에 의해 개발되었습니다.
Memcached의 주요 특징
- 속도:
- 모든 데이터를 메모리에 저장하므로 매우 빠른 읽기/쓰기 성능을 제공합니다.
- 네트워크 기반 캐싱 솔루션 중 가장 가벼운 도구 중 하나.
- 단순성:
- Key-Value 구조로 작동하며, 복잡한 데이터 타입을 지원하지 않아 사용이 간단합니다.
- 분산 환경 지원:
- 여러 서버를 클러스터로 구성하여 데이터를 분산 저장.
- 클라이언트가 해싱을 통해 데이터를 자동으로 적절한 서버에 분배.
- 데이터 휘발성:
- 데이터를 RAM에 저장하므로 서버가 재시작되거나 캐시가 만료되면 데이터가 사라집니다.
- 영속성을 보장하지 않는 점에서 Redis와 차별화됩니다.
- 캐시 정책:
- LRU(Least Recently Used) 정책을 사용하여, 메모리가 가득 찰 경우 오래된 데이터를 제거.
- 다중 언어 지원:
- C, Java, Python, PHP, Ruby 등 다양한 프로그래밍 언어용 클라이언트를 제공합니다.
Memcached의 주요 구성 요소
- Server:
- 데이터를 메모리에 저장하고, 클라이언트의 요청에 따라 데이터를 반환.
- Client:
- 애플리케이션과 Memcached 서버 간의 통신을 담당.
- 키에 따라 적절한 서버에 요청을 보냄.
- Key-Value Store:
- 데이터를 Key-Value 형식으로 저장하며, Key는 고유 식별자, Value는 저장할 데이터.
Memcached의 사용 사례
- 캐싱:
- 데이터베이스 쿼리 결과, API 응답, 템플릿 렌더링 결과 등을 캐싱하여 응답 속도를 높이고 데이터베이스 부하를 줄임.
- 세션 관리:
- 웹 애플리케이션에서 사용자 세션 데이터를 메모리에 저장.
- 빈번히 접근되는 데이터 저장:
- 자주 참조되는 설정 값, 구성 정보 등을 저장.
- 애플리케이션 가속화:
- 게임, 광고 플랫폼, 추천 시스템 등에서 실시간 데이터 제공.
Memcached 설치
sudo apt update
sudo apt install memcached
memcached -p 11211 -m 64 -vv
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.12.3</version>
</dependency>
import net.spy.memcached.MemcachedClient;
import java.net.InetSocketAddress;
public class MemcachedExample {
public static void main(String[] args) {
try {
// Memcached 서버 연결
MemcachedClient memcachedClient = new MemcachedClient(new InetSocketAddress("localhost", 11211));
// 데이터 저장
String key = "username";
String value = "Alice";
memcachedClient.set(key, 3600, value); // TTL: 3600초
System.out.println("Stored: " + key + " -> " + value);
// 데이터 조회
String cachedValue = (String) memcachedClient.get(key);
System.out.println("Retrieved: " + key + " -> " + cachedValue);
// 데이터 삭제
memcachedClient.delete(key);
System.out.println("Deleted: " + key);
// 다시 데이터 조회 (null 반환)
String deletedValue = (String) memcachedClient.get(key);
System.out.println("After deletion: " + key + " -> " + deletedValue);
// 연결 종료
memcachedClient.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Stored: username -> Alice
Retrieved: username -> Alice
Deleted: username
After deletion: username -> null
Memcached의 장단점
장점:
- 빠른 성능:
- 메모리 기반이라 응답 시간이 짧고 대량의 요청을 처리 가능.
- 단순성:
- 사용하기 쉬운 API와 간단한 명령어 제공.
- 분산 처리:
- 여러 서버를 쉽게 추가하여 수평 확장 가능.
단점:
- 데이터 휘발성:
- 메모리 기반이라 서버가 재시작되면 데이터가 손실됨.
- 제한적인 데이터 구조:
- Key-Value만 지원하므로 복잡한 데이터 처리가 어려움.
- 내장 복제/클러스터링 미지원:
- 데이터 복제 및 클러스터링은 별도로 구현해야 함.
1.4 Redis는 왜 단일 스레드로 동작하나요?
- Redis는 단일 스레드 이벤트 루프를 사용하여 요청을 처리합니다.
- 이 방식은 **락(lock)**과 컨텍스트 스위칭 문제를 방지하며, 대부분의 작업이 메모리에서 발생하므로 단일 스레드로도 충분히 빠릅니다.
1.5 Redis는 NoSQL 데이터베이스인가요?
Redis는 키-값 저장소 형태의 NoSQL 데이터베이스로 분류
전통적인 관계형 데이터베이스처럼 테이블이나 SQL 쿼리 대신, 단순한 명령으로 데이터를 저장하고 검색합니다.
1.6 Redis의 주요 사용 사례는 무엇인가요?
- 캐싱: 데이터베이스나 API 호출 결과를 저장해 응답 시간 단축.
- 세션 관리: 사용자 로그인 세션을 저장.
- Pub/Sub: 실시간 채팅, 알림 등 메시지 브로커로 사용.
- 지표 저장: 애플리케이션의 실시간 지표(예: 방문자 수) 저장.
- 작업 큐: 비동기 작업 처리.
1.7 Redis는 왜 데이터를 메모리에 저장하나요?
- 메모리는 디스크보다 빠른 읽기/쓰기 속도를 제공하며, Redis의 설계 목표는 초고속 성능입니다.
- 필요한 경우 디스크에 데이터를 저장하여 영속성을 보장합니다.
2. Redis 데이터 구조 관련 질문
2.1 Redis의 String 데이터 구조를 활용할 수 있는 예는 무엇인가요?
- 캐싱: 데이터베이스에서 가져온 결과를 저장.
- 세션 관리: 사용자 ID와 세션 정보를 매핑.
- 카운터: INCR, DECR 명령어를 통해 조회수, 방문자 수 등을 저장.
-- 페이지 조회 시 증가
SET page_views 0 # 초기값 설정
INCR page_views # 1 증가
INCR page_views # 또 1 증가
GET page_views # 현재 값 확인 (2)
-- 조회수 확인
SET page_views 2 # 초기값 설정
DECR page_views # 1 감소
GET page_views # 현재 값 확인 (1)
-- 조회수 저장 예제
INCR page:home:views # '홈페이지' 조회수 +1
INCR page:about:views # '소개 페이지' 조회수 +1
-- 조회수 확인
GET page:home:views # 홈 페이지 조회수 가져오기
GET page:about:views # 소개 페이지 조회수 가져오기
-- 결과
page:home:views: 10
page:about:views: 5
-- 오늘의 방문자 수
INCR visitors:2024-12-16 # 오늘의 방문자 수 증가
GET visitors:2024-12-16 # 방문자 수 가져오기
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class PageViewService {
@Autowired
private StringRedisTemplate redisTemplate;
// 조회수 증가
public Long incrementPageView(String pageKey) {
return redisTemplate.opsForValue().increment(pageKey);
}
// 조회수 가져오기
public Long getPageView(String pageKey) {
String value = redisTemplate.opsForValue().get(pageKey);
return value == null ? 0 : Long.parseLong(value);
}
}
@Autowired
PageViewService pageViewService;
@Test
public void testPageViewCounter() {
String pageKey = "page:home:views";
// 조회수 증가
pageViewService.incrementPageView(pageKey); // +1
pageViewService.incrementPageView(pageKey); // +1
// 조회수 확인
Long pageViews = pageViewService.getPageView(pageKey);
System.out.println("조회수: " + pageViews); // 출력: 조회수: 2
}
public void recordVisitor() {
String todayKey = "visitors:" + LocalDate.now(); // 오늘의 키
redisTemplate.opsForValue().increment(todayKey); // 방문자 수 증가
}
public Long getVisitorCountForToday() {
String todayKey = "visitors:" + LocalDate.now(); // 오늘의 키
String count = redisTemplate.opsForValue().get(todayKey);
return count == null ? 0 : Long.parseLong(count); // 방문자 수 반환
}
2.2 Redis의 List와 Set의 차이점은 무엇인가요?
List - RPUSH, LPOP
Set - SADD, SMEMBERS -> S 기억하라
-- 1. Redis List 예제
RPUSH queue "Task 1" # 큐의 끝에 삽입
RPUSH queue "Task 2" # 큐의 끝에 삽입
RPUSH queue "Task 3" # 큐의 끝에 삽입
LPOP queue # 큐의 앞에서 제거 (Task 1 반환)
LPOP queue # 큐의 앞에서 제거 (Task 2 반환)
-- 2. Redis Set 예제
SADD users "user1" # user1 추가
SADD users "user2" # user2 추가
SADD users "user1" # 중복, 추가되지 않음
SMEMBERS users # 모든 사용자 반환
-- set 교집합 및 합집합
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"
SINTER set1 set2 # 교집합: ["b", "c"]
SUNION set1 set2 # 합집합: ["a", "b", "c", "d"]
-- List와 Set을 함께 사용하는 예제
SADD processed_tasks "Task 1" # 처리된 작업 저장
SADD processed_tasks "Task 2"
RPUSH task_queue "Task 3" # 처리 대기 중인 작업 추가
RPUSH task_queue "Task 2" # 중복 작업
LPOP task_queue # 작업 큐에서 꺼냄 (Task 3 반환)
SISMEMBER processed_tasks "Task 3" # 이미 처리된 작업인지 확인
Redis List 예제
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class QueueService {
@Autowired
private StringRedisTemplate redisTemplate;
private final String QUEUE_KEY = "task_queue";
// 작업 추가
public void addTask(String task) {
redisTemplate.opsForList().rightPush(QUEUE_KEY, task);
}
// 작업 처리
public String processTask() {
return redisTemplate.opsForList().leftPop(QUEUE_KEY);
}
}
@Autowired
QueueService queueService;
@Test
public void testQueue() {
queueService.addTask("Task 1");
queueService.addTask("Task 2");
queueService.addTask("Task 3");
System.out.println(queueService.processTask()); // Task 1
System.out.println(queueService.processTask()); // Task 2
}
Redis Set 예제
@Service
public class SetService {
@Autowired
private StringRedisTemplate redisTemplate;
private final String SET_KEY = "user_set";
// 사용자 추가
public void addUser(String userId) {
redisTemplate.opsForSet().add(SET_KEY, userId);
}
// 모든 사용자 가져오기
public Set<String> getAllUsers() {
return redisTemplate.opsForSet().members(SET_KEY);
}
// 특정 사용자 존재 여부 확인
public boolean isUserExists(String userId) {
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(SET_KEY, userId));
}
}
@Autowired
SetService setService;
@Test
public void testSet() {
setService.addUser("user1");
setService.addUser("user2");
setService.addUser("user1"); // 중복, 저장되지 않음
System.out.println(setService.getAllUsers()); // [user1, user2]
System.out.println(setService.isUserExists("user1")); // true
System.out.println(setService.isUserExists("user3")); // false
}
2.3 Sorted Set은 어떤 경우에 사용되며, 일반 Set과의 차이점은 무엇인가요?
- Sorted Set은 각 요소에 **스코어(score)**가 부여되어 정렬됩니다.
- 예: 랭킹 시스템(점수 기반 사용자 순위), 우선순위 큐.
Set - SADD, SMEMBERS -> S 기억하라
Sorted Set - ZADD -> Z 기억하라
-- Sorted Set
--- 점수 추가 (ZADD)
ZADD leaderboard 100 user1 # user1의 점수 100
ZADD leaderboard 200 user2 # user2의 점수 200
ZADD leaderboard 150 user3 # user3의 점수 150
--- 상위 랭커 조회 (ZREVRANGE)
ZREVRANGE leaderboard 0 2 WITHSCORES
-- 결과
1) "user2"
2) "200"
3) "user3"
4) "150"
5) "user1"
6) "100"
-- 특정 사용자의 순위 조회 (ZREVRANK)
ZREVRANK leaderboard user1
-- 점수 업데이트
ZINCRBY leaderboard 50 user1
-- 점수 범위로 사용자 조회 (ZRANGEBYSCORE)
ZRANGEBYSCORE leaderboard 100 150
-- 결과
1) "user1"
2) "user3"
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class LeaderboardService {
@Autowired
private StringRedisTemplate redisTemplate;
private final String LEADERBOARD_KEY = "leaderboard";
// 점수 추가 또는 업데이트
public void addOrUpdateScore(String user, double score) {
redisTemplate.opsForZSet().add(LEADERBOARD_KEY, user, score);
}
// 상위 N명 조회
public Set<ZSetOperations.TypedTuple<String>> getTopRankers(int topN) {
return redisTemplate.opsForZSet().reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);
}
// 특정 사용자의 순위 조회
public Long getUserRank(String user) {
Long rank = redisTemplate.opsForZSet().reverseRank(LEADERBOARD_KEY, user);
return rank != null ? rank + 1 : null; // 1-based 순위 반환
}
// 특정 사용자의 점수 조회
public Double getUserScore(String user) {
return redisTemplate.opsForZSet().score(LEADERBOARD_KEY, user);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;
import java.util.Set;
public class LeaderboardServiceTest {
@Autowired
private LeaderboardService leaderboardService;
@Test
public void testLeaderboard() {
// 점수 추가
leaderboardService.addOrUpdateScore("user1", 100);
leaderboardService.addOrUpdateScore("user2", 200);
leaderboardService.addOrUpdateScore("user3", 150);
// 상위 3명 조회
Set<ZSetOperations.TypedTuple<String>> topRankers = leaderboardService.getTopRankers(3);
System.out.println("Top 3 Rankers:");
for (ZSetOperations.TypedTuple<String> ranker : topRankers) {
System.out.println(ranker.getValue() + " - " + ranker.getScore());
}
// 특정 사용자 순위 조회
Long user1Rank = leaderboardService.getUserRank("user1");
System.out.println("User1 Rank: " + user1Rank);
// 특정 사용자 점수 조회
Double user1Score = leaderboardService.getUserScore("user1");
System.out.println("User1 Score: " + user1Score);
}
}
2.4 Redis의 Hash는 어떤 상황에서 유용하게 사용될 수 있나요?
필드 기반 데이터 관리에 주로 쓰임, java의 class 처럼 생각하면 쉬움
실제 사용 사례
- 사용자 세션 관리:
- 사용자의 ID, 로그인 시간, 마지막 접속 시간 등 프로필 정보를 저장.
- 사내 관리 시스템:
- 직원의 기본 정보(name, department, position)를 관리.
- 장바구니:
- 고객의 장바구니 정보(item_id, quantity)를 하나의 키에 저장.
Set - SADD, SMEMBERS -> S 기억하라
Sorted Set - ZADD -> Z 기억하라
Hash - HSET, HGET -> H 기억하라
-- 사용자 프로필 저장
HSET user:1 name "John Doe"
HSET user:1 age "30"
HSET user:1 email "john.doe@example.com"
-- 사용자 프로필 조회
HGET user:1 name
-- 결과
"John Doe"
-- 모든 필드 조회
HGETALL user:1
-- 결과
1) "name"
2) "John Doe"
3) "age"
4) "30"
5) "email"
6) "john.doe@example.com"
-- 필드 업데이트
HSET user:1 age "31"
-- 특정 필드 값 확인 (HEXISTS)
HEXISTS user:1 email
-- 결과
1 (존재하면 1, 없으면 0)
-- 필드 삭제
HDEL user:1 email
-- 특정 Hash의 필드 개수 조회 (HLEN)
HLEN user:1
-- 결과
2 (현재 name, age만 남아 있음)
-- 필드 이름만 조회 (HKEYS):
HKEYS user:1
-- 필드 값만 조회 (HVALS):
HVALS user:1
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class UserProfileService {
private static final String USER_KEY_PREFIX = "user:";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private HashOperations<String, String, String> hashOps;
@Autowired
public void init() {
this.hashOps = redisTemplate.opsForHash();
}
// 사용자 프로필 저장
public void saveUserProfile(String userId, Map<String, String> profile) {
String key = USER_KEY_PREFIX + userId;
hashOps.putAll(key, profile);
}
// 특정 사용자 프로필 조회
public Map<String, String> getUserProfile(String userId) {
String key = USER_KEY_PREFIX + userId;
return hashOps.entries(key);
}
// 특정 필드 값 조회
public String getUserField(String userId, String field) {
String key = USER_KEY_PREFIX + userId;
return hashOps.get(key, field);
}
// 특정 필드 업데이트
public void updateUserField(String userId, String field, String value) {
String key = USER_KEY_PREFIX + userId;
hashOps.put(key, field, value);
}
// 특정 필드 삭제
public void deleteUserField(String userId, String field) {
String key = USER_KEY_PREFIX + userId;
hashOps.delete(key, field);
}
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashMap;
import java.util.Map;
public class UserProfileServiceTest {
@Autowired
private UserProfileService userProfileService;
@Test
public void testUserProfile() {
// 사용자 프로필 저장
Map<String, String> profile = new HashMap<>();
profile.put("name", "John Doe");
profile.put("age", "30");
profile.put("email", "john.doe@example.com");
userProfileService.saveUserProfile("1", profile);
// 사용자 프로필 조회
Map<String, String> retrievedProfile = userProfileService.getUserProfile("1");
System.out.println("User Profile: " + retrievedProfile);
// 특정 필드 값 조회
String userName = userProfileService.getUserField("1", "name");
System.out.println("User Name: " + userName);
// 특정 필드 업데이트
userProfileService.updateUserField("1", "age", "31");
System.out.println("Updated Age: " + userProfileService.getUserField("1", "age"));
// 특정 필드 삭제
userProfileService.deleteUserField("1", "email");
System.out.println("After Email Deletion: " + userProfileService.getUserProfile("1"));
}
}
2.5 HyperLogLog란 무엇이며, 어떤 용도로 사용되나요?
HyperLogLog는 Redis에서 제공하는 데이터 구조
대량의 데이터에서 고유 값(Unique Values)의 개수를 효율적으로 근사 계산하기 위해 사용
Redis의 HyperLogLog는 고유 값의 개수를 계산해야 하지만 메모리 사용량이 중요한 경우에 매우 적합합니다.
특히 대규모 방문자 수 추적과 같은 시나리오에서 큰 장점을 제공
특징:
- 메모리 효율적:
- 2^64개의 고유 데이터를 처리하더라도 12KB 미만의 메모리를 사용합니다.
- 근사 계산:
- 정확한 값이 아니라 근사값을 반환하지만, 오차율은 0.81% 이내로 매우 낮습니다.
- 주요 용도:
- 대규모 방문자 수 추적(예: 웹사이트 UV 추적).
- 고유 이벤트 개수 계산.
- 중복 데이터 검사가 필요 없는 경우.
사용 사례
웹사이트 고유 방문자(Unique Visitors)
추적 매일의 방문자 ID를 HyperLogLog에 저장.
하루, 주, 월 단위로 병합하여 고유 방문자 수를 계산.
이벤트 참여자 수 계산
이벤트 참여자의 고유 ID를 HyperLogLog에 저장하여, 중복 체크 없이 참여자 수를 계산.
고유 항목 개수 근사 계산
대규모 데이터에서 중복된 값을 제거하고 고유 항목의 개수(예: IP 주소, 제품 ID)를 빠르게 계산.
HyperLogLog의 장단점
장점:
- 메모리 효율적:
- 최대 12KB의 메모리만 사용.
- 간단한 사용법:
- PFADD, PFCOUNT 등 단순한 명령어로 동작.
- 대규모 데이터 처리:
- 대규모 데이터를 처리할 때 중복 검사가 필요 없는 경우 적합.
단점:
- 근사 계산:
- 고유 개수는 정확하지 않고 근사값임.
- 정확도가 중요할 경우 사용에 주의해야 함.
- 중복된 데이터 확인 불가:
- 실제로 중복된 항목이 무엇인지 알 수 없음.
-- PFADD: 고유 값 추가
-- PFADD key value [value ...]
PFADD visitors user1
PFADD visitors user2
PFADD visitors user3
PFADD visitors user1 # 중복된 값 추가
-- PFCOUNT: 고유 값 개수 조회
-- PFCOUNT key [key ...]
PFCOUNT visitors
-- 결과
3 (중복된 user1은 제외되고, user1, user2, user3의 3명만 계산됨)
-- PFMERGE: HyperLogLog 병합
-- PFMERGE destkey sourcekey [sourcekey ...]
PFADD day1_V user1 user2 user3
PFADD day2_V user3 user4 user5
-- 두 날의 방문자 수 병합
PFMERGE all_V day1_V day2_V
PFCOUNT all_V
-- 결과
5 (user1, user2, user3, user4, user5의 고유 방문자)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class HyperLogLogService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 방문자 추가
public void addVisitor(String key, String visitorId) {
redisTemplate.opsForHyperLogLog().add(key, visitorId);
}
// 고유 방문자 수 조회
public long getUniqueVisitorCount(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
// 여러 HyperLogLog 병합
public void mergeVisitors(String destKey, String... sourceKeys) {
redisTemplate.opsForHyperLogLog().union(destKey, sourceKeys);
}
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
public class HyperLogLogServiceTest {
@Autowired
private HyperLogLogService hyperLogLogService;
@Test
public void testHyperLogLog() {
// Day 1 방문자 추가
hyperLogLogService.addVisitor("day1_visitors", "user1");
hyperLogLogService.addVisitor("day1_visitors", "user2");
hyperLogLogService.addVisitor("day1_visitors", "user3");
// Day 2 방문자 추가
hyperLogLogService.addVisitor("day2_visitors", "user3");
hyperLogLogService.addVisitor("day2_visitors", "user4");
hyperLogLogService.addVisitor("day2_visitors", "user5");
// 고유 방문자 수 확인
long day1Count = hyperLogLogService.getUniqueVisitorCount("day1_visitors");
long day2Count = hyperLogLogService.getUniqueVisitorCount("day2_visitors");
System.out.println("Day 1 Visitors: " + day1Count); // 3
System.out.println("Day 2 Visitors: " + day2Count); // 3
// 방문자 병합
hyperLogLogService.mergeVisitors("all_visitors", "day1_visitors", "day2_visitors");
// 병합된 방문자 수 확인
long allVisitorsCount = hyperLogLogService.getUniqueVisitorCount("all_visitors");
System.out.println("All Visitors: " + allVisitorsCount); // 5
}
}
2.6 Stream 데이터 구조란 무엇이며, 어떤 시나리오에서 적합한가요?
Redis Stream은 실시간 이벤트 데이터 스트림을 처리하기 위한 데이터 구조
Kafka와 같은 메시지 브로커의 역할을 수행하며, 로그 데이터 관리, 실시간 데이터 처리, 비동기 이벤트 처리에 적합
특징:
- 순차적 데이터 스트림:
- 시간 순서대로 데이터를 관리하며, 각 항목은 고유 ID를 가집니다.
- 소비자 그룹(Consumer Groups):
- 메시지를 여러 소비자(Consumer)에게 분배하여 병렬 처리를 지원합니다.
- 비동기 처리:
- 메시지를 읽고 처리한 뒤, 이를 수동으로 확인(Acknowledge)할 수 있습니다.
- 내장 메시지 브로커:
- Kafka, RabbitMQ와 비슷한 기능을 제공하지만 Redis의 데이터 구조를 사용합니다.
Redis Stream을 사용하는 적합한 시나리오
- 로그 데이터 관리:
- 애플리케이션의 로그를 수집하고 분석.
- 예: 사용자 행동 로그, 시스템 이벤트 로그.
- 실시간 데이터 처리:
- 실시간으로 발생하는 데이터를 소비자 그룹에 분배하여 처리.
- 예: 실시간 채팅 메시지, 금융 거래 이벤트.
- 비동기 작업 큐:
- 작업을 여러 워커(worker)로 분산 처리.
- 예: 이메일 발송 큐, 이미지 처리 작업.
-- 데이터 추가 (XADD)
--- *: Redis가 자동으로 고유 ID를 생성.
XADD mystream * message "User logged in" user_id "123"
XADD mystream * message "User clicked button" user_id "456"
--- 결과
1681234567890-0
1681234567891-0
-- 데이터 조회 (XRANGE)
XRANGE mystream - +
--- 결과
1) 1681234567890-0
1) "message"
2) "User logged in"
3) "user_id"
4) "123"
2) 1681234567891-0
1) "message"
2) "User clicked button"
3) "user_id"
4) "456"
-- 소비자 그룹 생성 (XGROUP CREATE)
--- mygroup: 그룹 이름
--- $: 기존 메시지는 무시하고 이후에 추가된 메시지부터 소비
XGROUP CREATE mystream mygroup $ MKSTREAM
-- 메시지 읽기 (XREADGROUP)
XREADGROUP GROUP mygroup consumer1 COUNT 2 STREAMS mystream >
--- 결과
1) "mystream"
1) 1) "1681234567890-0"
1) "message"
2) "User logged in"
3) "user_id"
4) "123"
2) "1681234567891-0"
1) "message"
2) "User clicked button"
3) "user_id"
4) "456"
-- 메시지 확인(Acknowledgement) (XACK)
--- 소비자가 읽은 메시지를 처리 완료로 표시
XACK mystream mygroup 1681234567890-0
추가:
redisTemplate.opsForStream()
.add(
StreamRecords.mapBacked(Map.of("key", key, "value", value)).withStreamKey(STREAM_KEY)
);
읽기:
redisTemplate.opsForStream()
.read(MapRecord.class, StreamOffset.latest(STREAM_KEY));
소비자 그룹:
redisTemplate.opsForStream()
.createGroup(STREAM_KEY, GROUP_NAME);
소비자 그룹 소비:
redisTemplate.opsForStream() .read(Consumer.from(GROUP_NAME, consumerName), StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed()));
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class RedisStreamService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String STREAM_KEY = "mystream";
private static final String GROUP_NAME = "mygroup";
// 데이터 추가
public String addLog(String message, String userId) {
Map<String, String> data = Map.of(
"message", message,
"user_id", userId
);
RecordId recordId = redisTemplate.opsForStream().add(STREAM_KEY, data);
return recordId.getValue();
}
// 소비자 그룹 생성
public void createConsumerGroup() {
try {
redisTemplate.opsForStream().createGroup(STREAM_KEY, GROUP_NAME);
} catch (Exception e) {
System.out.println("Consumer group already exists.");
}
}
// 메시지 읽기
public void readMessages(String consumerName) {
Consumer consumer = Consumer.from(GROUP_NAME, consumerName);
StreamReadOptions options = StreamReadOptions.empty().count(5);
List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream()
.read(consumer, StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed()));
for (MapRecord<String, Object, Object> message : messages) {
System.out.println("ID: " + message.getId());
System.out.println("Data: " + message.getValue());
// 메시지 처리 완료로 표시
redisTemplate.opsForStream().acknowledge(GROUP_NAME, message);
}
}
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
public class RedisStreamServiceTest {
@Autowired
private RedisStreamService redisStreamService;
@Test
public void testStream() {
// Step 1: 소비자 그룹 생성
redisStreamService.createConsumerGroup();
// Step 2: 데이터 추가
redisStreamService.addLog("User logged in", "123");
redisStreamService.addLog("User clicked button", "456");
// Step 3: 메시지 읽기
redisStreamService.readMessages("consumer1");
}
}
// Listener
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;
@Component
public class StreamMessageListener implements StreamListener<String, ObjectRecord<String, String>> {
@Override
public void onMessage(ObjectRecord<String, String> message) {
System.out.println("Stream Key: " + message.getStream());
System.out.println("Message ID: " + message.getId());
System.out.println("Message Value: " + message.getValue());
}
}
// Listener Container 설정
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.data.redis.stream.Subscription;
import java.time.Duration;
@Configuration
public class StreamListenerConfig {
@Bean
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> streamMessageListenerContainer(
RedisConnectionFactory connectionFactory, StreamMessageListener listener) {
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> options =
StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
.pollTimeout(Duration.ofMillis(100))
.build();
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
StreamMessageListenerContainer.create(connectionFactory, options);
container.receive(StreamOffset.latest("mystream"), listener);
container.start();
return container;
}
}
Redis Stream의 장단점
장점:
- 소비자 그룹:
- 메시지를 여러 소비자에게 병렬로 분배 가능.
- 순차적 데이터:
- 로그와 같은 순차적 데이터 관리에 적합.
- 다양한 명령어:
- XADD, XREADGROUP, XACK 등으로 세부적인 데이터 관리 가능.
단점:
- Kafka 대비 기능 제한:
- Kafka와 같은 전문 메시지 브로커보다 기능이 단순.
- 메모리 관리:
- 오래된 데이터를 삭제하지 않으면 메모리 증가 문제 발생.
Redis Streams vs Kafka vs RabbitMQ
Redis Streams만으로 충분한 경우
- 단순한 메시징 요구:
- 실시간 로그 관리.
- 간단한 작업 큐.
- Redis를 이미 사용 중인 경우:
- Redis 설치 및 운영 비용을 절감 가능.
- 기존 Redis 데이터와 통합하여 활용 가능.
- 고성능이 요구되는 경우:
- Redis Streams는 메모리 기반이므로 매우 빠른 메시지 처리 속도를 제공.
Redis Streams로는 부족한 경우
- 대규모 데이터 파이프라인:
- Kafka는 수백만 건의 메시지를 분산 처리하는 데 더 적합.
- 복잡한 메시지 라우팅:
- RabbitMQ는 교환기(Exchange)를 통해 고급 메시지 라우팅이 가능.
- 장기 데이터 보관:
- Kafka는 디스크 기반 저장으로 데이터 보관 및 재처리에 강점.
- 고도의 내구성과 신뢰성:
- RabbitMQ는 ACK/NACK 메커니즘으로 메시지의 확실한 전달을 보장.
3. Redis 캐싱
3.1 Redis를 캐싱에 사용하는 이유는 무엇인가요?
- 초고속 성능: 메모리 기반 저장소로, 읽기/쓰기 속도가 매우 빠름.
- 간단한 사용법: 키-값 구조와 직관적인 명령어 제공.
- TTL 지원: 캐시 데이터의 유효 기간을 쉽게 설정 가능.
3.2 Redis 캐시의 TTL을 설정하는 방법과 그 효과는 무엇인가요?
- 명령어: SET key value EX seconds
- 효과: 데이터가 자동 만료되어 메모리를 효율적으로 관리.
3.3 Cache Stampede란 무엇이며, 이를 방지하는 방법은 무엇인가요?
- 캐시 스탬피드: 캐시 만료 시, 여러 클라이언트가 동시에 데이터베이스를 조회하는 현상.
- 해결 방법:
- 잠금(lock): SETNX로 단일 클라이언트만 캐시 갱신.
- 캐시 슬라이싱: TTL을 랜덤하게 설정.
3.4 Redis에서 LRU 정책이란 무엇인가요?
- LRU(Least Recently Used): 가장 오래 사용하지 않은 데이터를 제거하는 캐싱 정책.
LRU의 구현 방식
LRU는 일반적으로 다음 두 가지 방법으로 구현됩니다:
1. 리스트(List) 기반 구현
- 데이터가 삽입되거나 참조될 때, 데이터를 리스트의 맨 앞으로 이동.
- 캐시가 가득 찼을 때 리스트의 **맨 뒤의 데이터(가장 오래 사용되지 않은 데이터)**를 제거.
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
// Constructor
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // true: Access order 유지
this.capacity = capacity;
}
// 가장 오래된 데이터 제거 조건
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // 캐시가 용량을 초과하면 가장 오래된 데이터 제거
}
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
// 데이터 추가
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
System.out.println(cache); // {1=A, 2=B, 3=C}
// 데이터 참조
cache.get(1);
System.out.println(cache); // {2=B, 3=C, 1=A}
// 새로운 데이터 추가
cache.put(4, "D");
System.out.println(cache); // {3=C, 1=A, 4=D} (2 제거됨)
}
}
2. 해시맵(HashMap) + 이중 연결 리스트(Doubly Linked List)
- 해시맵: 데이터에 빠르게 접근하기 위해 사용.
- 이중 연결 리스트: 데이터의 참조 순서를 관리하기 위해 사용.
- 참조된 데이터를 리스트의 맨 앞으로 이동.
- 맨 뒤의 데이터가 가장 오래 사용되지 않은 데이터.
import java.util.HashMap;
public class LRUCache<K, V> {
// 노드 클래스 (이중 연결 리스트 노드)
private static class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> next;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private final int capacity; // LRU 캐시의 최대 크기
private final HashMap<K, Node<K, V>> cache; // 키-노드 매핑을 위한 해시맵
private final Node<K, V> head; // 더미 헤드 노드 (가장 최근 사용된 데이터의 시작)
private final Node<K, V> tail; // 더미 테일 노드 (가장 오래 사용되지 않은 데이터의 끝)
// 생성자
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
this.head = new Node<>(null, null); // 더미 헤드
this.tail = new Node<>(null, null); // 더미 테일
head.next = tail; // 이중 연결 리스트 초기화
tail.prev = head;
}
// 데이터 가져오기 (Get)
public V get(K key) {
Node<K, V> node = cache.get(key);
if (node == null) {
return null; // 키가 캐시에 없으면 null 반환
}
moveToHead(node); // 사용된 데이터를 가장 최근 위치로 이동
return node.value;
}
// 데이터 추가 또는 갱신 (Put)
public void put(K key, V value) {
Node<K, V> node = cache.get(key);
if (node != null) {
// 기존 데이터 갱신
node.value = value;
moveToHead(node); // 갱신된 데이터를 가장 최근 위치로 이동
} else {
// 새로운 데이터 추가
Node<K, V> newNode = new Node<>(key, value);
cache.put(key, newNode);
addToHead(newNode);
if (cache.size() > capacity) {
// 캐시가 가득 찼다면 가장 오래된 데이터 제거
Node<K, V> tailNode = removeTail();
cache.remove(tailNode.key);
}
}
}
// 이중 연결 리스트의 맨 앞에 노드 추가
private void addToHead(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 이중 연결 리스트에서 노드 제거
private void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 이중 연결 리스트에서 노드를 맨 앞으로 이동
private void moveToHead(Node<K, V> node) {
removeNode(node); // 기존 위치에서 제거
addToHead(node); // 맨 앞으로 추가
}
// 이중 연결 리스트에서 가장 오래된 노드(맨 뒤)를 제거
private Node<K, V> removeTail() {
Node<K, V> tailNode = tail.prev;
removeNode(tailNode);
return tailNode;
}
// 테스트 코드
public static void main(String[] args) {
LRUCache<Integer, String> lruCache = new LRUCache<>(3);
// 데이터 추가
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
System.out.println(lruCache.cache.keySet()); // 출력: [1, 2, 3]
// 데이터 접근
lruCache.get(1); // 1이 가장 최근에 사용됨
System.out.println(lruCache.cache.keySet()); // 출력: [2, 3, 1]
// 새로운 데이터 추가 (용량 초과 -> 가장 오래된 2 제거)
lruCache.put(4, "D");
System.out.println(lruCache.cache.keySet()); // 출력: [3, 1, 4]
// 데이터 접근
lruCache.get(3); // 3이 가장 최근에 사용됨
System.out.println(lruCache.cache.keySet()); // 출력: [1, 4, 3]
// 새로운 데이터 추가 (용량 초과 -> 가장 오래된 1 제거)
lruCache.put(5, "E");
System.out.println(lruCache.cache.keySet()); // 출력: [4, 3, 5]
}
}
작동 과정
- 해시맵으로 데이터를 검색하여 O(1) 시간 복잡도로 데이터 접근 가능.
- 연결 리스트를 사용해 데이터 참조 순서를 유지.
- 참조 기록 유지:
- 캐시는 각 데이터가 마지막으로 참조된 시간을 기록하거나, 참조된 순서를 유지합니다.
- 가장 오래 참조되지 않은 데이터를 쉽게 식별할 수 있어야 합니다.
- 데이터 삽입 시:
- 새로운 데이터가 캐시에 추가될 때:
- 캐시가 가득 차 있으면, 가장 오래 사용되지 않은 데이터를 제거하고 새 데이터를 추가합니다.
- 캐시가 여유 공간이 있으면 그대로 추가합니다.
- 새로운 데이터가 캐시에 추가될 때:
- 데이터 참조 시:
- 이미 캐시에 존재하는 데이터를 참조하면:
- 해당 데이터는 최신 상태로 업데이트됩니다. (참조 기록이 갱신됩니다.)
- 이를 통해 해당 데이터가 "최근 사용"되었음을 표시합니다.
- 이미 캐시에 존재하는 데이터를 참조하면:
- 데이터 교체 시:
- 캐시 용량이 초과되면, 가장 오래 사용되지 않은 데이터를 제거합니다.
3.5 Redis의 eviction 정책에는 어떤 종류가 있나요?
Redis에서는 LRU를 캐싱 정책으로 지원
Redis는 데이터가 메모리를 초과할 경우, 설정된 정책에 따라 데이터를 제거
1. Redis 설정 파일(redis.conf)에서 LRU 정책을 설정
- maxmemory-policy allkeys-lru
2. Redis CLI를 사용해 실시간으로 설정
CONFIG SET maxmemory-policy allkeys-lru
3. Redis 메모리 제한 설정
maxmemory 100mb
- allkeys-lru: 모든 키에서 가장 오래 사용되지 않은 키 제거.
- volatile-lru: TTL(Time-to-Live)이 설정된 키 중 가장 오래 사용되지 않은 키 제거.
- volatile-ttl: TTL이 가장 짧은 키 제거.
- noeviction: 메모리가 꽉 찼을 때 추가 삽입을 차단.
4. Redis 성능 및 확장성
4.1 Redis는 왜 빠른 성능을 제공할 수 있나요?
- 메모리 기반 처리:
- Redis는 데이터를 메모리에 저장하고, 디스크 I/O를 최소화하여 매우 빠른 읽기/쓰기 성능을 제공합니다.
- 단일 스레드 아키텍처:
- 단일 스레드로 동작하므로 컨텍스트 스위칭과 락(lock) 경합이 발생하지 않습니다.
- 이벤트 기반 I/O:
- 비동기 이벤트 루프를 사용하여 요청을 처리합니다.
- 간단한 명령어 구조:
- 대부분의 명령어가 O(1) 또는 O(log(n)) 복잡도를 가지며, 빠른 응답을 제공합니다.
4.2 Redis의 단일 스레드 구조가 성능에 어떤 영향을 미치나요?
- 장점:
- 락 경쟁이 없으므로 성능 저하가 없습니다.
- 단일 스레드에서 순차적으로 요청을 처리하여 높은 처리량을 유지합니다.
- 단점:
- CPU 집약적인 작업(예: 복잡한 스크립트 실행)이 병목 현상을 일으킬 수 있습니다.
- 여러 CPU 코어를 효과적으로 활용하려면 Redis 인스턴스를 추가로 실행해야 합니다.
Redis 서버 간 캐싱 데이터의 일관성 문제
Q: redis에 캐시해놔도 redis 서버가 4대이면 서로 다르게 캐싱하고있는거 아니야?
A: Redis 서버를 4대 사용하는 경우, 기본적으로 각 서버는 독립적으로 작동하므로 동일한 데이터를 캐시하더라도 각 서버에 별도로 저장됩니다. 이는 Redis가 분산 캐시로 동작하지 않고, 개별적인 단일 노드 캐시로 설정된 경우 발생합니다.
Redis 4대 환경에서 고려할 점
- 독립 실행: 4개의 Redis 서버가 독립적으로 실행되면, 각 서버의 데이터는 서로 공유되지 않음.
- 일관성 요구: 데이터의 일관성을 요구하는 경우 Redis 클러스터나 Replication을 반드시 사용해야 함.
- 키 매핑 전략: 분산 환경에서는 Consistent Hashing을 활용하여 키를 특정 Redis 서버로 매핑.
1. Redis 서버가 독립적으로 동작하는 경우
- 각 Redis 서버는 독립적으로 데이터를 저장합니다.
- 같은 키를 저장하더라도 서로 다른 서버에 데이터를 쓰거나 읽을 수 있으므로 캐시 데이터가 일관되지 않을 수 있습니다.
- 서버 A에 key1을 저장하고 서버 B에서 key1을 요청하면, 해당 키를 찾지 못합니다.
- 이는 서버 A와 서버 B가 독립적으로 데이터를 관리하기 때문입니다.
2. 분산 환경에서 캐싱 일관성을 해결하는 방법
Redis는 단일 서버로도 고성능을 제공하지만, **수평 확장(Scale-Out)**이 필요하면 Redis 서버를 분산 환경으로 구성해야 합니다.
이를 해결하기 위해 다음과 같은 옵션을 고려할 수 있습니다
1) Redis 클러스터 (Redis Cluster)
4개의 서버를 묶음, 데이터를 **샤딩(Sharding)**하여 저장
데이터 분배 방식: Redis 클러스터는 키를 **16384개의 해시 슬롯(Hash Slot)**으로 나누고, 각 노드가 일부 슬롯을 담당합니다.
- 예: 4개의 Redis 서버가 있다면, 각 서버가 슬롯의 1/4을 담당.
- 특정 키는 해시 값에 따라 한 서버에만 저장되며, 모든 서버가 데이터를 공유하지 않습니다.
2) Redis Replication (Master-Slave)
**Redis 복제(Replication)**는 데이터를 한 서버(Master)에 저장하고, 다른 서버(Slave)가 이를 복제하는 구조
- 데이터 흐름: Master 서버에서 데이터 변경이 발생하면 Slave 서버로 변경 사항이 전파
- 읽기 요청: Slave 서버에서도 읽기 요청을 처리
- 쓰기 요청: Master 서버에서만 처리
구성 예:
- 서버 A: Master (쓰기 처리)
- 서버 B, C, D: Slave (읽기 처리)
장점:
- 읽기 부하를 여러 서버로 분산 가능.
- 데이터의 일관성을 보장.
단점:
- Master 서버가 장애를 일으키면, 장애 조치(Failover) 필요.
- 쓰기 성능은 Master 서버에 종속됨.
3) 분산 캐시 키 전략 (Consistent Hashing)
**Consistent Hashing(일관된 해싱)**을 사용하여 특정 키를 항상 동일한 서버에 저장하도록 강제
- 원리:
- 키를 해싱하여 특정 Redis 서버에 매핑.
- 동일한 키는 항상 동일한 서버에 저장됨.
- 장점:
- 데이터를 수동으로 샤딩하지 않아도 됨.
- 특정 서버가 다운되면, 영향을 받는 데이터만 다른 서버로 재배치.
4) Centralized Cache Coordination
여러 Redis 서버를 사용하는 대신, 중앙 집중식 로드 밸런서를 활용하여 모든 요청이 동일한 Redis 서버로 라우팅되도록 구성할 수도 있습니다.
- 예: HAProxy, Nginx를 활용.
- 클라이언트는 직접 Redis 노드에 접근하지 않고, 로드 밸런서를 통해 접근.
4.3 Redis 클러스터(Redis Cluster)란 무엇이며, 왜 사용하는 것인가요?
- Redis 클러스터는 데이터를 여러 노드에 분산 저장하여 **수평 확장(Scale-Out)**을 지원하는 기능입니다.
- 데이터를 샤딩(Sharding)하여 대규모 데이터 처리를 가능하게 하고, 일부 노드가 장애를 일으켜도 데이터를 복구할 수 있는 **고가용성(High Availability)**을 제공합니다.
# Redis 클러스터 생성
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 --cluster-replicas 1
Redis 클러스터 생성은 Redis를 호출하는 클라이언트 서버(즉, Redis CLI를 실행할 수 있는 서버)에서 한 번의 명령어로 수행됩니다. Redis가 실행되고 있는 각 Redis 서버에서는 별도로 명령어를 입력할 필요가 없습니다.
port가 다른 이유는 무엇인가요?
port 번호가 다른 이유는 Redis 클러스터가 서로 다른 포트를 사용하여 통신하도록 설정하기 때문입니다.
실제로 한 서버에 Redis 인스턴스를 여러 개 실행하려면 포트를 다르게 설정해야 합니다.
노드(Node)란?
- **노드(Node)**는 Redis 클러스터를 구성하는 단일 Redis 서버 인스턴스입니다.
- 따라서, 한 서버에 Redis를 하나 실행하면 그 Redis 서버가 하나의 노드가 됩니다.
예시:
- 서버 1: Redis 서버 하나 실행 → Node A
- 서버 2: Redis 서버 하나 실행 → Node B
- 서버 3: Redis 서버 하나 실행 → Node C
결론적으로, **각각의 Redis 인스턴스(서버)**가 노드로 불립니다.
Redis 클러스터 구성 예시
서버 3개에 Redis를 실행하여 클러스터를 구성한다고 가정:
- 서버 1: Redis 실행 → Node A
- 서버 2: Redis 실행 → Node B
- 서버 3: Redis 실행 → Node C
이 3개의 Redis 인스턴스를 클러스터로 묶으면:
- 데이터가 **해시 슬롯(Hash Slot)**을 기준으로 분산 저장됩니다.
- Node A: 슬롯 0 ~ 5460 담당
- Node B: 슬롯 5461 ~ 10922 담당
- Node C: 슬롯 10923 ~ 16383 담당
이제 **이 세 개의 노드(Node A, Node B, Node C)**가 하나의 Redis 클러스터를 구성합니다.
4.4 Redis Sentinel이란 무엇이며, 어떤 역할을 하나요?
Redis Sentinel은 Redis의 고가용성을 보장하기 위한 모니터링 및 장애 조치(Failover) 도구입니다.
- Redis 인스턴스를 모니터링하고, 마스터 노드가 실패할 경우 자동으로 새 마스터를 선택합니다.
- 클라이언트에게 새로운 마스터 정보를 제공합니다.
sentinel.conf 설정
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
4.5 Redis는 어떤 방식으로 데이터를 샤딩(Sharding)하나요?
Redis는 **슬롯(Slot)**을 사용하여 데이터를 샤딩합니다.
- Redis 클러스터는 0부터 16383까지의 **해시 슬롯(Hash Slot)**으로 데이터를 분할합니다.
- 각 키는 CRC16 해시 값을 기반으로 슬롯에 매핑됩니다.
# 키의 해시 슬롯 확인
redis-cli -c
> CLUSTER KEYSLOT mykey
해시 슬롯(Hash Slot)이란?
데이터를 여러 노드에 분산 저장하기 위한 데이터 분배 단위
Redis 클러스터는 0부터 16383까지 총 16384개의 해시 슬롯으로 데이터를 나눕니다.
Redis 클러스터 전체에 걸쳐 데이터를 분산하기 위한 개념으로,
**마스터 노드(Master Node)**가 해시 슬롯을 담당합니다.
즉, 각 마스터 노드가 자신의 책임 범위에 해당하는 해시 슬롯을 가집니다.
해시 슬롯의 동작 원리
- 키를 해싱: Redis는 키를 저장하거나 검색할 때 CRC16 해시 알고리즘을 사용하여 해시 값을 계산합니다.
- 계산된 해시 값에 16384로 나눈 나머지를 구해 슬롯 번호를 결정합니다.
- 슬롯 번호 = CRC16(key) % 16384
- 슬롯 분배:
- 클러스터의 각 노드는 총 16384개의 슬롯 중 일부를 할당받습니다.
- 예를 들어, 클러스터에 3개의 노드가 있다면 슬롯이 다음과 같이 분배될 수 있습니다:
- Node A: 0 ~ 5460
- Node B: 5461 ~ 10922
- Node C: 10923 ~ 16383
- 슬롯을 기준으로 데이터 저장:
- 각 키는 해시 슬롯에 따라 적절한 노드에 저장됩니다.
- 이렇게 하면 데이터가 샤딩(Sharding) 되어 클러스터 내에서 분산 저장됩니다.
해시 슬롯 분배 예시
1. 클러스터 구성
- Node A (Master): 해시 슬롯 0 ~ 5460
- Node B (Master): 해시 슬롯 5461 ~ 10922
- Node C (Master): 해시 슬롯 10923 ~ 16383
2. 키 저장
- 클러스터는 키를 저장할 때, 키의 해시 슬롯을 계산하여 적절한 마스터 노드에 저장합니다.
- 해시 슬롯 계산: CRC16(key) % 16384
예:
- 키 user:1 → CRC16(user:1) % 16384 = 1234 → Node A (슬롯 0 ~ 5460 담당)
- 키 user:2 → CRC16(user:2) % 16384 = 5678 → Node B (슬롯 5461 ~ 10922 담당)
- 키 user:3 → CRC16(user:3) % 16384 = 11000 → Node C (슬롯 10923 ~ 16383 담당)
해시 슬롯의 역할
- 데이터 분산:
- 해시 슬롯은 데이터를 여러 노드에 균등하게 분산시켜 로드 밸런싱을 제공합니다.
- 확장성:
- 노드를 추가하거나 제거할 때, 해당 노드가 담당할 슬롯만 이동하면 되므로 데이터 이동이 최소화됩니다.
- 장애 복구:
- 특정 노드가 장애를 일으키면, 해당 노드의 슬롯은 다른 노드로 재배치되어 고가용성을 유지합니다.
4.6 레디스 Pipelining? 성능에 어떤 영향을 주나요?
- **파이프라이닝(Pipelining)**은 여러 명령을 한 번에 서버로 전송하고, 응답을 기다리지 않고 처리하는 방식입니다.
- 네트워크 왕복 시간(RTT)을 줄여 성능을 향상시킵니다.
import redis
r = redis.Redis()
pipeline = r.pipeline()
pipeline.set("key1", "value1")
pipeline.set("key2", "value2")
pipeline.execute()
4.7 Redis의 레이턴시(latency)를 줄이는 방법은 무엇인가요?
- 명령어 최적화:
- 복잡한 명령어를 피하고 O(1) 또는 O(log(n)) 복잡도를 가진 명령을 사용.
- 파이프라이닝 사용:
- 네트워크 요청 수를 줄이기 위해 파이프라이닝을 사용.
- RDB 스냅샷 주기 조정:
- 데이터 스냅샷 빈도를 낮춰 I/O 작업을 최소화.
- 배치 처리:
- 데이터를 한꺼번에 처리하여 요청 수를 줄임.
5. 데이터 영속성(Persistence)
5.1 Redis에서 영속성을 제공하는 두 가지 주요 방식은 무엇인가요?
- RDB(Snapshot): 주기적으로 데이터 상태를 스냅샷으로 저장.
- AOF(Append-Only File): 모든 쓰기 작업을 로그로 기록.
5.2 RDB와 AOF의 차이점은 무엇인가요?
5.3 Redis AOF 파일의 크기가 커질 때 어떻게 최적화할 수 있나요? (rewrite 옵션)
Redis는 **AOF 파일 리라이트(AOF Rewrite)**를 통해 파일 크기를 줄입니다.
- Rewrite 과정에서 중복 명령을 제거하고, 최소한의 데이터로 재작성합니다.
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
5.4 RDB 스냅샷이 언제 유용하게 사용되나요?
- 데이터 손실이 허용되는 환경.
- 빠른 재시작이 필요한 경우(스냅샷이 더 빠름).
6. Redis 클러스터 및 분산 시스템
6.1 Redis 클러스터와 Redis Sentinel의 차이점
cluster: 물리적 분리
sentinel: 논리적 분리
6.2 Redis 클러스터에서 파티셔닝(Partitioning)은 어떻게 동작하나요?
- 클러스터는 키를 16384개의 해시 슬롯으로 나눕니다.
- 각 슬롯은 노드에 분배됩니다.
- 키는 CRC16 해시 함수로 슬롯에 매핑됩니다.
6.3 Redis의 Consistent Hashing(일관된 해싱)이란 무엇인가요?
**Consistent Hashing(일관된 해싱)**을 사용하여 특정 키를 항상 동일한 서버에 저장하도록 강제합니다.
- 원리:
- 키를 해싱하여 특정 Redis 서버에 매핑.
- 동일한 키는 항상 동일한 서버에 저장됨.
- 장점:
- 데이터를 수동으로 샤딩하지 않아도 됨.
- 특정 서버가 다운되면, 영향을 받는 데이터만 다른 서버로 재배치.
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHashing {
private final SortedMap<Integer, String> circle = new TreeMap<>();
private final int numberOfReplicas;
public ConsistentHashing(int numberOfReplicas, String[] nodes) {
this.numberOfReplicas = numberOfReplicas;
for (String node : nodes) {
addNode(node);
}
}
public void addNode(String node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.put((node + i).hashCode(), node);
}
}
public String getNode(String key) {
if (circle.isEmpty()) return null;
int hash = key.hashCode();
if (!circle.containsKey(hash)) {
SortedMap<Integer, String> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
public static void main(String[] args) {
String[] redisServers = {"Redis-1", "Redis-2", "Redis-3", "Redis-4"};
ConsistentHashing hashing = new ConsistentHashing(3, redisServers);
System.out.println("Key1 -> " + hashing.getNode("Key1"));
System.out.println("Key2 -> " + hashing.getNode("Key2"));
}
}
- 노드 추가/제거 시 최소한의 데이터 이동으로 데이터를 재분배하는 해싱 방식입니다.
- Redis 클러스터는 슬롯 기반 샤딩을 사용하므로 Consistent Hashing 대신 슬롯 해싱을 활용합니다.
# 입력된 키(mykey)의 해시 슬롯 번호를 반환합니다.
redis-cli CLUSTER KEYSLOT mykey
# 결과
1) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 6379
2) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 6380
# 슬롯 범위
Node A: 0 ~ 5460
Node B: 5461 ~ 10922
노드 추가 시 슬롯 이동
노드 추가 전:
- Node A: 0 ~ 5460
- Node B: 5461 ~ 10922
- Node C: 10923 ~ 16383
노드 D 추가 후:
- Redis 클러스터는 기존 노드에서 슬롯을 일부 이동하여 Node D에 분배합니다.
- 결과:
- Node A: 0 ~ 4095
- Node B: 4096 ~ 8191
- Node C: 8192 ~ 12287
- Node D: 12288 ~ 16383
Redis는 데이터를 새로운 슬롯에 자동으로 이동합니다.
6.4 Redis 클러스터에서 슬롯(Slot)이란 무엇이며, 어떤 역할을 하나요?
해시 테이블
- 슬롯은 Redis 클러스터에서 데이터를 분산 저장하기 위한 단위입니다.
- 0~16383 사이의 슬롯으로 나뉘며, 각 키는 특정 슬롯에 매핑됩니다.
6.5 Redis에서 Pub/Sub과 Redis Streams의 차이점은 무엇인가요?
7. Redis 보안
7.1 Redis의 기본 인증(authentication) 방법
Redis는 기본적으로 비밀번호 기반 인증을 제공합니다.
1. 설정 파일에 비밀번호 설정:
- redis.conf 파일에서 비밀번호를 설정합니다
requirepass your_password
2. 클라이언트에서 인증:
Redis CLI:
redis-cli -a your_password
7.2 Redis를 보안 위협으로부터 보호하는 방법
- 외부 접속 차단:
- Redis를 내부 네트워크로만 접근 가능하게 설정합니다
bind 127.0.0.1
protected-mode yes
- 방화벽 설정:
- Redis 포트를 방화벽으로 보호합니다.
- TLS/SSL 사용:
- 데이터를 암호화하여 전송.
- Redis 서버에서 TLS/SSL을 활성화하려면 인증서와 키 파일이 필요합니다.
- 아래 단계는 TLS 인증서 생성과 Redis 서버 설정 방법을 포함합니다.
1. Redis 서버에 TLS/SSL 설정
- OpenSSL로 인증서 생성
- 인증서 생성 후 파일 준비
- ca.crt: CA 인증서
- redis.crt: Redis 서버 인증서
- redis.key: Redis 서버 키
- Redis 설정 파일(redis.conf)에서 TLS 설정을 추가
# CA 인증서 생성
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=example.com"
# 서버 키와 인증서 서명 요청(CSR) 생성
openssl genrsa -out redis.key 2048
openssl req -new -key redis.key -out redis.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=redis-server"
# 서버 인증서 서명
openssl x509 -req -days 365 -in redis.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out redis.crt
# Redis 설정 파일(redis.conf)에서 TLS 설정을 추가
tls-port 6379 # TLS 포트 설정
port 0 # 비-TLS 포트 비활성화
tls-cert-file /path/to/redis.crt
tls-key-file /path/to/redis.key
tls-ca-cert-file /path/to/ca.crt
tls-auth-clients yes # 클라이언트 인증 요구 (옵션)
# Redis 서버를 재시작하여 TLS 설정을 적용
redis-server /path/to/redis.conf
2. Redis 클라이언트 설정 (TLS/SSL 사용)
Redis CLI 또는 Spring Boot에서 Redis와의 보안 연결을 설정
2.1 Redis CLI에서 TLS 사용
Redis CLI를 사용하여 TLS 연결을 설정하려면 --tls 옵션과 인증서 파일을 지정
redis-cli --tls --cert /path/to/redis.crt --key /path/to/redis.key --cacert /path/to/ca.crt -h <redis-host> -p 6379
2.2 Spring Boot에서 TLS 설정
Spring Boot 애플리케이션에서 Redis와 TLS를 사용하는 방법입니다.
application.yml 또는 application.properties에서 TLS 인증서를 설정
spring:
redis:
host: <redis-host>
port: 6379
ssl: true
ssl-truststore: /path/to/truststore.jks
ssl-truststore-password: password
ssl-keystore: /path/to/keystore.jks
ssl-keystore-password: password
2.3 RedisTemplate를 사용한 연결 설정
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashMap;
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.ssl-keystore}")
private String keyStorePath;
@Value("${spring.redis.ssl-keystore-password}")
private String keyStorePassword;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(10);
poolConfig.setMaxIdle(5);
poolConfig.setMinIdle(1);
// SSL/TLS 설정 추가
HashMap<String, Object> sslConfig = new HashMap<>();
sslConfig.put("javax.net.ssl.keyStore", keyStorePath);
sslConfig.put("javax.net.ssl.keyStorePassword", keyStorePassword);
JedisConnectionFactory factory = new JedisConnectionFactory(poolConfig);
factory.setHostName(redisHost);
factory.setPort(redisPort);
factory.setUseSsl(true); // SSL 활성화
factory.afterPropertiesSet();
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void testConnection() {
redisTemplate.opsForValue().set("key1", "Hello, Redis with TLS!");
String value = redisTemplate.opsForValue().get("key1");
System.out.println("Value from Redis: " + value);
}
}
- ACL 설정:
- 특정 명령어 및 키에 대한 접근 제한.
*ACL (Access Control List)**를 설정하면 사용자별로 특정 명령어와 키 패턴에 대한 접근을 제한할 수 있습니다.
Redis 6.0부터 ACL이 도입되었으며, 사용자 계정을 생성하고 권한을 세부적으로 설정할 수 있습니다.
1. Redis ACL 설정 개념
Redis ACL은 다음을 제어할 수 있습니다:
- 명령어 제한: 특정 명령어(예: GET, SET, DEL) 사용 허용/차단.
- 키 패턴 제한: 특정 키 패턴에만 접근 가능하도록 제한.
- 비밀번호 인증: 각 사용자마다 비밀번호를 설정.
- 읽기/쓰기 권한: +(허용), -(제한), ~(키 패턴), &(속성) 등 ACL 규칙을 사용.
2. Redis CLI를 통한 ACL 설정 예시
2.1 기본 명령어
- ACL 사용자 생성
- on: 계정을 활성화.
- >password: 사용자 비밀번호 설정.
- ~key-pattern: 특정 키 패턴 접근 허용.
- +command: 허용할 명령어 설정.
ACL SETUSER <username> on >password ~<key-pattern> +<command>
# 1) 사용자 생성 및 권한 설정
# "user1" 생성: 특정 명령어(GET, SET) 및 키 패턴 제한
ACL SETUSER user1 on >mypassword ~user:* +GET +SET
# "admin" 생성: 모든 권한 부여
ACL SETUSER admin on >adminpassword ALL
# "readonly" 생성: 읽기만 가능
ACL SETUSER readonly on >readonlypassword ~readonly:* +GET
# 2) 사용자 확인
ACL LIST
# 3) 결과
1) "user user1 on >mypassword ~user:* +GET +SET"
2) "user admin on >adminpassword ALL"
3) "user readonly on >readonlypassword ~readonly:* +GET"
# 1) 사용자 인증 및 데이터 접근
# "user1" 로그인
redis-cli -u user1 -a mypassword
# 허용된 명령어 실행 (성공)
SET user:123 "value1"
GET user:123
# 허용되지 않은 명령어 실행 (실패)
DEL user:123
# Output: "(error) NOPERM this user has no permissions to run the 'DEL' command"
# 2) 키 패턴 제한 테스트
# 허용된 키 패턴 (성공)
SET user:456 "value2"
# 허용되지 않은 키 패턴 (실패)
SET admin:123 "value3"
# Output: "(error) NOPERM this user has no permissions to access this key"
3. Spring Boot를 통한 ACL 인증
Spring Boot에서 Redis ACL 사용자 계정을 사용하려면 Spring Data Redis와 비밀번호 설정을 사용합니다.
3.1 application.yml
spring:
redis:
host: localhost
port: 6379
username: user1
password: mypassword
3.2 RedisTemplate 설정
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void testAclAccess() {
// 허용된 키 패턴
redisTemplate.opsForValue().set("user:123", "value1");
System.out.println(redisTemplate.opsForValue().get("user:123"));
// 허용되지 않은 키 패턴 (예외 발생)
try {
redisTemplate.opsForValue().set("admin:123", "value2");
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}
5. Redis ACL 설정 시 고려사항
- 보안 강화를 위해 사용자별 권한 세분화:
- 민감한 데이터와 일반 데이터를 분리하여 관리.
- 기본 사용자(default user) 비활성화:
- Redis 설치 후 기본적으로 활성화된 default 사용자는 비활성화하는 것이 좋습니다:
ACL SETUSER default off
3. 비밀번호 정책 강화:
- 강력한 비밀번호를 사용하여 무단 접근 방지.
- 최신 버전 유지:
- 최신 보안 패치 적용.
7.3 Redis와 TLS/SSL 설정
Redis 설정 파일 수정:
- redis.conf
- tls-port 6379 port 0 tls-cert-file /path/to/redis.crt tls-key-file /path/to/redis.key tls-ca-cert-file /path/to/ca.crt
클라이언트 연결:
- Redis CLI
redis-cli --tls --cert /path/to/redis.crt --key /path/to/redis.key --cacert /path/to/ca.crt
7.4 Redis의 ACL(Access Control List)
ACL을 통해 사용자별 명령어, 키 패턴, 비밀번호를 제한할 수 있습니다.
ACL 설정:
- Redis CLI:
ACL SETUSER alice on >mypassword ~pattern1 +set +get
- 설정 설명:
- on: 계정 활성화.
- >mypassword: 비밀번호 설정.
- ~pattern1: 특정 키 패턴만 접근 허용.
- +set, +get: 허용할 명령어 지정.
7.5 Redis의 보안 취약점과 방지 방법
- 비밀번호 설정 누락:
- 기본적으로 Redis는 인증 없이 접근 가능.
- 대응: requirepass로 비밀번호 설정.
- 인터넷 노출:
- Redis를 외부에서 접근 가능하게 설정하면 위험.
- 대응: 내부 네트워크에서만 접근 가능하도록 설정.
- RCE(원격 코드 실행):
- Redis를 잘못 설정하면 악성 코드를 실행당할 수 있음.
- 대응: protected-mode 활성화 및 인증 설정.
8. Redis와 데이터 일관성
8.1 Redis는 CAP 이론에서 어디에 속하나요?
Redis는 AP(Availability, Partition tolerance)에 더 가깝습니다.
- A (가용성): 대부분의 상황에서 읽기/쓰기 요청에 응답 가능.
- P (파티션 허용): 네트워크 분할 시에도 일부 데이터가 사용할 수 있음.
- Redis 클러스터는 강한 일관성(C) 대신 가용성을 우선합니다.
8.2 Redis에서 강한 일관성을 보장하려면?
- WAIT 명령어를 사용하여 데이터가 특정 수의 복제본에 기록될 때까지 대기합니다.
- 1: 복제본 최소 개수.
- 1000: 타임아웃(ms).
WAIT 1 1000
8.3 Redis 클러스터에서 쓰기 후 읽기 일관성 보장
동기 복제 활성화:
- min-replicas-to-write와 min-replicas-max-lag 설정
min-replicas-to-write 1
min-replicas-max-lag 1
클라이언트 측 확인:
- 애플리케이션에서 동기화된 데이터만 읽도록 설계.
8.4 Redis에서 멱등성(Idempotency) 구현
멱등성(Idempotency)은 같은 작업을 여러 번 반복해서 수행해도 결과가 동일하게 유지되는 성질
특징
- 멱등성 작업에서는 동일한 입력으로 동일한 요청을 여러 번 처리하더라도, 최종 상태가 항상 동일해야 합니다.
- 네트워크 지연, 재시도 로직, 또는 중복 요청이 발생해도 시스템의 상태가 일관성을 유지하도록 보장합니다.
멱등성의 필요성
- 네트워크 환경에서 중복 요청 처리
- 네트워크 불안정으로 클라이언트가 요청을 재전송하는 경우.
- 서버는 중복 요청으로 인해 잘못된 상태가 발생하지 않아야 합니다.
- 분산 시스템에서 데이터 일관성 보장
- 여러 서비스 간 중복 작업 방지.
- 예: 동일한 주문 요청이 중복 처리되지 않도록.
- 안정성과 신뢰성
- 사용자에게 일관된 결과를 제공하여 신뢰성 향상.
Redis에서 멱등성 구현 방법
Redis를 사용하여 중복 요청을 방지하거나 멱등성 보장을 구현할 수 있습니다.
대표적인 방법은 Unique Key와 TTL(Time-To-Live)을 활용하는 방식입니다.
1. Redis의 SETNX로 중복 요청 방지
Redis의 SETNX (Set if Not Exists) 명령어를 사용하여, 동일한 작업이 중복으로 수행되지 않도록 합니다.
1.1 동작 원리
- SETNX는 키가 존재하지 않을 때만 데이터를 설정합니다.
- 이후 요청에서 동일한 키로 요청하면 데이터가 이미 존재하므로 요청을 무시합니다.
- setIfAbsent
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class IdempotencyService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean processRequest(String requestId, String data) {
// Redis 키 생성
String key = "idempotency:" + requestId;
// SETNX 명령으로 키 설정 (10분 TTL)
Boolean isUnique = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(isUnique)) {
// 요청 처리 (멱등성 보장)
System.out.println("Processing request: " + data);
return true;
} else {
// 중복 요청
System.out.println("Duplicate request detected: " + requestId);
return false;
}
}
}
public static void main(String[] args) {
IdempotencyService service = new IdempotencyService();
// 첫 번째 요청 (처리됨)
service.processRequest("12345", "Order Payment");
// 두 번째 요청 (중복 요청으로 무시됨)
service.processRequest("12345", "Order Payment");
}
Processing request: Order Payment
Duplicate request detected: 12345
2. Redis Lua 스크립트를 사용한 원자적 처리
Redis는 SETNX와 EXPIRE를 별도로 호출하면 원자적이지 않으므로, Lua 스크립트를 사용하여 원자적 처리를 보장할 수 있습니다.
원자성
전체가 성공하거나 전체가 실패
Redis의 원자성
- Redis는 한 번에 하나의 명령만 처리하므로, 클라이언트에서 요청한 명령이 완료되기 전에는 다른 명령이 끼어들지 못합니다.
- 예:
- INCR key (키의 값을 1 증가시키는 명령어)는 항상 원자적으로 실행됩니다.
- 복잡한 작업을 Lua 스크립트로 작성하면 전체 스크립트가 하나의 단일 명령으로 실행되어 원자성이 보장됩니다.
Redis 원자성의 장점
- 간단한 동작 방식: 별도의 락(lock)을 구현할 필요 없음.
- 낮은 CPU 소모: 단일 스레드로 동작하므로 문맥 전환(Context Switching) 오버헤드가 줄어듦.
# INCR 명령은 원자적으로 실행됩니다. 여러 클라이언트가 동시에 요청을 보내더라도 데이터 무결성이 보장
redis-cli SET counter 0
redis-cli INCR counter
redis-cli INCR counter
# 결과
counter = 2
원자성이 CPU에 미치는 영향
긍정적인 측면:
- 재작업 방지:
- 원자성을 보장하면, 작업이 중간에 실패하거나 잘못된 데이터로 인해 재처리할 필요가 없습니다.
- 이는 간접적으로 CPU 사용량을 줄이는 데 기여합니다.
- 락 오버헤드 제거:
- Redis처럼 단일 스레드로 원자성을 보장하는 시스템에서는 복잡한 락 매커니즘 없이도 안전하게 작업을 처리할 수 있습니다.
- 이로 인해 락 경합(lock contention)이나 문맥 전환과 같은 CPU 오버헤드를 피할 수 있습니다.
부정적인 측면:
- 추가 연산:
- 원자성을 보장하기 위해 동기화 및 추가적인 제어 메커니즘이 필요하다면 CPU 사용량이 약간 증가할 수 있습니다.
- 단일 스레드 모델의 병목:
- Redis는 단일 스레드로 원자성을 보장하기 때문에, CPU 성능에 제한이 있을 경우 병목 현상이 발생할 수 있습니다.
2.1 Lua 스크립트
1993년 브라질 PUC-Rio 대학에서 개발되었으며, 속도, 단순성, 유연성이 주요 특징
Lua의 사용 사례
- 게임 개발
- 게임 엔진에서 이벤트 처리나 스크립트 기반 로직을 구현하기 위해 사용됩니다.
- 대표적으로 Unity, Roblox, World of Warcraft 등이 Lua를 사용.
- 임베디드 시스템
- 하드웨어 리소스가 제한된 환경에서 확장 기능을 추가하기 위해 사용됩니다.
- Redis와 Nginx
- Redis에서 Lua를 사용해 복잡한 연산을 처리하는 스크립트를 실행.
- Nginx에서도 Lua를 사용해 고급 요청 처리 로직을 구현.
- 웹 애플리케이션
- 웹 애플리케이션에서 경량화된 서버 로직이나 API를 처리.
local key = KEYS[1]
local value = ARGV[1]
local ttl = ARGV[2]
if redis.call("SETNX", key, value) == 1 then
redis.call("EXPIRE", key, ttl)
return 1
else
return 0
end
- 키가 존재하지 않으면 값을 초기화.
- 키가 존재하면 값을 증가.
2.2 Java 코드
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class IdempotencyServiceWithLua {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LUA_SCRIPT =
"if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then " +
" redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
public boolean processRequest(String requestId, String data) {
String key = "idempotency:" + requestId;
// Lua 스크립트를 실행하여 원자적 처리
Long result = redisTemplate.execute(
(connection) -> connection.eval(
LUA_SCRIPT.getBytes(),
Collections.singletonList(key),
new byte[][]{"1".getBytes(), "600".getBytes()} // 600초 TTL
),
true
);
if (result != null && result == 1L) {
System.out.println("Processing request: " + data);
return true;
} else {
System.out.println("Duplicate request detected: " + requestId);
return false;
}
}
}
9. Redis Pub/Sub 및 Streams
9.1 Redis Pub/Sub의 작동 원리
- Publisher가 특정 채널로 메시지를 보냄.
- Subscriber가 해당 채널을 구독하고 메시지를 수신.
PUBLISH mychannel "Hello, World!"
SUBSCRIBE mychannel
9.2 Pub/Sub의 유용성과 한계
- 유용성: 실시간 알림, 채팅 애플리케이션.
- 한계: 메시지가 영구 저장되지 않음.
9.3 Redis Streams와 Kafka의 차이점
- Redis Streams: 데이터가 Redis 메모리에 저장됨.
- Kafka: 디스크 기반 저장.
9.4 Consumer Group의 역할
- Consumer Group은 메시지를 여러 소비자 간에 분배하여 병렬 처리를 가능하게 합니다.
9.5 Pub/Sub과 Streams의 주요 차이점
edis Pub/Sub에서 여러 Redis 서버 간 통신 문제
- 단일 서버 기준:
- Redis Pub/Sub은 메시지를 같은 Redis 인스턴스에 연결된 클라이언트끼리만 주고받을 수 있습니다.
- 따라서 서버 4대가 각각 독립적으로 실행 중이라면, Pub/Sub 메시지는 각 서버 내에서만 전달됩니다.
- 문제점:
- 여러 Redis 서버를 사용하는 분산 환경에서는 Pub/Sub 메시지가 각 서버에 고립됩니다.
- 예를 들어, 서버 1의 클라이언트가 메시지를 발행(Publish)해도, 서버 2~4의 클라이언트는 이를 수신할 수 없습니다.
Redis Pub/Sub을 여러 서버에서 사용하려면?
1. Redis 클러스터 사용
Redis 클러스터를 구성하면 각 노드가 연결되어 데이터 샤딩 및 일부 통신을 할 수 있습니다.
하지만 Pub/Sub 메시지는 Redis 클러스터 전체에서 자동으로 공유되지 않습니다.
즉, 클러스터 환경에서도 Pub/Sub은 여전히 단일 노드 내에서만 동작합니다.
- 해결 방법: Redis 클러스터가 아닌 다른 통신 방식을 도입해야 합니다.
2. Redis Streams로 대체
Redis Streams는 데이터를 저장하고 여러 소비자 그룹에서 읽을 수 있으므로, 분산 환경에서도 활용 가능합니다.
하지만, Pub/Sub처럼 실시간 메시징 시스템과는 다소 차이가 있습니다.
3. 브로커 역할을 하는 메시지 큐 도입
여러 Redis 인스턴스를 사용하는 경우, Redis Pub/Sub 대신 Kafka, RabbitMQ, 또는 Redis Streams와 같은 메시지 브로커를 사용하는 것이 좋습니다.
이 시스템들은 분산 메시지 전달을 목적으로 설계되었으며, 여러 서버 간 메시지 전달을 보장합니다.
4. Redis Replication으로 해결
Redis Replication(마스터-슬레이브 구조)을 구성하여, 모든 Pub/Sub 메시지를 마스터 서버에서 발행하고, 슬레이브 서버들이 이를 복제받는 방식으로 사용할 수 있습니다.
- 동작 방식:
- 모든 클라이언트는 마스터 Redis 서버를 통해 메시지를 송수신합니다.
- 슬레이브 서버는 데이터를 복제받아 읽기 전용으로 활용.
- 구성 예:
- 서버 1: 마스터 Redis.
- 서버 2~4: 슬레이브 Redis.
- 단점:
- Pub/Sub은 마스터 서버에 부하가 집중될 수 있습니다.
- 메시지의 실시간성 요구가 강한 경우, 복제 딜레이 문제가 발생할 수 있습니다.
1. Redis Pub/Sub 예제
// Producer 코드 (메시지 발행)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class PubSubPublisher {
@Autowired
private StringRedisTemplate redisTemplate;
public void publishMessage(String channel, String message) {
redisTemplate.convertAndSend(channel, message);
System.out.println("Published message: " + message + " to channel: " + channel);
}
}
// Consumer 코드 (메시지 구독)
import org.springframework.stereotype.Component;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
@Component
public class PubSubSubscriber implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println("Received message: " + message.toString());
}
}
// Listener 등록
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@Configuration
public class RedisPubSubConfig {
@Autowired
private PubSubSubscriber pubSubSubscriber;
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 구독할 채널 설정
container.addMessageListener(new MessageListenerAdapter(pubSubSubscriber), new PatternTopic("myChannel"));
return container;
}
}
// 메시지 발행
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class PubSubTestRunner implements CommandLineRunner {
@Autowired
private PubSubPublisher publisher;
@Override
public void run(String... args) throws Exception {
publisher.publishMessage("myChannel", "Hello, Redis Pub/Sub!");
}
}
2. Redis Streams 예제
Redis Streams는 메시지가 저장되므로, 이후에 데이터를 읽거나 여러 소비자 그룹을 만들 수 있습니다.
// Producer 코드 (메시지 발행)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class StreamProducer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void produceMessage(String streamKey, String field, String value) {
ObjectRecord<String, String> record = ObjectRecord.create(streamKey, field, value);
redisTemplate.opsForStream().add(record);
System.out.println("Produced message: " + field + " -> " + value + " to stream: " + streamKey);
}
}
// Consumer 코드 (메시지 소비 - 특정 소비자 그룹)
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StreamConsumer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void consumeMessages(String streamKey, String groupName, String consumerName) {
List<MapRecord<String, String, String>> messages = redisTemplate.opsForStream().read(
Consumer.from(groupName, consumerName),
StreamOffset.latest(streamKey)
);
for (MapRecord<String, String, String> message : messages) {
System.out.println("Consumed message: " + message.getValue());
}
}
}
// 소비자 그룹 생성
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class StreamGroupManager {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void createConsumerGroup(String streamKey, String groupName) {
try {
redisTemplate.opsForStream().createGroup(streamKey, groupName);
System.out.println("Consumer group created: " + groupName);
} catch (Exception e) {
System.out.println("Consumer group already exists or stream is empty.");
}
}
}
// 메시지 발행 및 소비 테스트
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class StreamTestRunner implements CommandLineRunner {
@Autowired
private StreamProducer producer;
@Autowired
private StreamConsumer consumer;
@Autowired
private StreamGroupManager groupManager;
@Override
public void run(String... args) throws Exception {
String streamKey = "myStream";
String groupName = "myGroup";
String consumerName = "consumer1";
// 소비자 그룹 생성
groupManager.createConsumerGroup(streamKey, groupName);
// 메시지 발행
producer.produceMessage(streamKey, "field1", "value1");
producer.produceMessage(streamKey, "field2", "value2");
// 메시지 소비
consumer.consumeMessages(streamKey, groupName, consumerName);
}
}
10. Redis의 고급 주제
10.1 Redis의 Lua 스크립트
- 용도: 원자적 작업 처리.(하나의 작업은 성공/실패)
redis.call("SET", "key", "value")
10.2 Redis 트랜잭션
- MULTI로 시작하고, EXEC로 실행
MULTI
SET key value
INCR counter
EXEC
10.3 비동기 복제(Asynchronous Replication)
- 마스터 노드가 슬레이브 노드에 데이터를 비동기적으로 복제.
Redis에서 비동기 복제는 Master-Slave 구조에서 사용되며, Master 노드가 변경된 데이터를 Slave 노드로 비동기적으로 전파하는 방식입니다. 이 방식은 Redis에서 기본적으로 제공하는 복제(Replication) 메커니즘의 핵심입니다.
- Master-Slave 설정:
- 하나의 Master 노드와 하나 이상의 Slave 노드가 연결됩니다.
- Master는 쓰기 연산을 처리하며, Slave는 Master로부터 복제된 데이터를 기반으로 읽기 요청을 처리합니다.
- 복제 과정:
- Master에서 데이터 변경 발생:
- Master에서 SET, INCR 등 쓰기 작업이 발생하면 이 변경 사항은 Replication Buffer에 저장됩니다.
- Master가 Slave로 데이터를 전송:
- Master는 비동기적으로 Slave에게 데이터를 보냅니다.
- 전송된 데이터는 Slave의 Replication Backlog에 저장됩니다.
- Slave가 데이터 적용:
- Slave는 받은 데이터를 자신의 데이터베이스에 적용합니다.
- Master에서 데이터 변경 발생:
- 비동기적 전송:
- Master는 데이터를 전송한 후 Slave의 처리 상태를 기다리지 않고 즉시 다음 작업을 처리합니다.
- Slave가 데이터를 늦게 수신하거나 적용 실패 시, 잠시 데이터 일관성이 깨질 수 있습니다(일시적인 Eventual Consistency).
2. 비동기 복제 방식
2.1 초기 동기화 (Full Sync)
- Slave가 처음 Master에 연결될 때 수행되는 동작입니다.
- Master는 자신의 전체 데이터를 RDB 스냅샷으로 생성하여 Slave에 전송합니다.
- 동작 과정:
- Slave가 Master에 PSYNC 명령을 보냄.
- Master가 RDB 파일을 생성하고 Slave에게 전송.
- Slave는 RDB 파일을 받아 자신의 데이터베이스를 초기화.
- Master는 동기화 중 발생한 변경 사항(Replication Buffer)을 Slave로 전송.
2.2 부분 동기화 (Partial Sync)
- Slave가 Master와 이미 동기화된 상태에서 일부 변경 사항만 전송받는 방식.
- Master는 Replication Backlog를 사용하여, 연결이 잠시 끊어진 Slave가 필요한 데이터만 전송받을 수 있도록 지원.
- 동작 과정:
- Slave가 Master에 PSYNC 명령을 보냄.
- Master는 Slave의 요청에 따라 필요한 변경 사항을 복제.
- Slave는 변경 사항만 적용하여 데이터베이스를 최신 상태로 유지.
3. 비동기 복제의 장점과 단점
장점:
- 성능 최적화:
- Master는 쓰기 연산 후 Slave의 응답을 기다리지 않으므로 쓰기 작업 성능이 높음.
- Slave는 읽기 요청을 처리함으로써 읽기 부하를 분산.
- 확장성:
- Slave 노드를 쉽게 추가하여 읽기 부하를 분산 가능.
- 데이터 복제는 Master-Slave 간 비동기로 이루어지므로 시스템 확장이 용이.
- Failover 지원:
- Master 장애 발생 시 Slave가 빠르게 Master 역할을 대신할 수 있음(Redis Sentinel 사용 시 자동화 가능).
단점:
- 데이터 손실 가능성:
- Master에서 쓰기 작업이 완료된 직후 Master가 장애로 인해 복제되지 않은 데이터는 Slave에 반영되지 않을 수 있음.
- 일관성 문제:
- Slave는 Master로부터 데이터를 늦게 받을 수 있어, 일시적으로 강한 일관성을 보장하지 못함.
- Slave 의존성:
- Slave는 항상 Master의 변경 사항을 기다려야 하므로, Master 장애 시 데이터 불일치가 발생 가능.
4. Redis 복제의 동작 과정
4.1 기본 복제 흐름
- Slave가 Master에 연결:
- Slave가 PSYNC 명령을 Master로 전송.
- Master는 Slave의 요청에 따라 Full Sync 또는 Partial Sync를 수행.
- Master에서 쓰기 작업 발생:
- Master는 쓰기 작업을 처리한 후, 변경 사항을 Replication Buffer에 저장.
- Replication Buffer의 내용은 Slave로 비동기적으로 전송.
- Slave에서 데이터 복제:
- Slave는 받은 변경 사항을 자신의 데이터베이스에 적용.
- 변경 사항 적용이 완료되면 Slave는 읽기 요청을 처리 가능.
5. 비동기 복제의 설정
Redis에서 비동기 복제를 설정하려면 Master-Slave 구성을 적용해야 합니다.
5.1 Redis Master 설정
Master 노드의 redis.conf 파일에 추가 설정은 필요 없습니다.
5.2 Redis Slave 설정
Slave 노드의 redis.conf 파일에서 Master의 IP와 포트를 지정합니다
replicaof 192.168.1.100 6379
6. Redis 비동기 복제의 한계와 보완
6.1 한계
- 데이터 손실:
- Master에서 Slave로 전송되지 않은 데이터는 Master 장애 시 손실될 수 있음.
- 일관성 부족:
- Slave 데이터가 Master 데이터와 일치하지 않는 상황이 발생 가능(일시적 Eventual Consistency).
6.2 보완 방안
- Redis Sentinel 사용:
- Master 장애 시 자동으로 Slave를 Master로 승격하여 복구 시간을 단축.
- 쓰기 작업 내구성 강화:
- Redis 6.0 이후 추가된 WAIT 명령어를 사용하여 쓰기 작업이 일정 Slave에 반영된 후 응답을 반환
- Strong Consistency가 필요한 경우:
- Redis의 기본 비동기 복제를 포기하고 동기화 복제를 구현하거나, Kafka, RabbitMQ 등과 같은 메시지 큐를 사용하는 것이 적합.
10.4 Bloom Filter
- 용도: 메모리 효율적인 중복 검사.
BF.ADD myfilter value
10.5 RedisJSON
- JSON 데이터를 Redis에 저장 및 처리.
RedisJSON은 Redis의 JSON 데이터 타입을 다룰 수 있는 확장 모듈입니다.
이를 통해 JSON 데이터를 Redis에 저장, 업데이트, 검색할 수 있으며, SQL과 유사한 JSONPath 쿼리를 지원합니다.
RedisJSON은 Redis의 확장 모듈 중 하나로, JSON 데이터를 다루는 작업을 Redis의 빠른 처리 속도와 결합할 수 있게 합니다.
RedisJSON 주요 특징
- JSON 문서 저장: JSON 데이터를 그대로 Redis에 저장 가능.
- JSON 필드 수정: JSON 문서 전체를 덮어쓰지 않고 특정 필드만 업데이트 가능.
- JSONPath 지원: JSONPath 문법으로 JSON 데이터를 검색.
- 통합: 다른 Redis 모듈(RedisSearch, RedisGraph 등)과 함께 사용할 수 있음.
- 빠른 속도: Redis의 기본 성능을 활용.
RedisJSON 설치
docker pull redis/redis-stack:latest
docker run -d --name redis-json -p 6379:6379 redis/redis-stack:latest
# JSON 저장 (SET)
127.0.0.1:6379> JSON.SET user:1 $ '{"name":"Alice","age":30,"skills":["Python","Redis"]}'
OK
# JSON 조회 (GET)
127.0.0.1:6379> JSON.GET user:1
"{\"name\":\"Alice\",\"age\":30,\"skills\":[\"Python\",\"Redis\"]}"
127.0.0.1:6379> JSON.GET user:1 $..name
"[\"Alice\"]"
# JSON 필드 업데이트
127.0.0.1:6379> JSON.SET user:1 $.age 35
OK
127.0.0.1:6379> JSON.GET user:1 $.age
"[35]"
# JSON 배열에 요소 추가
127.0.0.1:6379> JSON.ARRAPPEND user:1 $.skills '"Java"'
(integer) 3
127.0.0.1:6379> JSON.GET user:1 $.skills
"[\"Python\",\"Redis\",\"Java\"]"
# 배열에서 특정 위치에 삽입
127.0.0.1:6379> JSON.ARRINSERT user:1 $.skills 1 '"Docker"'
(integer) 4
127.0.0.1:6379> JSON.GET user:1 $.skills
"[\"Python\",\"Docker\",\"Redis\",\"Java\"]"
# 배열에서 요소 삭제
127.0.0.1:6379> JSON.DEL user:1 $.skills[1]
(integer) 1
127.0.0.1:6379> JSON.GET user:1 $.skills
"[\"Python\",\"Redis\",\"Java\"]"
# JSON 데이터 숫자 필드 증감
127.0.0.1:6379> JSON.NUMINCRBY user:1 $.age 5
OK
127.0.0.1:6379> JSON.GET user:1 $.age
"[40]"
implementation 'org.redisson:redisson-spring-boot-starter:3.20.0'
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class RedisJsonService {
@Autowired
private RedissonClient redissonClient;
// JSON 데이터 저장
public void setJson(String key, String jsonData) {
RBucket<String> bucket = redissonClient.getBucket(key);
bucket.set(jsonData);
System.out.println("JSON data saved: " + jsonData);
}
// JSON 데이터 조회
public String getJson(String key) {
RBucket<String> bucket = redissonClient.getBucket(key);
String jsonData = bucket.get();
System.out.println("JSON data retrieved: " + jsonData);
return jsonData;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class RedisJsonTestRunner implements CommandLineRunner {
@Autowired
private RedisJsonService redisJsonService;
@Override
public void run(String... args) throws Exception {
String key = "user:1";
String jsonData = "{\"name\":\"Alice\",\"age\":30,\"skills\":[\"Python\",\"Redis\"]}";
// JSON 데이터 저장
redisJsonService.setJson(key, jsonData);
// JSON 데이터 조회
redisJsonService.getJson(key);
}
}
JSON data saved: {"name":"Alice","age":30,"skills":["Python","Redis"]}
JSON data retrieved: {"name":"Alice","age":30,"skills":["Python","Redis"]}
RedisJSON 사용 사례
- 사용자 프로필 관리:
- JSON 데이터를 사용하여 사용자 이름, 나이, 선호도 등의 정보를 관리.
- 실시간 로그 관리:
- JSON 데이터 형식으로 로그 데이터를 저장하고 빠르게 검색.
- 상품 데이터 저장:
- 전자상거래 플랫폼에서 상품 정보(가격, 재고, 설명 등)를 JSON 형식으로 저장.
- 복잡한 데이터 모델 관리:
- 관계형 데이터베이스에서 처리하기 어려운 계층적 데이터 구조를 JSON으로 처리.
10.6 Geo 데이터 구조
Redis는 Geo 데이터 구조를 통해 위도(latitude)와 경도(longitude) 정보를 저장하고,
위치 기반 계산(예: 거리, 반경 내 검색)을 제공합니다.
1. 위치 데이터 추가 (GEOADD)
127.0.0.1:6379> GEOADD cities 13.361389 38.115556 "Palermo"
(integer) 1
127.0.0.1:6379> GEOADD cities 15.087269 37.502669 "Catania"
(integer) 1
127.0.0.1:6379> GEOADD cities 12.496365 41.902783 "Rome"
(integer) 1
cities는 Redis 키.
13.361389과 38.115556은 "Palermo"의 경도(longitude)와 위도(latitude).
15.087269과 37.502669은 "Catania"의 좌표.
"Rome"은 12.496365(경도)와 41.902783(위도).
2. 두 지점 간 거리 계산 (GEODIST)
127.0.0.1:6379> GEODIST cities Palermo Catania km
"166.2742"
// "Palermo"와 "Catania" 사이의 거리를 킬로미터 단위로 반환합니다.
단위:
m: 미터
km: 킬로미터
mi: 마일
ft: 피트
3. 반경 내 지점 검색 (GEORADIUS)
127.0.0.1:6379> GEORADIUS cities 13.361389 38.115556 200 km
1) "Palermo"
2) "Catania"
경도 13.361389, 위도 38.115556을 기준으로 반경 200km 내의 지점을 반환.
옵션:
WITHDIST: 거리 포함.
WITHCOORD: 좌표 포함.
ASC/DESC: 거리 기준 정렬.
4. 특정 멤버를 기준으로 반경 내 지점 검색 (GEORADIUSBYMEMBER)
127.0.0.1:6379> GEORADIUSBYMEMBER cities Palermo 200 km WITHDIST
1) 1) "Palermo"
2) "0.0000"
2) 1) "Catania"
2) "166.2742"
"Palermo"를 기준으로 반경 200km 내의 지점을 거리와 함께 반환.
5. 위치 데이터 조회 (GEOPOS)
127.0.0.1:6379> GEOPOS cities Palermo
1) 1) "13.36138904094791412"
2) "38.11555639549629859"
"Palermo"의 정확한 위도, 경도를 반환.
6. GeoHash 값 가져오기 (GEOHASH)
127.0.0.1:6379> GEOHASH cities Palermo
1) "sqc8b49rny0"
"Palermo"의 GeoHash 값을 반환.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class GeoService {
@Autowired
private StringRedisTemplate redisTemplate;
// GEOADD: 위치 데이터 추가
public void addLocation(String key, double longitude, double latitude, String member) {
redisTemplate.opsForGeo().add(key, new org.springframework.data.geo.Point(longitude, latitude), member);
System.out.println("Location added: " + member);
}
// GEODIST: 두 지점 간 거리 계산
public double getDistance(String key, String member1, String member2) {
return redisTemplate.opsForGeo()
.distance(key, member1, member2, DistanceUnit.KILOMETERS)
.getValue();
}
// GEORADIUS: 반경 내 위치 검색
public List<String> getLocationsWithinRadius(String key, double longitude, double latitude, double radius) {
return redisTemplate.opsForGeo()
.radius(key, new org.springframework.data.geo.Point(longitude, latitude), radius)
.getContent()
.stream()
.map(org.springframework.data.geo.GeoResult::getContent)
.toList();
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class GeoTestRunner implements CommandLineRunner {
@Autowired
private GeoService geoService;
@Override
public void run(String... args) throws Exception {
String key = "cities";
// 위치 데이터 추가
geoService.addLocation(key, 13.361389, 38.115556, "Palermo");
geoService.addLocation(key, 15.087269, 37.502669, "Catania");
geoService.addLocation(key, 12.496365, 41.902783, "Rome");
// 거리 계산
double distance = geoService.getDistance(key, "Palermo", "Catania");
System.out.println("Distance between Palermo and Catania: " + distance + " km");
// 반경 내 위치 검색
List<String> locations = geoService.getLocationsWithinRadius(key, 13.361389, 38.115556, 200);
System.out.println("Locations within 200 km: " + locations);
}
}
Location added: Palermo
Location added: Catania
Location added: Rome
Distance between Palermo and Catania: 166.2742 km
Locations within 200 km: [Palermo, Catania]
Redis Geo 데이터 구조의 활용 사례
- 근처 가게 검색:
- 사용자의 현재 위치를 기준으로 반경 내 가게나 매장을 검색.
- 물류/배송 관리:
- 창고, 배송 차량의 위치를 관리하고, 가까운 지점을 찾음.
- 여행 및 지도 서비스:
- 관광지, 호텔, 식당 등을 기준으로 거리 계산 및 반경 내 검색.
- 실시간 서비스:
- 택시 호출, 배달 서비스에서 고객과 드라이버의 거리 계산.
'학습 기록 (Learning Logs) > CS Study' 카테고리의 다른 글
WebSocket (0) | 2025.01.06 |
---|---|
AOP (0) | 2024.12.23 |
[Note]In-memory cache-basic (0) | 2024.12.16 |
Fetch Join (0) | 2024.12.12 |
JPA-troubleShooting (0) | 2024.12.12 |