로블록스 MemoryStore와 DataStore 선택법: Queue·SortedMap·HashMap

DataStore와 MemoryStore는 속도가 빠른 저장소와 느린 저장소의 관계가 아닙니다. 서버가 바뀌어도 잠깐 공유하면 되는 대기열·캐시·실시간 순위에는 MemoryStore가 맞지만, 재접속 뒤에도 남아야 할 재화·인벤토리·진행도는 DataStore가 원본이어야 합니다. 먼저 데이터의 수명을 결정하고, 그다음 조회 방식과 실패 시 복구 경로를 정해야 저장소 선택을 다시 뒤집지 않습니다.

공개 자료를 직접 비교한 범위

2026년 10월 4일 Windows 데스크톱 브라우저에서 Roblox Creator Hub의 Data stores versus memory stores, Memory stores, Queue, Per-partition limits와 MemoryStoreService·HashMap·SortedMap·Queue API 문서를 함께 대조했습니다. 문서마다 기능 목록은 잘 나와 있었지만 실제 설계에서는 “어느 API가 더 빠른가”보다 사라져도 복구 가능한 데이터인가, 어떤 순서로 읽는가, 처리 완료를 어떻게 확정하는가가 먼저라는 결론을 냈습니다. 아래 코드는 그 판단 순서를 재현할 수 있게 최소 단위로 다시 구성했습니다. 실제 Roblox Studio에서 공개 Place를 만들거나 동시 서버 부하를 측정하지는 않았으므로 처리량·지연 수치는 제시하지 않습니다. 자신의 게임에서 정상·실패 경로를 따로 시험해야 합니다.

먼저 한 줄 결론재접속 뒤에도 남아야 하면 DataStore를 원본으로 두세요. 잠깐만 여러 서버가 공유하고 사라져도 원본에서 다시 만들 수 있을 때 MemoryStore를 쓰고, 그 안에서 키 조회는 HashMap, 정렬 조회는 SortedMap, 작업 순서는 Queue를 고릅니다.

1. API 이름보다 데이터 수명을 먼저 적습니다

공식 비교표를 따라가며 가장 먼저 확인한 차이는 지속성입니다. DataStore는 세션이 끝나도 보존할 영구 데이터용이고, MemoryStore는 여러 라이브 서버가 낮은 지연으로 공유하는 임시 데이터용입니다. MemoryStore 항목은 만료 시간을 받아야 하며 공식 문서는 최대 만료 기간을 45일로 설명합니다. “45일까지 남을 수 있다”는 말은 영구 저장을 대신한다는 뜻이 아닙니다.

저장할 값마다 아래 질문을 한 문장으로 답하면 첫 선택이 거의 끝납니다.

  1. 게임을 닫고 며칠 뒤 돌아와도 반드시 남아야 하는가? 그렇다면 DataStore가 원본입니다.
  2. 값이 사라졌을 때 다른 원본이나 현재 상태로 다시 만들 수 있는가? 그렇다면 MemoryStore 후보입니다.
  3. 한 서버에서만 쓰고 서버 종료와 함께 사라져도 되는가? 일반 Luau table이 더 단순합니다.

플레이어 재화, 구매 내역, 인벤토리와 진행도를 MemoryStore에만 저장하면 만료·할당량·서비스 오류 때 복구 근거가 없습니다. 반대로 매칭 대기 참가자를 DataStore에 계속 쓰면 이미 나간 이용자를 정리하고 높은 빈도의 갱신을 감당해야 하므로 목적과 맞지 않습니다.

2. DataStore·MemoryStore·서버 메모리를 한 표로 비교합니다

