-
Notifications
You must be signed in to change notification settings - Fork 2
Redis Schema
Redis 는 해당 프로젝트에서 다양한 방식으로 사용됩니다. 단순한 캐싱, 임시 데이터 저장, 최신 데이터 버퍼링, append-only 로깅 등에 사용되며 이를 위해 다양한 자료구조를 사용하고 있습니다.
다만, 아직 단순 캐싱에 대해서는 사용하지 않고 있습니다. 이에 대한 건 추후 논의가 필요해 보입니다.
- String : 단순한 Key - value 구조
"user:1:name" -> "상혁"
"config:maxSession" -> "1000"- hash : Hash Key 아래 여러 Field–Value 구조
"user:1" -> {
"name": "상혁",
"age": "25",
"role": "manager"
}- zset : Score 기반 정렬된 Field–Value 구조
"rank:studyTime" -> {
("user1", 120),
("user2", 95),
("user3", 300)
}- stream : Key 아래 Record ID 와 Record(Field–Value) 목록 구조
"chat:room:1" -> [
"1701234567890-0" : { "sender": "1", "msg": "안녕" },
"1701234567891-0" : { "sender": "2", "msg": "ㅎㅇ" }
]또한 최적화 및 원자적 수행(트랜잭션)을 위해 Lua Script 를 사용하고 있으며, Hash 자료구조를 보다 간단히 사용하기 위해 RedisHashRepository 등을 정의하였습니다.
RedisHashRepository 문서
공식 redis 문서
각 채팅방 별로, 유저가 가장 마지막으로 읽은 메시지에 대한 데이터가 필요합니다. 이를 통하여 언리드 메시지 카운트(안읽은 메시지 개수), 언리드 유저 카운트(안읽은 유저 수), 로그 시작 지점 등을 알 수 있습니다.
| 타입 | 설명 |
|---|---|
| 자료구조 | hash |
| 엔티티 | UserLastReadMessage |
| 라이브러리 | RedisHashRepository(custom) |
| 접두사 | lastRead: |
| 접미사 | 사용하지 않음 |
| key 데이터 | 채팅방 아이디 |
| 저장 데이터 정의 | 유저 id -> 채팅 id |
| TTL | 정의되지 않음 |
| Lock 여부 | lock 사용하지 않음 |
"lastRead:86f4dcf0-ce82-11f0-9639-b969e772b3dd" -> {
"1" : "14047c8d1045b000",
"32" : "14047c906745b000",
"5" : "14047c90ae85b000",
"152" : "14047c9122c5b000"
}읽기의 경우, 해당 전체 데이터를 가져옵니다. RedisHashRepository 의 findById 를 사용해 해당 데이터를 엔티티로 매핑하여 반환합니다.
쓰기의 경우, 특정 필드의 데이터만 변경합니다. RedisHashRepository 의 saveMapById 등을 통해 데이터를 덮어씌웁니다. 해당 메서드는 hash key, field key, field value 를 통해 저장을 수행합니다.
각 채팅방에 대해, 상위 N 개의 채팅 내역을 캐싱합니다. 채팅 로그 조회 및 언리드 메시지 카운트를 정확히 세기 위해 사용됩니다.
| 타입 | 설명 |
|---|---|
| 자료구조 | stream |
| 엔티티 | 정의되지 않음 |
| 라이브러리 | RedisTemplate |
| 접두사 | chat:msg:room: |
| 접미사 | 사용하지 않음 |
| key 데이터 | 채팅방 아이디 |
| 저장 데이터 정의 | RecordId(ChatId) -> {id : chatId, type : chatType... } |
| TTL | 정의되지 않음 |
| Lock 여부 | lock 사용하지 않음 |
"chat:room:86f4dcf0-ce82-11f0-9639-b969e772b3dd" -> [
"1701234567890-0" : { "id" : "14047c8d1045b000", "type" : "TEXT", "sender": "1", "message" : "안녕" },
"1701234567891-0" : { "id" : "14047c8d1045b020", "type" : "TEXT", "sender": "12", "message" : "방가" },
...
]stream 자료구조는 일종의 append-only log 처럼 작동하는 자료구조이며 record key 를 통해 시점을 관리합니다. record key는 다음과 같은 제약이 있습니다.
- 보통 record key 는 redis 에서 자동으로 생성합니다.
- 형식은
[timestamp]-[sequence]이며,-가 들어가 있지 않으면 파싱 오류가 나옵니다. - 새로운 데이터를 삽입하기 위해선, 해당 stream 의 최신 데이터 record key 보다 큰 데이터만 가능합니다.(정의 상, 저장된 최신 데이터보다 이전의 데이터는 삽입할 수 없다)
- record key 는 오로지 정수 및
-로만 구성되어 있어야 하며, 문자는 포함될 수 없습니다.
이를 통해, chatId 를 record key 로 파싱하여 저장하고 있습니다. chatId 자체가 snowflake 기반으로 작성되었기에 record key 와 성질이 비슷합니다. 다만 데이터 형식이 다르기에 인코딩/디코딩 과정을 필요로 합니다.
chatId 는 64비트 정수를 16진수 hex string 으로 변환한 문자열이므로 이를 다시 정수 -> concat("-0") 을 하면 record id 로 만들어집니다.
recordId 는 반대로substring(0, indexOf("-"))-> 16진수 문자열로 변환하면 chatId 가 됩니다.
또한, stream 은 저장되어 있는 데이터의 최대 길이를 지정할 수 있는데, 여기에는 trimming 전략이 있습니다. trimming 을 사용하지 않는다면 redis 는 삽입이 일어날 때마다 길이를 확인하여 항상 최대 길이를 정확하게 유지합니다. trimming 을 사용한다면, redis 는 최대 길이를 넘더라도, 여유롭게 길이를 확인하여 최대 길이를 엇비슷하게 유지합니다.
해당 stream 자료구조도 trimming 전략을 사용 중이며, 현재 대략 최대 길이 값(100) + 60 개 까지 존재할 수 있습니다.
ChatMessageCacheRepositoryImpl 에 대한 내용은 PR #111 를 확인해주세요.
사용자가 공부를 시작하고 종료할 때까지, 시작 시간 및 기타 데이터를 임시로 저장합니다. 해당 데이터는 사용자가 공부를 시작하는 순간 생성되며, 공부가 종료되어 데이터가 영속화되면 삭제됩니다. Hash 자료구조를 사용하였으나 RedisHashRepository 가 아닌, Redis template 에서 제공하는 기능을 사용하였습니다.
| 타입 | 설명 |
|---|---|
| 자료구조 | hash |
| 엔티티 | StudyStatus |
| 라이브러리 | RedisTemplate |
| 접두사 | studyStatus: |
| 접미사 | 사용하지 않음 |
| key 데이터 | 사용자 ID |
| 저장 데이터 정의 | 사용자, 공부 시작 시간, 종류... |
| TTL | 1 Day |
| Lock 여부 | lock 사용하지 않음 |
"studyStatus:101" -> {
"studying" : 1,
"startTime" : "2025-09-11:07:30:11",
"studyTime" : 0,
"name" : "",
"categoryId" : 533,
"goal" : 12000
}각 항목은 다음과 같습니다.
- studying : 공부 중 여부. 현재는 무조건 1(true) 지만 추후 기능을 위해 남겨놓음
- startTime : 공부 시작 시간
- categoryId : 공부 중인 카테고리의 카테고리 아이디(RDB :
study_category) - name : categoryId 가 null 인 경우, 임시 카테고리 이름
- goal : 목표 시간
- studyTime : 누적 공부 시간(시작 시점 기준), 그러나 사용하지 않음 - 추후 확장을 위해 남겨놓음
해당 status 는 `study_time` 테이블(RDB) 와 대응되며, 보다 자세한 내용 및 처리 전략 등은 [RDB study_time](https://github.com/study-pals/backend/wiki/RDB-Schema#study_time) 에서 확인할 수 있습니다.
access token 재발급을 위한 refresh token 의 경우, mysql 이 아닌 redis 에서 관리하고 있습니다. 해당 방식에 대한 타당성은 토의가 필요할 듯 합니다.
| 타입 | 설명 |
|---|---|
| 자료구조 | hash |
| 엔티티 | RefreshToken |
| 라이브러리 | RedisTemplate |
| 접두사 | refreshToken: |
| 접미사 | 사용하지 않음 |
| key 데이터 | 사용자 ID |
| 저장 데이터 정의 | id 에 따른 refresh token |
| TTL | expire 필드(refresh token 전략에 따라) |
| Lock 여부 | lock 사용하지 않음 |
"refreshToken:101" -> {
"token" : "some refresh token"
}채팅 시 구독 및 메시지 전송에 대한 인가 시 필요한 데이터를 관리합니다. 사용자가 특정 토픽에 대해 구독을 하게 되면, 해당 세션 아이디 및 구독 경로를 저장하여 잘못된 경로에 대한 접근을 차단합니다.
| 타입 | 설명 |
|---|---|
| 자료구조 | hash |
| 엔티티 | UserSubscribeInfo |
| 라이브러리 | RedisHashRepository(custom) |
| 접두사 | userSubscribeInfo: |
| 접미사 | 사용하지 않음 |
| key 데이터 | 세션 아이디 |
| 저장 데이터 정의 | 세션 당 구독한 채팅방 정보 - 권한 |
| TTL | 정의되지 않음 |
| Lock 여부 | lock 사용하지 않음 |
"userSubscribeInfo:e9dfe739-2e7c-4934-a516-b67deb6e5cfb" -> {
"a5922d1c-a856-4cc5-b26c-8f845bbec256" : 17,
"26c783a0-7334-4533-a9af-93b463d6dbfd" : 15
}해당 데이터의 생명주기는 유저가 구독을 시작한 순간 부터, 구독 해제(unsubscribe) 혹은 연결 끊김(disconnect) 까지 입니다. 어플리케이션에서 명시적으로 해당 필드를 제거해 주어야 합니다. 해당 데이터를 통해, 유저가 잘못된 메시지 전송(구독하지 않은 채팅방에 메시지를 보내는 행위) 등을 막을 수 있습니다.
내부 값 중 key 는 채팅방 아이디이며, value 는 해당 유저가 해당 채팅방에서의 권한을 명시하나, 아직 구현되지 않았습니다.
그룹에 참가하기 위한 방법 중 하나는, 그룹의 관리자가 생성한 가입 코드를 통해 참가하는 것입니다. 이때, 관리자가 생성하는 코드는 redis 에 저장됩니다.
| 타입 | 설명 |
|---|---|
| 자료구조 | hash |
| 엔티티 | GroupEntryCode |
| 라이브러리 | RedisTemplate |
| 접두사 | entryCode: |
| 접미사 | 사용하지 않음 |
| key 데이터 | 생성된 코드 |
| 저장 데이터 정의 | 코드 및 그룹 ID |
| TTL | 정의되지 않음(정의 필요) |
| Lock 여부 | lock 사용하지 않음 |
"entryCode:E4FOQ" -> {
"id" : 12
}가입 코드 및 해당 코드를 생성한 그룹의 ID 정보가 포함되어 있습니다. 유저가 해당 코드를 기반으로 그룹을 가입하게 되면, 이 값을 기반으로 유효성 검증 및 어떤 그룹인지에 대한 정보를 파악할 수 있습니다.