Nakama 게임 서버로 인게임 친구 시스템 구현하기: 상태 머신부터 실시간 동기화까지
오픈소스 게임 서버 Nakama를 활용해 인게임 친구 요청, 수락, 차단 상태 머신을 구축하고 클라이언트 연동 및 실시간 접속 상태(Presence) 동기화 파이프라인을 최적화하는 실무 가이드입니다.

핵심 요약
Nakama(v3.x+)는 PostgreSQL 기반의 관계형 데이터 모델을 통해 친구 요청, 수락, 차단 상태를 4단계 정수형 State(0: Friend, 1: Invite Sent, 2: Invite Received, 3: Blocked)로 관리하는 기본 프렌드 시스템을 제공합니다. 클라이언트는 gRPC/WebSocket API를 호출하여 친구 목록과 페이징 커서를 조회하고 서버 런타임(Go/TypeScript) 후크(Hook)와 Notification/Status 스트림을 결합하여 최대 친구 수 제한 검증과 실시간 온라인 상태 동기화를 안전하게 구현할 수 있습니다.
Nakama 친구 시스템의 데이터 구조와 상태 머신
멀티플레이 게임에서 친구 시스템은 단순한 일대일 매핑이 아닙니다. 요청 대기, 상호 수락, 거절, 차단 등 다양한 관계 상태를 일관성 있게 관리해야 합니다. Heroic Labs의 오픈소스 게임 서버인 Nakama는 이러한 소셜 그래프 처리를 내장 프로토콜 버퍼(Protobuf)와 관계형 테이블(user_edge)을 통해 지원합니다.
Nakama 내부에서 두 유저 간의 관계는 다음과 같은 4가지 상태 코드(State)로 표현됩니다.
| 상태 코드 (State) | 상태 명칭 | 설명 |
|---|---|---|
0 | Friend (상호 친구) | 양방향 요청 및 수락이 완료되어 상호 친구 목록에 노출되는 상태 |
1 | Invite Sent (보낸 요청) | 가 에게 친구 요청을 보낸 상태 ( 기준) |
2 | Invite Received (받은 요청) | 가 로부터 친구 요청을 받은 상태 ( 기준) |
3 | Blocked (차단됨) | 특정 유저와의 상호작용 및 친구 요청을 차단한 상태 |
flowchart TD
A[None / 비관계] -->|AddFriend 요청| B[Invite Sent / Received]
B -->|상대방 AddFriend 수락| C[Friend 상호 친구]
B -->|DeleteFriends 거절/취소| A
C -->|DeleteFriends 삭제| A
A -->|BlockFriends 차단| D[Blocked 차단]
B -->|BlockFriends 차단| D
C -->|BlockFriends 차단| D
D -->|DeleteFriends 차단 해제| A
유저 가 에게 AddFriends API를 호출하면 Nakama 데이터베이스의 user_edge 테이블에 두 레코드가 원자적(Atomic)으로 기록됩니다. 에게는 position=1(Invite Sent), 에게는 position=2(Invite Received) 레코드가 생성됩니다. 이후 가 동일하게 를 대상으로 AddFriends를 호출하면 두 레코드의 상태가 모두 0(Friend)으로 갱신됩니다.

인게임 친구 시스템을 어떻게 구현할까? 단계별 연동 가이드
Unity C# 클라이언트와 Nakama Go 서버 런타임을 기준으로 친구 시스템을 구축하는 표준 4단계 절차를 살펴봅니다.
1단계: 클라이언트 세션 연결 및 친구 목록 페이징 조회
로그인된 세션을 기반으로 친구 목록을 가져옵니다. 친구 수가 많은 유저를 고려하여 반드시 limit과 cursor를 활용한 페이징 조회를 구현해야 합니다.
using Nakama;
using System.Threading.Tasks;
using UnityEngine;
public class FriendManager : MonoBehaviour
{
private IClient _client;
private ISession _session;
public async Task LoadFriendListAsync(int limit = 50, string cursor = null)
{
// limit: 한 페이지에 가져올 친구 수, state: null(전체) 또는 특정 상태 필터
IApiFriendList result = await _client.ListFriendsAsync(_session, null, limit, cursor);
foreach (var friend in result.Friends)
{
string userId = friend.User.Id;
string username = friend.User.Username;
switch (friend.State)
{
case 0: // 상호 친구
Debug.Log($"[Friend] {username} ({userId})");
break;
case 1: // 보낸 요청
Debug.Log($"[Outgoing Request] {username}");
break;
case 2: // 받은 요청
Debug.Log($"[Incoming Request] {username}");
break;
case 3: // 차단
Debug.Log($"[Blocked] {username}");
break;
}
}
// 다음 페이지 커서 보관
if (!string.IsNullOrEmpty(result.Cursor))
{
string nextCursor = result.Cursor;
}
}
}
2단계: 친구 요청 전송 및 수락 처리
유저 ID(UUID) 또는 유저네임(Username)을 기반으로 친구 요청을 보냅니다. 상대방이 수락할 때도 동일한 AddFriendsAsync API를 호출합니다.
public async Task SendOrAcceptFriendRequestAsync(string targetUserId)
{
var ids = new string[] { targetUserId };
// 대상 유저 ID로 요청/수락 실행
await _client.AddFriendsAsync(_session, ids);
Debug.Log($"친구 요청 또는 수락 완료: {targetUserId}");
}
public async Task RemoveOrDeclineFriendAsync(string targetUserId)
{
var ids = new string[] { targetUserId };
// 친구 삭제, 받은 요청 거절, 보낸 요청 취소에 공통 사용
await _client.DeleteFriendsAsync(_session, ids);
Debug.Log($"친구 삭제 또는 요청 거절 완료: {targetUserId}");
}
3단계: 서버 런타임에서 최대 친구 수 검증 (Go Runtime Hook)
Nakama Go 런타임의 BeforeAddFriends 후크로 추가 요청을 검증할 수 있습니다. 아래 예시는 현재 상호 친구 수를 확인하는 사전 검사일 뿐, 한 요청의 여러 대상이나 동시에 도착한 요청까지 포함한 엄격한 최대치 보장은 아닙니다. 하드 리밋이 필요하면 모든 추가·수락 경로를 직렬화하는 서버 권위 처리와 원자적 저장소 검증을 설계해야 합니다.
package main
import (
"context"
"database/sql"
"errors"
"github.com/heroiclabs/nakama-common/api"
"github.com/heroiclabs/nakama-common/runtime"
)
const MaxFriendLimit = 100
func BeforeAddFriendsHook(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error) {
userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
if !ok {
return nil, errors.New("invalid user context")
}
// 예시용 사전 검사: 동시 요청이나 이번 요청의 여러 대상을 포함하지 않음
friends, _, err := nk.FriendsList(ctx, userId, 1000, nil, "")
if err != nil {
logger.Error("친구 목록 조회 실패: %v", err)
return nil, err
}
friendCount := 0
for _, f := range friends {
if f.GetState().GetValue() == 0 {
friendCount++
}
}
if friendCount >= MaxFriendLimit {
return nil, runtime.NewError("친구 최대 보유 한도를 초과했습니다.", 3) // 3: INVALID_ARGUMENT
}
return in, nil
}
// InitModule에서 후크 등록
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
if err := initializer.RegisterBeforeAddFriends(BeforeAddFriendsHook); err != nil {
return err
}
return nil
}

