구현 예제
출시 예정
데이터베이스는 2026년 1월 출시 예정입니다.
데이터베이스를 활용하여 실제 서비스에서 자주 사용되는 시스템을 구현하는 예제입니다.
1. 공유 데이터 관리 (예: 월드 보스)
전체 유저가 공유하는 데이터를 관리하고, 개별 유저의 기여도를 기록하는 시스템입니다. (예: 월드 보스 레이드, 서버 전체 목표 달성 이벤트 등)
데이터 모델
[Table("world_boss", TableType.FlexibleTable, ClientAccess = true, ReadPermissions = new[] { TablePermission.OTHERS }, WritePermissions = new[] { TablePermission.OTHERS })]
public class WorldBoss : BaseModel
{
[PrimaryKey(AutoIncrement = true)]
[Column(NotNull = true)]
public int Id { get; set; }
[Column(NotNull = true, DefaultValue = "0")]
public long TotalDamage { get; set; } = 0; // 누적 데미지
}
[Table("boss_raid_record", TableType.UserTable, ClientAccess = true, ReadPermissions = new[] { TablePermission.SELF }, WritePermissions = new[] { TablePermission.SELF })]
public class RaidRecord : BaseModel
{
[PrimaryKey]
[Column(NotNull = true)]
public string InDate { get; set; } // 유저 식별자 (inDate)
[Column(NotNull = true)]
public int BossId { get; set; }
[Column(NotNull = true, DefaultValue = "0")]
public long TotalDamage { get; set; } = 0;
[Column]
public DateTime? LastAttackTime { get; set; }
}
데이터 조회 및 갱신
성능 팁
Where 절에서 BossId로 조회하는 경우, 해당 컬럼에 인덱스를 설정하면 조회 성능이 향상됩니다. 인덱스 생성 가이드
public async Task AttackBoss(int bossId, long damage)
{
// 1. 차트(정적 데이터)에서 보스 정보(MaxHp) 조회
long maxHp = 100000;
// 2. 현재 보스 상태 조회 (누적 데미지)
var boss = await DBClient.From<WorldBoss>()
.Where(b => b.Id == bossId)
.FirstOrDefault();
if (boss == null)
{
// 보스 정보가 없으면 새로 생성
boss = new WorldBoss
{
Id = bossId,
TotalDamage = 0
};
await DBClient.From<WorldBoss>().Insert(boss);
}
// 3. 데미지 계산
long validDamage = damage;
if (boss.TotalDamage + damage > maxHp)
{
validDamage = maxHp - boss.TotalDamage;
}
if (validDamage <= 0) return; // 이미 처치됨
// 4. 보스 누적 데미지 업데이트
var result = await DBClient.From<WorldBoss>()
.Where(b => b.Id == bossId)
.Inc(b => b.TotalDamage, validDamage)
.Exec();
if (result.AffectedRows == 0) return; // 동시성 처리: 이미 처치된 경우
// 5. 기여도(데미지) 기록 업데이트
var myRecord = await DBClient.From<RaidRecord>()
.Where(r => r.BossId == bossId)
.FirstOrDefault();
if (myRecord == null)
{
// 첫 기록 생성
myRecord = new RaidRecord
{
BossId = bossId,
TotalDamage = validDamage,
LastAttackTime = DateTime.UtcNow
};
await DBClient.From<RaidRecord>().Insert(myRecord);
}
else
{
// 기록 갱신
myRecord.TotalDamage += validDamage;
myRecord.LastAttackTime = DateTime.UtcNow;
await DBClient.From<RaidRecord>()
.Where(r => r.InDate == myRecord.InDate)
.Update(myRecord);
}
}
2. 점수 및 랭킹 시스템 (예: ELO 레이팅)
유저 간의 경쟁 결과에 따라 점수를 계산하고 갱신하는 시스템입니다. 이 예제는 ELO Rating System 공식을 적용하여, 실력 차이에 따른 공정한 점수 변동을 구현합니다.
데이터 모델
[Table("battle_record", TableType.UserTable, ClientAccess = true, ReadPermissions = new[] { TablePermission.SELF, TablePermission.OTHERS }, WritePermissions = new[] { TablePermission.SELF, TablePermission.OTHERS })]
public class BattleRecord : BaseModel
{
[PrimaryKey]
[Column(NotNull = true)]
public string InDate { get; set; } // 유저 식별자 (inDate)
[Column]
public string Nickname { get; set; }
[Column(NotNull = true, DefaultValue = "1000")]
public int MMR { get; set; } = 1000; // 기본 점수
[Column(NotNull = true, DefaultValue = "0")]
public int Wins { get; set; } = 0;
[Column(NotNull = true, DefaultValue = "0")]
public int Losses { get; set; } = 0;
}
점수 계산 및 결과 처리
public class EloSystem
{
// ELO 변동폭 계산 (K-Factor: 가중치, 보통 32 사용)
private int CalculateEloChange(int myRating, int opponentRating, bool isWin, int kFactor = 32)
{
// 승리 기대 확률 계산
double expectedScore = 1.0 / (1.0 + Math.Pow(10.0, (opponentRating - myRating) / 400.0));
// 실제 결과 (승리: 1.0, 패배: 0.0)
double actualScore = isWin ? 1.0 : 0.0;
// 변동 점수 계산
return (int)(kFactor * (actualScore - expectedScore));
}
// 매치 결과 처리 함수
public async Task ProcessMatchResult(string myInDate, string opponentInDate, bool isWin)
{
// 1. 두 유저의 정보 조회
var me = await DBClient.From<BattleRecord>()
.Where(x => x.InDate == myInDate)
.FirstOrDefault();
var opponent = await DBClient.From<BattleRecord>()
.Where(x => x.InDate == opponentInDate)
.FirstOrDefault();
if (me == null || opponent == null) return;
// 2. 점수 변동 계산
int scoreChange = CalculateEloChange(me.MMR, opponent.MMR, isWin);
// 3. 점수 및 전적 반영 (Inc/Dec 사용)
if (isWin)
{
// 승리: 내 MMR 증가 + 승리 횟수 증가
await DBClient.From<BattleRecord>()
.Where(x => x.InDate == me.InDate)
.Inc(x => x.MMR, scoreChange)
.Inc(x => x.Wins, 1)
.Exec();
// 패배: 상대 MMR 감소 + 패배 횟수 증가
await DBClient.From<BattleRecord>()
.Where(x => x.InDate == opponent.InDate)
.Dec(x => x.MMR, scoreChange)
.Inc(x => x.Losses, 1)
.Exec();
}
else
{
// 패배: 내 MMR 감소 + 패배 횟수 증가
await DBClient.From<BattleRecord>()
.Where(x => x.InDate == me.InDate)
.Dec(x => x.MMR, scoreChange)
.Inc(x => x.Losses, 1)
.Exec();
// 승리: 상대 MMR 증가 + 승리 횟수 증가
await DBClient.From<BattleRecord>()
.Where(x => x.InDate == opponent.InDate)
.Inc(x => x.MMR, scoreChange)
.Inc(x => x.Wins, 1)
.Exec();
}
}
}
3. 리더보드 연동
데이터베이스와 리더보드를 연동하여 점수를 업데이트하면서 동시에 리더보드를 갱신하는 예제입니다.
리더보드란?
뒤끝 베이스에서 제공하는 순위 기능입니다. 데이터베이스의 테이블과 연결하여 점수 기반 순위를 관리할 수 있습니다. 자세한 내용은 리더보드 문서를 참고하세요.
데이터 모델
[Table("boss_leaderboard", TableType.UserTable, ClientAccess = true, ReadPermissions = new[] { TablePermission.SELF }, WritePermissions = new[] { TablePermission.SELF })]
public class BossLeaderboard : BaseModel
{
[PrimaryKey]
[Column(NotNull = true)]
public string Indate { get; set; } // 유저 식별자 (inDate)
[Column(NotNull = true, DefaultValue = "0")]
public int Score { get; set; } = 0;
}
점수 업데이트 및 리더보드 갱신
public async Task UpdateBossLeaderboard()
{
// 1. 내 리더보드 데이터 조회
var row = await DBClient.From<BossLeaderboard>().ToList();
if (row.Count == 0)
{
// 2-1. 데이터가 없으면 새로 삽입
var leaderboardEntry = new BossLeaderboard
{
Indate = Backend.UserInDate,
Score = 0
};
await DBClient.From<BossLeaderboard>().Insert(leaderboardEntry);
}
else
{
// 2-2. 데이터가 있으면 점수 업데이트 + 리더보드 갱신
row[0].Score += 100;
Param param = new Param();
param.Add("score", row[0].Score);
// 리더보드 갱신 (데이터베이스 업데이트 + 리더보드 순위 반영)
var bro = Backend.Leaderboard.User.UpdateMyDataAndRefreshLeaderboard(
"리더보드 uuid", // 뒤끝 콘솔에서 생성한 리더보드의 uuid
"boss_leaderboard", // 테이블 이름
row[0].GetPrimaryKey(), // 업데이트할 row의 inDate (PrimaryKey)
param
);
}
}
핵심 포인트
- UpdateMyDataAndRefreshLeaderboard: 테이블 데이터 업데이트와 리더보드 갱신을 한 번에 처리합니다.
- 리더보드 uuid: 뒤끝 콘솔에서 리더보드 생성 후 확인하거나, 모든 유저 리더보드 정보 조회 함수로 확인할 수 있습니다.
- 사전 조건: 리더보드를 갱신하려면 해당 테이블에 먼저 1회 Insert하여 row를 생성해야 합니다.
4. 진행도 관리 (예: 퀘스트)
유저별 작업 진행 상황을 저장하고 관리합니다. (예: 퀘스트, 업적, 일일 미션 등)
데이터 모델
[Table("quest_progress", TableType.UserTable, ClientAccess = true, ReadPermissions = new[] { TablePermission.SELF }, WritePermissions = new[] { TablePermission.SELF })]
public class QuestProgress : BaseModel
{
[PrimaryKey(AutoIncrement = true)]
[Column(NotNull = true)]
public long Id { get; set; }
[Column]
public int? QuestId { get; set; }
[Column(NotNull = true, DefaultValue = "0")]
public int CurrentCount { get; set; } = 0; // 진행도
[Column(NotNull = true, DefaultValue = "0")]
public int TargetCount { get; set; } = 0; // 목표치
[Column(NotNull = true, DefaultValue = "false")]
public bool IsCompleted { get; set; } = false;
[Column(NotNull = true, DefaultValue = "false")]
public bool IsRewardClaimed { get; set; } = false;
}
진행도 업데이트
성능 팁
자주 조회하는 QuestId에 인덱스를 설정하면 조회 성능이 향상됩니다. 인덱스 생성 가이드
public async Task UpdateQuestProgress(int questId, int addCount)
{
// 내 퀘스트 정보 조회
var quest = await DBClient.From<QuestProgress>()
.Where(q => q.QuestId == questId)
.FirstOrDefault();
if (quest != null && !quest.IsCompleted)
{
// 진행도 증가
quest.CurrentCount += addCount;
// 목표 달성 시 완료 처리
if (quest.CurrentCount >= quest.TargetCount)
{
quest.CurrentCount = quest.TargetCount;
quest.IsCompleted = true;
}
await DBClient.From<QuestProgress>()
.Where(q => q.Id == quest.Id)
.Update(quest);
}
}