판단 기준DataStoreMemoryStore서버 Luau table
서버 종료 뒤 보존예. 영구 원본에 적합아니요. TTL이 있는 임시 데이터아니요. 서버와 함께 소멸
다른 라이브 서버와 공유가능하지만 고빈도 임시 조정용은 아님예. 핵심 목적아니요
대표 용도재화·인벤토리·영구 설정·진행도매칭·실시간 순위·교차 서버 캐시·작업 큐현재 라운드 상태·서버 내부 캐시
사라졌을 때 대응재시도·백업·사용자 데이터 보호원본에서 재생성하거나 대기열 재등록현재 서버 상태로 다시 계산
설계의 핵심저장 충돌·요청 예산·종료 저장TTL·중복 처리·파티션·백오프메모리 정리·플레이어 퇴장 정리

자주 읽는 영구 데이터를 빠르게 보여 주고 싶다면 둘 중 하나만 고르는 문제가 아닙니다. DataStore를 진실의 원본으로 유지하고 MemoryStore를 만료 가능한 캐시로 덧붙입니다. 캐시가 비었거나 오류가 나면 DataStore에서 읽고, 결과를 짧은 TTL로 다시 채우는 구조여야 합니다.

3. MemoryStore 안에서는 ‘다음 조회’를 기준으로 고릅니다

MemoryStoreService는 이름으로 HashMap, SortedMap, Queue를 가져옵니다. 같은 게임의 모든 서버가 이름을 공유하므로 개발·운영 환경이나 지역을 구분해야 한다면 이름 또는 키에 명시적으로 포함해야 합니다. 세 구조의 차이는 저장할 값의 모양보다 다음에 어떤 질문으로 읽을지에 있습니다.

구조잘 맞는 질문예시피해야 할 오용
HashMap“이 키의 현재 값은?”서버별 상태, 사용자별 짧은 캐시, 분산 잠금전체 항목을 계속 순회해 순위를 만들기
SortedMap“점수가 높은/낮은 앞 N개는?”실시간 순위, 만료 시각순 예약, 우선 대상 조회하나의 거대한 맵에 모든 지역·모드를 몰기
Queue“먼저 또는 우선 처리할 작업은?”매칭 대기, 비동기 작업 분배, 처리 파이프라인읽은 순간 완료됐다고 가정하기

HashMap은 키별로 분산될 수 있어 독립 키 조회가 많은 데이터에 유리합니다. SortedMap과 Queue는 각각 하나의 파티션을 사용한다는 공식 설명이 있으므로, 트래픽이 커지면 모드·지역·랭크 구간처럼 의미 있는 단위로 여러 구조 이름에 나누는 설계가 필요합니다.

HashMap: 값 하나를 키로 찾을 때

local MemoryStoreService = game:GetService("MemoryStoreService")
local cache = MemoryStoreService:GetHashMap("ProfilePreview:v1")

local key = tostring(player.UserId)
local ttlSeconds = 120

local ok, result = pcall(function()
    return cache:GetAsync(key)
end)

if not ok or result == nil then
    -- DataStore 같은 영구 원본에서 다시 읽은 뒤 캐시를 채웁니다.
    result = loadPreviewFromPermanentStore(player.UserId)
    pcall(function()
        cache:SetAsync(key, result, ttlSeconds)
    end)
end

캐시 쓰기 실패가 영구 데이터 손실로 이어지지 않는 구조가 핵심입니다. 먼저 DataStore 쓰기를 성공시키고 캐시를 지우거나 갱신해야 하며, 캐시 성공을 저장 완료의 증거로 사용하지 않습니다.

SortedMap: 정렬된 앞부분이 필요할 때

local MemoryStoreService = game:GetService("MemoryStoreService")
local board = MemoryStoreService:GetSortedMap("RoundScore:RegionKR:v1")

local ok = pcall(function()
    board:SetAsync(
        tostring(player.UserId),
        { displayName = player.DisplayName, score = score },
        900,
        score
    )
end)

local readOk, leaders = pcall(function()
    return board:GetRangeAsync(Enum.SortDirection.Descending, 20)
end)

value와 sortKey를 구분하세요. 화면에 보여 줄 묶음은 value에, 정렬 기준은 sortKey에 둡니다. 공식 API는 GetRangeAsync 한 번에 요청하는 count를 최대 200으로 제한하므로 전체 이용자를 매번 읽는 설계보다 화면에 필요한 상위 구간만 요청하는 편이 맞습니다.

