feat : RDB 기능 추가 (라이브러리, base 코드 구현)

This commit is contained in:
qornwh1
2026-03-01 16:58:38 +09:00
parent 563448a09a
commit 34394f2c96
14 changed files with 410 additions and 0 deletions

View File

@@ -11,12 +11,21 @@
<ItemGroup>
<PackageReference Include="LiteNetLib" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServerLib\ServerLib.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.Configuration;
using MMOserver.Game;
using MMOserver.RDB;
using Serilog;
namespace MMOserver;
@@ -9,6 +11,15 @@ class Program
{
// .MinimumLevel.Warning() // Warning 이상만 출력 배포시
IConfigurationRoot config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("config.json", optional: true) // 로컬 개발용
.AddEnvironmentVariables() // 도커 배포용
.Build();
// DB 연결
// DbConnectionFactory dbFactory = new DbConnectionFactory(config);
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
Log.Logger = new LoggerConfiguration()

View File

@@ -0,0 +1,31 @@
using System.Data;
using Microsoft.Extensions.Configuration;
using MySql.Data.MySqlClient;
using ServerLib.RDB.Database;
namespace MMOserver.RDB;
public class DbConnectionFactory : IDbConnectionFactory
{
private readonly string connectionString;
public DbConnectionFactory(IConfiguration config)
{
IConfigurationSection db = config.GetSection("Database");
connectionString = $"Server={db["Host"]};" +
$"Port={db["Port"]};" +
$"Database={db["Name"]};" +
$"User={db["User"]};" +
$"Password={db["Password"]};" +
$"Pooling=true;" +
$"MinimumPoolSize={db["Pooling:MinimumPoolSize"]};" +
$"MaximumPoolSize={db["Pooling:MaximumPoolSize"]};" +
$"ConnectionTimeout={db["Pooling:ConnectionTimeout"]};" +
$"ConnectionIdleTimeout={db["Pooling:ConnectionIdleTimeout"]};";
}
public IDbConnection CreateConnection()
{
return new MySqlConnection(connectionString);
}
}

View File

@@ -0,0 +1,48 @@
using MMOserver.RDB.Models;
using MMOserver.RDB.Services;
using ServerLib.RDB.Handlers;
namespace MMOserver.RDB.Handlers;
public class TestHandler
{
private readonly TestService _testService;
public TestHandler(TestService testService) => _testService = testService;
public async Task<string> GetTestAsync(int id)
{
Test? result = await _testService.GetTestAsync(id);
return result is null
? HandlerHelper.Error("Test not found")
: HandlerHelper.Success(result);
}
public async Task<string> GetTestByUuidAsync(string uuid)
{
Test? result = await _testService.GetTestByUuidAsync(uuid);
return result is null
? HandlerHelper.Error("Test not found")
: HandlerHelper.Success(result);
}
public async Task<string> GetAllTestsAsync()
{
return HandlerHelper.Success(await _testService.GetAllTestsAsync());
}
public async Task<string> CreateTestAsync(int testA, string testB, double testC)
{
return HandlerHelper.Success(await _testService.CreateTestAsync(testA, testB, testC));
}
public async Task<string> UpdateTestAsync(int id, int? testA, string? testB, double? testC)
{
return HandlerHelper.Success(await _testService.UpdateTestAsync(id, testA, testB, testC));
}
public async Task<string> DeleteTestAsync(int id)
{
return HandlerHelper.Success(await _testService.DeleteTestAsync(id));
}
}

View File

@@ -0,0 +1,16 @@
using Dapper.Contrib.Extensions;
namespace MMOserver.RDB.Models;
// 테이블 entity
[Table("test")]
public class Test
{
[Key]
public int Id { get; set; }
public int TestA { get; set; }
public string? TestB { get; set; }
public double TestC { get; set; }
public string? Uuid { get; set; } // UNIQUE
}

View File

@@ -0,0 +1,18 @@
using MMOserver.RDB.Models;
using ServerLib.RDB.Database;
using ServerLib.RDB.Repositories;
namespace MMOserver.RDB.Repositories;
// 실제 호출할 쿼리들
public class TestRepository : ARepository<Test>
{
public TestRepository(DbConnectionFactory factory) : base(factory)
{
}
public async Task<Test?> GetByUuidAsync(string uuid)
{
return await QueryFirstOrDefaultAsync("SELECT * FROM test WHERE uuid = @Uuid", new { Uuid = uuid });
}
}

View File

@@ -0,0 +1,57 @@
using MMOserver.RDB.Models;
using MMOserver.RDB.Repositories;
namespace MMOserver.RDB.Services;
public class TestService
{
private readonly TestRepository repo;
public TestService(TestRepository repo)
{
this.repo = repo;
}
public Task<Test?> GetTestAsync(int id)
{
return repo.GetByIdAsync(id);
}
public Task<Test?> GetTestByUuidAsync(string uuid)
{
return repo.GetByUuidAsync(uuid);
}
public Task<IEnumerable<Test>> GetAllTestsAsync()
{
return repo.GetAllAsync();
}
public async Task<long> CreateTestAsync(int testA, string testB, double testC)
{
if (string.IsNullOrWhiteSpace(testB))
{
throw new ArgumentException("TestB is required");
}
return await repo.InsertAsync(new Test { TestA = testA, TestB = testB, TestC = testC, Uuid = Guid.NewGuid().ToString() });
}
public async Task<bool> UpdateTestAsync(int id, int? testA, string? testB, double? testC)
{
Test entity = await repo.GetByIdAsync(id) ?? throw new KeyNotFoundException("Test not found");
entity.TestA = testA ?? entity.TestA;
entity.TestB = testB ?? entity.TestB;
entity.TestC = testC ?? entity.TestC;
return await repo.UpdateAsync(entity);
}
public async Task<bool> DeleteTestAsync(int id)
{
Test entity = await repo.GetByIdAsync(id) ?? throw new KeyNotFoundException("Test not found");
return await repo.DeleteAsync(entity);
}
}

View File

@@ -0,0 +1,15 @@
{
"Database": {
"Host": "localhost",
"Port": "0000",
"Name": "XXXX",
"User": "root",
"Password": "11212121",
"Pooling": {
"MinimumPoolSize": "5",
"MaximumPoolSize": "100",
"ConnectionTimeout": "30",
"ConnectionIdleTimeout": "180"
}
}
}

View File

@@ -0,0 +1,8 @@
using System.Data;
namespace ServerLib.RDB.Database;
public interface IDbConnectionFactory
{
public IDbConnection CreateConnection();
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json;
namespace ServerLib.RDB.Handlers;
public static class HandlerHelper
{
public static string Success<T>(T data)
{
return JsonSerializer.Serialize(new Response<T> { Success = true, Data = data });
}
public static string Error(string message)
{
return JsonSerializer.Serialize(new Response<object> { Success = false, Error = message });
}
public static T? Deserialize<T>(string payload)
{
try { return JsonSerializer.Deserialize<T>(payload); }
catch { return default; }
}
}

View File

@@ -0,0 +1,8 @@
namespace ServerLib.RDB.Handlers;
public class Response<T>
{
public bool Success { get; init; }
public T? Data { get; init; }
public string? Error { get; init; }
}

View File

@@ -0,0 +1,90 @@
using System.Data;
using Dapper;
using Dapper.Contrib.Extensions;
using ServerLib.RDB.Database;
namespace ServerLib.RDB.Repositories;
public abstract class ARepository<T> where T : class
{
private readonly IDbConnectionFactory factory;
protected ARepository(IDbConnectionFactory factory)
{
this.factory = factory;
}
protected IDbConnection CreateConnection()
{
return factory.CreateConnection();
}
// Dapper.Contrib 기본 CRUD
public async Task<T?> GetByIdAsync(int id)
{
IDbConnection conn = CreateConnection();
return await conn.GetAsync<T>(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
IDbConnection conn = CreateConnection();
return await conn.GetAllAsync<T>();
}
public async Task<long> InsertAsync(T entity)
{
IDbConnection conn = CreateConnection();
return await conn.InsertAsync(entity);
}
public async Task<bool> UpdateAsync(T entity)
{
IDbConnection conn = CreateConnection();
return await conn.UpdateAsync(entity);
}
public async Task<bool> DeleteAsync(T entity)
{
IDbConnection conn = CreateConnection();
return await conn.DeleteAsync(entity);
}
// 커스텀 쿼리 헬퍼 (복잡한 쿼리용)
protected async Task<IEnumerable<T>> QueryAsync(string sql, object? param = null)
{
IDbConnection conn = CreateConnection();
return await conn.QueryAsync<T>(sql, param);
}
protected async Task<T?> QueryFirstOrDefaultAsync(string sql, object? param = null)
{
IDbConnection conn = CreateConnection();
return await conn.QueryFirstOrDefaultAsync<T>(sql, param);
}
protected async Task<int> ExecuteAsync(string sql, object? param = null)
{
IDbConnection conn = CreateConnection();
return await conn.ExecuteAsync(sql, param);
}
// 트랜잭션 헬퍼
protected async Task<TResult> WithTransactionAsync<TResult>(Func<IDbConnection, IDbTransaction, Task<TResult>> action)
{
IDbConnection conn = CreateConnection();
conn.Open();
IDbTransaction tx = conn.BeginTransaction();
try
{
TResult result = await action(conn, tx);
tx.Commit();
return result;
}
catch
{
tx.Rollback();
throw;
}
}
}

View File

@@ -8,7 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
<PackageReference Include="LiteNetLib" Version="2.0.2" />
<PackageReference Include="MySql.Data" Version="9.6.0" />
<PackageReference Include="MySqlConnector" Version="2.5.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />