Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Kuma's Curious Paradise

[이룸] Redis에는 어떻게 채팅과 리프레시 토큰이 저장되고 있을까? 본문

이룸 프로젝트

[이룸] Redis에는 어떻게 채팅과 리프레시 토큰이 저장되고 있을까?

쿠마냥 2024. 3. 19. 00:24

📌 Redis에 대해 간략히 설명해 주세요.

Redis는 키-값(key-value) 데이터를 저장하고 관리하기 위한 인메모리 데이터 저장소. 문자열(Strings), 해시(Hashes), 리스트(Lists), 집합(Sets), 정렬된 집합(Sorted Sets) 등 다양한 데이터 구조를 지원.

 

📌 키-값 구조 데이터를 저장하는 데이터 베이스인데, 어떻게 다양한 데이터 구조를 지원한다는 걸까?

Redis의 '값(value)' 부분은 단순한 문자열 뿐만 아니라, 여러 가지 복잡한 데이터 구조를 지원.

  1. 문자열(Strings): 텍스트나 숫자. 캐싱, 임시 데이터 저장 등에 사용.

  2. 리스트(Lists): 순서가 있는 문자열의 집합을 저장. 리스트의 양 끝에 요소를 추가하거나 제거할 수 있어, 큐(Queues)나 스택(Stacks)과 같은 자료구조를 구현하는 데 사용.
    • 큐(Queues) 구현
    • 큐는 FIFO(First In, First Out) 원칙에 따라 운영되는 선형 자료구조입니다. 즉, 가장 먼저 추가된 요소가 가장 먼저 제거됩니다. Redis의 리스트를 사용하여 큐를 구현할 때는, 새로운 요소를 리스트의 한쪽 끝에 추가(push)하고, 다른 쪽 끝에서 요소를 제거(pop)하여 큐의 동작을 모방할 수 있습니다. 예를 들어, LPUSH 명령어로 리스트의 왼쪽 끝에 요소를 추가하고, RPOP 명령어로 리스트의 오른쪽 끝에서 요소를 제거함으로써 큐를 구현할 수 있습니다.
    • 스택(Stacks) 구현
    • 스택은 LIFO(Last In, First Out) 원칙에 따라 운영되는 선형 자료구조로, 가장 마지막에 추가된 요소가 가장 먼저 제거됩니다. Redis의 리스트를 사용하여 스택을 구현할 때는, 요소를 리스트의 같은 쪽 끝에 추가하고 제거하는 동작을 반복함으로써 스택의 동작을 모방할 수 있습니다. 예를 들어, RPUSH 명령어로 리스트의 오른쪽 끝에 요소를 추가하고, RPOP 명령어로 동일한 끝에서 요소를 제거함으로써 스택을 구현할 수 있습니다.
    (→ 채팅 내역을 저장할 때 데이터 구조로 ‘리스트’를 선택한 이유. 채팅은 시간순으로 출력돼야 하기 때문에 순서가 중요. 또한 끝에 계속해서 데이터가 추가되므로 이것이 가능한 리스트를 이용.)

  3. 집합(Sets): 순서를 갖지 않는 유일한 요소들의 모임. SNS 친구 목록, 태그 시스템 등에 사용.
    • 소셜 네트워크의 친구 목록: 사용자의 친구 목록을 관리하는 데 집합을 사용할 수 있습니다. 집합을 사용하면 중복 없이 각 친구를 한 번씩만 저장할 수 있으며, 빠른 검색, 친구 추가 및 삭제가 가능합니다.
    • 태그 시스템: 게시물이나 상품에 태그를 할당하는 시스템에서 집합을 사용할 수 있습니다. 두 개 이상의 집합 간의 연산(교집합, 합집합, 차집합)을 통해 공통된 태그를 가진 항목을 찾거나 태그 기반 검색을 최적화할 수 있습니다.
  4. 정렬된 집합(Sorted Sets): 집합의 요소마다 점수를 매겨 순서를 정함.
    • 리더보드: 게임이나 다른 경쟁 환경에서 사용자의 점수를 기록하고 순위를 매기는 데 사용됩니다. 각 사용자의 점수가 변경될 때마다 즉시 업데이트되고, 특정 순위의 사용자나 점수 범위를 빠르게 조회할 수 있습니다.
    • 시간 기반 이벤트: 이벤트나 메시지에 타임스탬프를 점수로 사용하여, 특정 시간에 발생한 이벤트를 조회하거나 관리할 수 있습니다.
  5. 해시(Hashes): 키-값 쌍 집합을 저장. 객체나 구조체를 저장하는 데 유용하며, 사용자의 프로필, 게시글의 속성 등을 저장하는 데 씀.
    • 사용자 프로필: 사용자의 이름, 이메일, 프로필 사진 URL 등과 같은 정보를 필드와 값으로 저장합니다.
    • 게시글 속성: 게시글의 제목, 내용, 작성자, 작성 시간 등을 필드와 값으로 저장하여, 하나의 키(게시글 ID)로 관련 정보를 그룹화합니다.
    (→ 리프레시 토큰은 데이터 구조로 ‘해시’를 선택. 사용자의 리프레시 토큰뿐만 아니라, 사용자 ID, 토큰 만료 시간 등과 같은 메타 데이터를 함께 구조화하여 저장할 수 있음.)

  6. 비트맵(Bitmaps) 및 하이퍼로그로그(HyperLogLogs): 비트맵은 각 비트를 개별적으로 조작할 수 있어, 단순한 플래그 세트나 카운팅 등에 사용됨. 하이퍼로그로그는 중복 없는 요소의 개수를 추정하는 데 사용되는 확률적 자료구조로, 대규모 데이터의 카운트에 유용.
    • 출석 체크: 사용자의 매일 출석 여부를 표시하는 데 비트맵을 사용할 수 있습니다. 각 비트는 하루를 대표하며, 출석한 날은 1, 불참한 날은 0으로 표시합니다.
    • 특성 표시: 사용자나 객체의 여러 특성을 비트로 표현하여, 각 비트의 켜짐/꺼짐 상태로 특정 특성의 유/무를 나타낼 수 있습니다.
    • 대규모 데이터의 카운트: 웹사이트 방문자 수, 고유한 이벤트 발생 횟수 등을 추정하는 데 사용됩니다. 정확한 수치보다는 대략적인 개수를 빠르게 알아내야 할 때 유용합니다.

 

📌 이룸 채팅에서는 Redis를 어떻게 사용했을까?

  • 채팅 메시지를 저장하려고 할 때, Redis에서의 처리 과정
    1. 키 생성: **"chat_room:1234"**와 같이 **CHAT_ROOM_PREFIX**와 **callengeId**를 결합하여 Redis에서 사용할 키를 생성합니다.
    2. 메시지 저장: 생성된 키와 관련된 리스트의 오른쪽 끝(rightPush)에 새로운 ChatMessage 객체를 추가합니다.
    3. TTL 설정: 해당 키에 대한 만료 시간(Time-To-Live, TTL)을 30일로 설정합니다. 이는 expire 메소드를 사용하여 수행됩니다.

      예를 들어, **challengeId**가 **"1234"**이고, ChatMessage 객체에 **"안녕하세요"**라는 메시지가 담겨 있다고 가정해 보겠습니다.
// 키: "chat_room:1234"
// 값: 리스트 형태로 저장된 채팅 메시지 객체들
[
    {"messageId": "1", "sender": "user1", "content": "안녕하세요", "timestamp": "2024-03-14T12:34:56"},
    {...},{...},{...}
]

 

  • 실제 어떻게 저장되어 있는지 확인하기
    1. "{\"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}"
    2. "{\"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}"
    3. "{\"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에서의 처리 과정
    1. 키 생성: "<jwtRefreshToken:user@example.com>"와 같이 @RedisHash에서 설정한 valuekeyEmail 값을 결합하여 Redis에서 사용할 키를 생성.
    2. 메시지 저장: Redis의 해시에 키와 값을 저장. ‘값’으로 RefreshToken 객체의 필드들이 해시(hash) 형태로 저장됨.
    3. TTL 설정: 해당 키에 대한 만료 시간(Time-To-Live, TTL)을 30일로 설정.

    예를 들어, RefreshToken 객체가 다음과 같은 값으로 구성되어 있다고 가정해 보자: 이 경우 Redis에 저장된 데이터 구조는 대략 다음과 같이 표현될 수 있다:
    • 해시 키(key): jwtRefreshToken:user@example.com
    • 해시 값(value):
      • refreshToken: "abc123xyz"

    이때, expiration 값은 객체가 Redis에 저장될 때 TTL 설정에 사용되므로, 실제 해시 내에는 나타나지 않는다. 대신, 이 값은 해당 Redis 해시의 생존 시간을 설정하는 데 사용된다. 즉, EXPIRE 명령을 통해 604800초(7일) 후에 자동으로 만료되도록 설정된다.

  • 실제 어떻게 저장되어 있는지 확인하기
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의 특징 중 하나는, 데이터를 메모리에 저장하기 때문에 매우 빠른 데이터 읽기 및 쓰기 속도를 제공한다는 점. 또한, 필요에 따라 디스크에 데이터를 지속적으로 저장할 수 있으며, 데이터 복제, 자동 파티셔닝 등을 통해 높은 가용성과 확장성을 제공한다.