4. Queue는 읽기와 완료가 다른 두 단계입니다

Queue에서 가장 헷갈린 지점은 ReadAsync()가 항목을 바로 지우지 않는다는 점이었습니다. 읽힌 항목은 Queue를 만들 때 지정한 invisibility timeout 동안 다른 소비자에게 보이지 않습니다. 처리가 성공하면 반환된 id로 RemoveAsync()를 호출하고, 제거하지 못하면 시간이 지난 뒤 다시 보일 수 있습니다.

local MemoryStoreService = game:GetService("MemoryStoreService")
local queue = MemoryStoreService:GetQueue("MatchJobs:KR:v1", 30)

local ok, items, readId = pcall(function()
    local values, id = queue:ReadAsync(1, false, 5)
    return values, id
end)

if ok and #items > 0 then
    local processed = processIdempotently(items[1])
    if processed then
        local removed, removeError = pcall(function()
            queue:RemoveAsync(readId)
        end)
        if not removed then
            warn("Queue 완료 표시 실패", removeError)
        end
    end
end

이 구조는 같은 작업이 다시 나타날 가능성을 없애지 않습니다. 처리에는 고유 jobId를 넣고 영구 원본이나 별도 상태에서 이미 완료된 jobId인지 확인하는 멱등성이 필요합니다. 결제·재화 지급처럼 두 번 실행되면 안 되는 작업은 “Queue에서 한 번만 읽힐 것”에 기대면 안 됩니다.

  • 처리 시간이 invisibility timeout보다 길면 다른 서버가 같은 항목을 다시 읽을 수 있습니다.
  • 처리 성공 뒤 RemoveAsync가 실패해도 항목이 재등장할 수 있습니다.
  • ReadAsync 오류와 빈 배열은 다릅니다. 오류는 재시도 대상이고, 빈 배열은 현재 읽을 항목이 없다는 정상 상태입니다.
  • allOrNothing=true는 요청한 개수를 모두 읽을 수 없을 때 빈 결과가 될 수 있으므로 매칭 규칙과 함께 정합니다.

5. TTL은 정리 기능이면서 복구 약속입니다

MemoryStore의 expiration은 장식이 아닙니다. 너무 짧으면 정상 처리 중 값이 사라지고, 너무 길면 떠난 이용자·오래된 잠금·끝난 라운드가 남아 저장 공간과 판단을 오염시킵니다. TTL을 정할 때는 “평균적으로 얼마나 쓰는가”보다 아래 세 시간을 더 보수적으로 봅니다.

  1. 정상 작업의 최장 예상 시간
  2. 네트워크·서비스 오류 뒤 재시도할 시간
  3. 오래된 항목을 안전하게 무시하거나 다시 만들 수 있는 시점

예를 들어 10초 안에 끝날 것으로 보이는 매칭이라도 서버 이동과 재시도를 고려해 TTL을 더 길게 잡되, 항목 안에 createdAt과 상태 버전을 넣어 너무 오래된 요청은 처리하지 않습니다. TTL을 늘리는 것만으로 오래된 데이터 문제가 해결되지는 않습니다.

원본과 파생 데이터 구분실시간 순위판은 MemoryStore에서 사라져도 이번 라운드 점수나 DataStore 기록으로 다시 만들 수 있어야 합니다. 복구할 자료가 없다면 그 값은 임시 캐시가 아니라 영구 원본일 가능성이 큽니다.

6. 파티션 병목은 ‘요청 수’보다 키가 몰리는 모양을 봅니다

공식 per-partition 문서를 비교하면 HashMap은 여러 파티션에 데이터를 나눌 수 있지만 SortedMap과 Queue는 구조 하나가 단일 파티션입니다. 따라서 전체 게임에서 하나의 GlobalQueue나 AllPlayersLeaderboard만 쓰는 설계는 이용자가 늘 때 한곳으로 요청이 몰릴 수 있습니다.