4단계: 실시간 접속 상태(Presence) 및 인앱 알림 구독
소켓(Socket) 세션을 활성화하여 친구가 로그인/로그아웃하거나 상태 메시지를 변경할 때 실시간 알림을 수신합니다.
public async Task InitializeStatusTrackingAsync(ISocket socket, string[] friendUserIds)
{
// 실시간 상태 변경 이벤트 리스너 등록
socket.ReceivedStatusPresence += presenceEvent =>
{
foreach (var join in presenceEvent.Joins)
{
Debug.Log($"[Online] {join.Username} ({join.Status})");
}
foreach (var leave in presenceEvent.Leaves)
{
Debug.Log($"[Offline] {leave.Username}");
}
};
// 친구 목록의 유저 ID 배열을 전달하여 실시간 상태 구독
var result = await socket.FollowUsersAsync(friendUserIds);
foreach (var presence in result.Presences)
{
Debug.Log($"초기 접속 상태: {presence.Username} -> {presence.Status}");
}
}
대규모 트래픽 환경에서의 성능 및 데이터베이스 최적화
동시 접속자가 수만 명 이상인 라이브 서비스에서는 친구 시스템이 데이터베이스와 네트워크 부하의 주요 원인이 될 수 있습니다.
- PostgreSQL 복합 인덱스 활용: Nakama의
user_edge테이블은(source_id, state, position)복합 인덱스를 사용하여 친구 조회를 고속 처리합니다. 커스텀 쿼리나 통계 조회가 필요할 경우 테이블 풀 스캔(Full Scan)이 발생하지 않도록 실행 계획(EXPLAIN ANALYZE)을 점검해야 합니다. - 상태 구독(FollowUsers) 개수 최적화: 클라이언트가 수백 명의 친구 상태를 동시에 구독하면 소켓 브로드캐스트 패킷 양이 급증합니다. UI상 현재 화면에 표시되는 상위 20~30명 단위로만
FollowUsersAsync를 부분 갱신하고 미표시 유저는 구독을 해제(UnfollowUsersAsync)하는 가상화(Virtualization) 기법이 필수적입니다. - 알림(Notification) 푸시 지연 완화: 친구 요청 전송 시 즉각적인 푸시 알림은 Nakama 내장 Notification 시스템(
nk.NotificationSend)을 사용하여 비동기 큐 방식으로 전송함으로써 API 응답 지연을 방지합니다.
자주 묻는 질문 (FAQ)
Q1. 차단된 유저(State = 3)가 나에게 다시 친구 요청을 보낼 수 있나요?
Nakama 내부 로직상 한쪽이 상대를 차단하면 user_edge에 차단 상태가 기록됩니다. 차단당한 유저가 AddFriends를 호출하더라도 Nakama 서버는 에러를 반환하지 않고 요청을 조용히 무시(Silent Drop)하거나 거부 처리하여 차단자의 프라이버시를 보호합니다.
Q2. 스팀(Steam)이나 페이스북(Facebook) 친구 목록을 Nakama로 가져올 수 있나요?
네, 가능합니다. Nakama는 ImportSteamFriendsAsync 및 ImportFacebookFriendsAsync 클라이언트 API를 기본 지원합니다. 유저가 스팀 토큰이나 페이스북 토큰으로 소셜 로그인을 완료한 후 해당 API를 호출하면 동일 게임을 플레이하는 외부 소셜 플랫폼 친구를 Nakama 친구 관계로 자동 동기화합니다.
Q3. 친구 삭제 시 양방향 데이터가 모두 삭제되나요?
DeleteFriends를 실행하면 요청을 보낸 유저와 대상 유저 사이의 양방향 user_edge 레코드가 단일 트랜잭션 내에서 모두 삭제되어 비관계 상태로 복구됩니다.



