Kuma's Curious Paradise
[이룸] Redis에는 어떻게 채팅과 리프레시 토큰이 저장되고 있을까? 본문
📌 Redis에 대해 간략히 설명해 주세요.
Redis는 키-값(key-value) 데이터를 저장하고 관리하기 위한 인메모리 데이터 저장소. 문자열(Strings), 해시(Hashes), 리스트(Lists), 집합(Sets), 정렬된 집합(Sorted Sets) 등 다양한 데이터 구조를 지원.
📌 키-값 구조 데이터를 저장하는 데이터 베이스인데, 어떻게 다양한 데이터 구조를 지원한다는 걸까?
Redis의 '값(value)' 부분은 단순한 문자열 뿐만 아니라, 여러 가지 복잡한 데이터 구조를 지원.
- 문자열(Strings): 텍스트나 숫자. 캐싱, 임시 데이터 저장 등에 사용.
- 리스트(Lists): 순서가 있는 문자열의 집합을 저장. 리스트의 양 끝에 요소를 추가하거나 제거할 수 있어, 큐(Queues)나 스택(Stacks)과 같은 자료구조를 구현하는 데 사용.
- 큐(Queues) 구현
- 큐는 FIFO(First In, First Out) 원칙에 따라 운영되는 선형 자료구조입니다. 즉, 가장 먼저 추가된 요소가 가장 먼저 제거됩니다. Redis의 리스트를 사용하여 큐를 구현할 때는, 새로운 요소를 리스트의 한쪽 끝에 추가(push)하고, 다른 쪽 끝에서 요소를 제거(pop)하여 큐의 동작을 모방할 수 있습니다. 예를 들어, LPUSH 명령어로 리스트의 왼쪽 끝에 요소를 추가하고, RPOP 명령어로 리스트의 오른쪽 끝에서 요소를 제거함으로써 큐를 구현할 수 있습니다.
- 스택(Stacks) 구현
- 스택은 LIFO(Last In, First Out) 원칙에 따라 운영되는 선형 자료구조로, 가장 마지막에 추가된 요소가 가장 먼저 제거됩니다. Redis의 리스트를 사용하여 스택을 구현할 때는, 요소를 리스트의 같은 쪽 끝에 추가하고 제거하는 동작을 반복함으로써 스택의 동작을 모방할 수 있습니다. 예를 들어, RPUSH 명령어로 리스트의 오른쪽 끝에 요소를 추가하고, RPOP 명령어로 동일한 끝에서 요소를 제거함으로써 스택을 구현할 수 있습니다.
- 집합(Sets): 순서를 갖지 않는 유일한 요소들의 모임. SNS 친구 목록, 태그 시스템 등에 사용.
- 소셜 네트워크의 친구 목록: 사용자의 친구 목록을 관리하는 데 집합을 사용할 수 있습니다. 집합을 사용하면 중복 없이 각 친구를 한 번씩만 저장할 수 있으며, 빠른 검색, 친구 추가 및 삭제가 가능합니다.
- 태그 시스템: 게시물이나 상품에 태그를 할당하는 시스템에서 집합을 사용할 수 있습니다. 두 개 이상의 집합 간의 연산(교집합, 합집합, 차집합)을 통해 공통된 태그를 가진 항목을 찾거나 태그 기반 검색을 최적화할 수 있습니다.
- 정렬된 집합(Sorted Sets): 집합의 요소마다 점수를 매겨 순서를 정함.
- 리더보드: 게임이나 다른 경쟁 환경에서 사용자의 점수를 기록하고 순위를 매기는 데 사용됩니다. 각 사용자의 점수가 변경될 때마다 즉시 업데이트되고, 특정 순위의 사용자나 점수 범위를 빠르게 조회할 수 있습니다.
- 시간 기반 이벤트: 이벤트나 메시지에 타임스탬프를 점수로 사용하여, 특정 시간에 발생한 이벤트를 조회하거나 관리할 수 있습니다.
- 해시(Hashes): 키-값 쌍 집합을 저장. 객체나 구조체를 저장하는 데 유용하며, 사용자의 프로필, 게시글의 속성 등을 저장하는 데 씀.
- 사용자 프로필: 사용자의 이름, 이메일, 프로필 사진 URL 등과 같은 정보를 필드와 값으로 저장합니다.
- 게시글 속성: 게시글의 제목, 내용, 작성자, 작성 시간 등을 필드와 값으로 저장하여, 하나의 키(게시글 ID)로 관련 정보를 그룹화합니다.
- 비트맵(Bitmaps) 및 하이퍼로그로그(HyperLogLogs): 비트맵은 각 비트를 개별적으로 조작할 수 있어, 단순한 플래그 세트나 카운팅 등에 사용됨. 하이퍼로그로그는 중복 없는 요소의 개수를 추정하는 데 사용되는 확률적 자료구조로, 대규모 데이터의 카운트에 유용.
- 출석 체크: 사용자의 매일 출석 여부를 표시하는 데 비트맵을 사용할 수 있습니다. 각 비트는 하루를 대표하며, 출석한 날은 1, 불참한 날은 0으로 표시합니다.
- 특성 표시: 사용자나 객체의 여러 특성을 비트로 표현하여, 각 비트의 켜짐/꺼짐 상태로 특정 특성의 유/무를 나타낼 수 있습니다.
- 대규모 데이터의 카운트: 웹사이트 방문자 수, 고유한 이벤트 발생 횟수 등을 추정하는 데 사용됩니다. 정확한 수치보다는 대략적인 개수를 빠르게 알아내야 할 때 유용합니다.
📌 이룸 채팅에서는 Redis를 어떻게 사용했을까?
- 채팅 메시지를 저장하려고 할 때, Redis에서의 처리 과정
- 키 생성: **"chat_room:1234"**와 같이 **CHAT_ROOM_PREFIX**와 **callengeId**를 결합하여 Redis에서 사용할 키를 생성합니다.
- 메시지 저장: 생성된 키와 관련된 리스트의 오른쪽 끝(rightPush)에 새로운 ChatMessage 객체를 추가합니다.
- TTL 설정: 해당 키에 대한 만료 시간(Time-To-Live, TTL)을 30일로 설정합니다. 이는 expire 메소드를 사용하여 수행됩니다.
예를 들어, **challengeId**가 **"1234"**이고, ChatMessage 객체에 **"안녕하세요"**라는 메시지가 담겨 있다고 가정해 보겠습니다.
// 키: "chat_room:1234"
// 값: 리스트 형태로 저장된 채팅 메시지 객체들
[
{"messageId": "1", "sender": "user1", "content": "안녕하세요", "timestamp": "2024-03-14T12:34:56"},
{...},{...},{...}
]
- 실제 어떻게 저장되어 있는지 확인하기
- "{\"messageId\":\"9b644004-e5a8-4821-997c-3ea2d5876edf\",\"type\":\"LEAVE\",\"message\":null,\"sender\":\"\xeb\x9e\x80\xec\xb1\x84\xeb\x8b\xb9\",\"time\":null,\"memberId\":null,\"challengeId\":\"66\",\"profileImageUrl\":null,\"currentMemberList\":null}"
- "{\"messageId\":\"d9080df0-4031-43a7-859f-736035e7be14\",\"type\":\"JOIN\",\"message\":null,\"sender\":\"\xeb\x9e\x80\xec\xb1\x84\xeb\x8b\xb9\",\"time\":\"2024-03-04T21:40:40\",\"memberId\":\"3\",\"challengeId\":\"66\",\"profileImageUrl\":\"https://eroomchallengebucket.s3.amazonaws.com/I7rMCvRnbN_\\xec\\x8a\\xa4\\xed\\x81\\xac\\xeb\\xa6\\xb0\\xec\\x83\\xb7 2024-02-24 192339.png\",\"currentMemberList\":null}"
- "{\"messageId\":\"879dff49-4922-4b98-8e2b-68ddf2dcce24\",\"type\":\"LEAVE\",\"message\":null,\"sender\":\"\xeb\x9e\x80\xec\xb1\x84\xeb\x8b\xb9\",\"time\":null,\"memberId\":null,\"challengeId\":\"66\",\"profileImageUrl\":null,\"currentMemberList\":null}"
- ChatRoomRepository 코드 보기
@Slf4j
@Repository
@Component
public class ChatRoomRepository {
private final RedisTemplate<String, Object> redisTemplate;
private final ListOperations<String, Object> listOperations;
private static final String CHAT_ROOM_PREFIX = "chat_room:";
public ChatRoomRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.listOperations = redisTemplate.opsForList();
}
public List<Object> getChatHistory(String challengeId) {
String key = CHAT_ROOM_PREFIX + challengeId;
return listOperations.range(key, 0, -1);
}
public void saveChatMessage(String challengeId, ChatMessage chatMessage) {
String key = CHAT_ROOM_PREFIX + challengeId;
listOperations.rightPush(key, chatMessage);
// 키의 TTL을 30일로 설정
redisTemplate.expire(key, Duration.ofDays(30));
}
public boolean deleteMessageById(String challengeId, String messageId) {
String key = CHAT_ROOM_PREFIX + challengeId;
List<Object> messages = listOperations.range(key, 0, -1);
for (Object message : messages) {
Map<String, Object> messageMap = (Map<String, Object>) message;
if (messageMap.get("messageId").equals(messageId)) {
listOperations.remove(key, 1, message);
return true;
}
}
return false; // 삭제 실패
}
}
- opsForList(): 리스트 작업을 위한 ListOperations 인터페이스의 인스턴스를 가져옵니다.
- range(key, start, end): 지정된 키의 리스트에서 범위에 해당하는 요소들을 조회합니다. 여기서는 0, -1로 채팅방의 전체 채팅 내역을 가져오는 데 사용됩니다.
- rightPush(key, value): 리스트의 오른쪽 끝에 새로운 요소를 추가합니다. 채팅 메시지를 저장할 때 사용됩니다.
- expire(key, timeout): 지정된 키에 대한 만료 시간을 설정합니다. 여기서는 채팅방의 키에 대해 30일 후에 만료되도록 설정합니다.
- remove(key, count, value): 리스트에서 주어진 값과 일치하는 요소들을 삭제합니다. 여기서는 특정 메시지 ID를 가진 메시지를 삭제하는 데 사용됩니다.
📌 리프레시 토큰 관리에서는 Redis를 어떻게 사용했지?
- 리프레시 토큰을 저장하려고 할 때, Redis에서의 처리 과정
- 키 생성: "<jwtRefreshToken:user@example.com>"와 같이 @RedisHash에서 설정한 value에 keyEmail 값을 결합하여 Redis에서 사용할 키를 생성.
- 메시지 저장: Redis의 해시에 키와 값을 저장. ‘값’으로 RefreshToken 객체의 필드들이 해시(hash) 형태로 저장됨.
- TTL 설정: 해당 키에 대한 만료 시간(Time-To-Live, TTL)을 30일로 설정.
- keyEmail: user@example.com
- refreshToken: abc123xyz
- expiration: 604800
- 해시 키(key): jwtRefreshToken:user@example.com
- 해시 값(value):
- refreshToken: "abc123xyz"
- refreshToken: "abc123xyz"
- 실제 어떻게 저장되어 있는지 확인하기
redis-eroomchallenge.zeyf14.ng.0001.use1.cache.amazonaws.com:6379> HGETALL jwtRefreshToken: [lanchaelog@naver.com](mailto:lanchaelog@naver.com)
1)"refreshToken"
2)"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsYW5jaGFlbG9nQG5hdmVyLmNvbSIsImV4cCI6MTcxMDI1NDAzNCwiaWF0IjoxNzA5NjQ5MjM0fQ.qnZjzz_4Zw3Wj4R0dV7R-Q9LvnoMp_drhMrAOyz9gBs"
3)"keyEmail"
4)"lanchaelog@naver.com"
5)"expiration"
6)"604800"
7)"_class" **// 객체의 자바 클래스 경로. Spring Data Redis가 객체를 직렬화, 역직렬화할 때 참조함**
8)"com.sparta.eroomprojectbe.global.RefreshToken" **// redis에 저장된 데이터가 어떤 자바 객체로 매핑될 수 있는지를 나타냄**
- 7~8번 자세히 : 왜 redis는 객체의 클래스 경로를 저장하지?
객체의 클래스 정보를 저장하여, Redis에서 데이터를 읽어올 때 해당 데이터를 올바른 클래스의 객체로 역직렬화하기 위함. - 왜 "_class" 필드가 필요한가?
Redis는 기본적으로 키-값 저장소이며, 저장되는 값의 타입 정보를 내장하지 않는다. 따라서 "_class" 필드를 통해 객체를 Redis에 저장할 때 해당 객체의 클래스 정보를 함께 저장함으로써, 나중에 데이터를 다시 객체로 복원할 때 필요한 타입 정보를 제공한다.
- RefreshToken 코드 보기
@Getter
@NoArgsConstructor
@RedisHash(value = "jwtRefreshToken", timeToLive = 60*60*24*7)
public class RefreshToken {
@Id
private String keyEmail;
private String refreshToken;
@TimeToLive
private long expiration; // 초 단위
public RefreshToken(String keyEmail, String refreshToken) {
this.keyEmail = keyEmail;
this.refreshToken = refreshToken;
this.expiration = 604800L;
}
public void updateToken(String newRefreshToken) {
this.refreshToken = newRefreshToken;
this.expiration = 604800L;
}
}
📌 Redis는 어떤 때에 사용하는 걸까?
- 캐싱: 데이터베이스, API 호출 또는 페이지 렌더링 결과와 같은 비용이 많이 드는 연산 결과를 저장하여, 다음 요청 때 빠르게 결과를 제공할 수 있다.
- 세션 관리: 웹 애플리케이션에서 사용자 세션 정보를 저장하여 빠른 접근과 세션 유지를 가능하게 한다.
- 메시징 및 큐: Redis의 리스트, 세트 등을 사용하여 메시지 큐 시스템을 구현할 수 있다. 이를 통해 백그라운드 작업 스케줄링, 비동기 처리 등을 할 수 있다.
- 실시간 애플리케이션: 채팅, 실시간 분석, 위치 기반 서비스 등 실시간으로 데이터를 처리하고 반응해야 하는 애플리케이션에서 사용된다.
Redis의 특징 중 하나는, 데이터를 메모리에 저장하기 때문에 매우 빠른 데이터 읽기 및 쓰기 속도를 제공한다는 점. 또한, 필요에 따라 디스크에 데이터를 지속적으로 저장할 수 있으며, 데이터 복제, 자동 파티셔닝 등을 통해 높은 가용성과 확장성을 제공한다.
'이룸 프로젝트' 카테고리의 다른 글
[이룸] 전역 에러 처리 리팩토링 (0) | 2024.03.27 |
---|---|
[이룸] 채팅 내역 저장을 위한 db 선택 (0) | 2024.03.21 |
이룸 프로젝트의 한 챕터를 마치며 (0) | 2024.03.13 |
[이룸] 240311 최종 발표 (0) | 2024.03.13 |
[이룸] 240308 SSE 알림 기능 (0) | 2024.03.13 |