무조건 무작위로 쪼개기보다 읽는 단위가 보존되는 샤드를 고릅니다.

  • 매칭: 지역 + 게임 모드 + 실력 구간으로 Queue 이름을 나눕니다.
  • 실시간 순위: 시즌 + 지역 또는 서버 그룹으로 SortedMap을 나누고, 전 세계 통합 표가 필요하면 별도 집계 단계를 둡니다.
  • 사용자 캐시: UserId를 HashMap 키로 사용하고, 하나의 인기 키에 모든 상태를 중첩하지 않습니다.

샤드를 너무 잘게 나누면 빈 대기열이 많아져 매칭이 늦고 여러 맵을 합치는 비용이 생깁니다. 먼저 실제 호출량, 파티션별 오류와 대기 시간을 관찰하고 의미 있는 경계부터 분리하세요.

7. 오류 문자열보다 먼저 상태를 바꾸지 않았는지 확인합니다

MemoryStore 공식 문서는 게시되지 않은 Place, Studio API 접근 설정, 잘못된 만료 시간, 요청 제한, UpdateAsync 충돌 같은 상태 코드를 설명합니다. 오류는 모두 같은 재시도로 해결되지 않습니다.

신호먼저 확인할 것권장 대응
UnpublishedPlacePlace가 Roblox에 게시됐는지테스트 Place를 게시한 뒤 다시 시도
InvalidClientAccessStudio에서 API Services 접근이 필요한 테스트인지테스트 전용 복사본과 권한 범위를 확인
InvalidExpirationTimeexpiration이 양수이고 허용 범위인지상수 단위를 초로 통일하고 경계 검사
RequestThrottled반복문·모든 서버가 같은 구조에 몰리는지지수 백오프 + jitter, 호출 합치기, 의미 있는 샤딩
UpdateConflict같은 키를 여러 서버가 동시에 갱신하는지변환 함수를 짧게 유지하고 제한된 횟수로 재시도

재시도 전에는 작업이 이미 일부 성공했는지 판정해야 합니다. SetAsync·RemoveAsync 호출이 실패로 보였다고 곧바로 같은 지급을 반복하면 중복 상태가 생길 수 있습니다. 작업 ID와 상태 전이를 기록하고, 재시도 횟수에 상한을 두며, 일정 횟수 뒤에는 영구 원본에서 다시 읽어 회복하세요.

local function retryWithBackoff(action, maxAttempts)
    local lastError
    for attempt = 1, maxAttempts do
        local ok, result = pcall(action)
        if ok then
            return true, result
        end
        lastError = result
        local delaySeconds = math.min(0.5 * (2 ^ (attempt - 1)), 8)
        task.wait(delaySeconds + math.random() * 0.25)
    end
    return false, lastError
end

이 예제는 모든 오류를 재시도하라는 뜻이 아닙니다. 잘못된 expiration이나 미게시 Place처럼 설정을 고쳐야 하는 오류는 즉시 중단하고, throttling처럼 일시적인 오류에만 제한적으로 적용합니다.

8. 실제 설계는 한 저장소가 아니라 역할 분리로 완성됩니다

사례 A: 교차 서버 매칭

Queue에는 requestId, userId, 모드, 실력 구간, 생성 시각을 넣고 짧은 기간만 보관합니다. 처리 서버는 읽은 참가자의 현재 접속·중복 매칭 여부를 다시 확인한 뒤 Teleport 예약과 상태 확정을 진행합니다. Queue는 대기 순서를 제공하지만 현재 접속과 유효성을 보증하지 않습니다. 서버 이동 코드의 재시도 구조는 TeleportAsync 코드 생성기에서 별도로 점검할 수 있습니다.

사례 B: 라운드 실시간 순위

SortedMap에는 현재 라운드 점수와 sortKey를 짧은 TTL로 넣습니다. 화면은 Descending 상위 20명만 읽습니다. 라운드 보상이나 영구 최고 기록은 서버 검증이 끝난 뒤 DataStore에 별도로 저장합니다. SortedMap이 비면 화면은 “집계 중” 상태를 보여 주고, 보상 지급 여부를 순위 캐시만으로 결정하지 않습니다.

사례 C: 프로필 미리보기 캐시

DataStore가 영구 프로필 원본이고 HashMap은 다른 서버가 반복 조회하는 공개 요약만 잠깐 보관합니다. 프로필 저장 성공 뒤 캐시 키를 제거하거나 새 버전으로 갱신합니다. 캐시 miss와 MemoryStore 오류 모두 DataStore 읽기로 복구하며, DataStore 호출량은 DataStore 저장 실패 점검법과 요청 예산 계산기로 따로 확인합니다.

9. Studio에서는 성공보다 소실·중복·제한을 재현합니다

MemoryStore는 여러 서버 사이 문제를 풀기 위한 서비스라 한 명으로 정상 경로만 눌러서는 구조를 검증하기 어렵습니다. 공개 전 아래 순서로 테스트 계획을 적어 두세요.

  1. 테스트 전용 Place와 구조 이름에 dev 또는 버전을 넣어 운영 데이터와 분리합니다.
  2. Server & Clients 테스트에서 두 서버가 같은 키·Queue를 동시에 갱신하도록 합니다.
  3. Queue 항목을 읽은 뒤 일부러 RemoveAsync를 생략하고 invisibility timeout 뒤 재등장하는지 확인합니다.
  4. 같은 jobId가 두 번 들어와도 결과가 한 번만 적용되는지 검사합니다.
  5. TTL이 지난 HashMap·SortedMap 항목이 없어졌을 때 원본에서 화면이 복구되는지 확인합니다.
  6. 호출을 짧은 시간에 몰아 throttling을 유도하되 운영 게임이 아닌 테스트 환경에서만 수행합니다.
  7. 재시도 중 플레이어가 나가거나 서버가 종료돼도 영구 데이터가 손상되지 않는지 확인합니다.
  8. 로그에는 구조 이름, 작업 ID, 시도 횟수와 오류 종류를 남기고 개인정보·민감한 값은 제외합니다.

통과 기준은 “오류가 한 번도 없음”이 아닙니다. 오류가 나도 영구 원본이 보존되고, 중복 작업이 한 번의 결과만 만들며, 캐시 소실 뒤 복구되고, 재시도가 무한 반복되지 않아야 합니다. 서버에서 들어온 요청 자체의 신뢰 경계는 RemoteEvent 서버 검증 점검법을 함께 적용하세요.

공식 출처·갱신일·적용 범위

아래 Roblox Creator Hub 1차 자료를 2026년 10월 4일 확인했습니다. 공개 Markdown 메타데이터에는 비교·MemoryStore·Queue·파티션·MemoryStoreService 문서가 2026년 10월 2일 21:48 UTC 무렵 갱신으로 표시됐습니다. API 클래스 문서는 웹 페이지에 표시된 현재 시그니처를 같은 날 대조했습니다.

적용 범위: Roblox Studio 제작자가 DataStore, MemoryStore HashMap·SortedMap·Queue와 서버 내부 메모리 중 하나를 선택하고 실패 복구 경로를 설계할 때의 기준입니다. 실제 게임의 CCU, 처리량, 지연, 저장 용량, 요청 예산을 측정한 결과가 아니며 특정 아키텍처의 성능을 보장하지 않습니다. 서비스 한도와 API 동작은 바뀔 수 있으므로 배포 시점의 Creator Hub 문서와 테스트 결과를 우선하세요.

이 글은 비공식 가이드입니다. 직접 실행한 범위와 공식 문서로만 확인한 범위를 구분해 읽고, 계정·결제·연령 관련 최종 기준은 Roblox 공식 안내와 보호자 판단을 우선하세요.
이 글의 순서