현재 진행 중인 포트폴리오 게임 프로젝트에서 ASP.NET Core을 Rest API 서버로 사용하고 있습니다.
저는 어떠한 기능을 구현할 때, 먼저 작동 여부를 확인한 후, 리펙토링 하는 방식으로 진행하는 편입니다.
이제 작동 여부는 확인을 했고, 리펙토링 작업이 필요하다고 생각되어 수정하였습니다.
기존에는 Controller에 코드를 집중하여 작성하였습니다.
(기존 코드를 수정하고 service-repo 패턴을 적용하면서 많이 수정되어 기존 commit에서 가져왔다보니, 코드가 조금 이상할 수도 있습니다.)
[Route("account")]
public class AccountController : ControllerBase
{
private readonly AccountDB _account;
public AccountController(AccountDB context)
{
_account = context;
}
[HttpPost]
[Route("create")]
public async Task<CreateAccountRes> CreateAccountRes(CreateAccountReq req)
{
var accountInfo = await _account.AccountInfo
.AsNoTracking()
.FirstOrDefaultAsync(x => x.AccountName == req.AccountName);
if (accountInfo == null)
{
accountInfo = new AccountInfo
{
AccountName = req.AccountName,
Password = req.Password
};
await _account.AccountInfo.AddAsync(accountInfo);
await _account.SaveChangesAsync();
}
else
{
return new CreateAccountRes
{
Result = (int)ErrorType.AlreadyExistName
};
}
return new CreateAccountRes
{
Result = (int)ErrorType.Success
};
}
[Route("login")]
public async Task<LoginAccountRes> LoginAccount(LoginAccountReq req)
{
var accountInfo = await _account.AccountInfo
.AsNoTracking()
.FirstOrDefaultAsync(x => x.AccountName == req.AccountName && x.Password == req.Password);
if (accountInfo == null)
{
accountInfo = new AccountInfo
{
AccountName = req.AccountName,
Password = req.Password
};
await _account.AccountInfo.AddAsync(accountInfo);
await _account.SaveChangesAsync();
}
var serverInfos = new List<ServerInfo>()
{
new ServerInfo() { Name = "Server1", Ip = "127.0.0.1", Congestion = 0},
new ServerInfo() { Name = "Server2", Ip = "127.0.0.1", Congestion = 1},
};
return new LoginAccountRes
{
Result = (int)ErrorType.Success,
ServerInfos = { serverInfos }
};
}
Account 관련하여 계정 생성, 계정 로그인 이라는 두가지의 기능이 있고, 현재는 코드가 크게 없어서 불편함을 못 느낄 수도 있겠지만, 추가됨에 따라 불편함을 느끼게 됩니다. ( 현재 위의 코드도 불편..)
일단 하나의 Controller에 응집된 코드의 문제점은 결합도와 응집도가 높아진다는 것입니다.
왜 결합도와 응집도가 높아지면 안 좋은가? 라고 이야기를 할 수도 있겠지만,
어느 정도 규모가 커지기 시작하면 Controller에서 너무 많은 역할을 하고 있기 때문에
추후에 어떤 부분에서 버그가 발생했는지, 디버깅을 할 때, 로직 플로우 파악하는데 어려움 등 여러 가지 부분이 있습니다.
혼자 프로젝트를 진행한다면 큰 문제가 없을 수도 있습니다.
하지만, 추후에 수정하거나 버그를 찾을 때, 코드를 직접 짠 본인도 까먹기 때문에 가능하면 코드를 파악하기 쉽게 만드는게 중요 하다고 생각됩니다.
Service-Repository 패턴 적용 후 코드
Controller 코드
[ApiController]
[Route("account")]
public class AccountController : ControllerBase
{
private readonly IAccountService _accountService;
public AccountController(IAccountService accountService)
{
_accountService = accountService;
}
[HttpPost]
[Route("create")]
public async Task<CreateAccountRes> CreateAccountRes(CreateAccountReq req)
{
var result = await _accountService.CreateAccountAsync(req.AccountName, req.Password);
return new CreateAccountRes
{
Result = result
};
}
[HttpPost]
[Route("login")]
public async Task<LoginAccountRes> LoginAccount(LoginAccountReq req)
{
var (result, serverInfos) = await _accountService.LoginAccountAsync(req.AccountName, req.Password);
return new LoginAccountRes
{
Result = result,
ServerInfos = { serverInfos }
};
}
}
Service 코드
public interface IAccountService
{
Task<int> CreateAccountAsync(string accountName, string password);
Task<(int, List<ServerInfo>)> LoginAccountAsync(string accountName, string password);
}
public class AccountService : IAccountService
{
private readonly IAccountRepository _accountRepository;
public AccountService(IAccountRepository accountRepository)
{
_accountRepository = accountRepository;
}
public async Task<int> CreateAccountAsync(string accountName, string password)
{
if(await _accountRepository.IsAccountExistAsync(accountName))
{
return (int)ErrorType.AlreadyExistName;
}
var accountInfo = new AccountInfo
{
AccountName = accountName,
Password = password
};
await _accountRepository.CreateAccountAsync(accountName, password);
await _accountRepository.SaveChangesAsync();
return (int)ErrorType.Success;
}
public async Task<(int, List<ServerInfo>)> LoginAccountAsync(string accountName, string password)
{
var accountInfo = await _accountRepository.GetAccountByNameAsync(accountName);
if (accountInfo == null)
{
var result = await CreateAccountAsync(accountName, password);
if (result != (int)ErrorType.Success)
{
return (result, new List<ServerInfo>());
}
}
// TODO : 서버 목록 임시로 반환
var serverInfos = new List<ServerInfo>()
{
new ServerInfo() { Name = "Server1", Ip = "127.0.0.1", Congestion = 0},
new ServerInfo() { Name = "Server2", Ip = "127.0.0.1", Congestion = 1},
};
return ((int)ErrorType.Success, serverInfos);
}
}
Repository 코드
public interface IAccountRepository
{
Task<AccountInfo?> GetAccountByNameAsync(string accountName);
Task<bool> IsAccountExistAsync(string accountName);
Task CreateAccountAsync(string accountName, string password);
Task SaveChangesAsync();
}
public class AccountRepository : IAccountRepository
{
private readonly AccountDB _context;
public AccountRepository(AccountDB context)
{
_context = context;
}
public async Task<AccountInfo?> GetAccountByNameAsync(string accountName)
{
return await _context.AccountInfo
.AsNoTracking()
.FirstOrDefaultAsync(x => x.AccountName == accountName);
}
public async Task<bool> IsAccountExistAsync(string accountName)
{
return await _context.AccountInfo.AnyAsync(x => x.AccountName == accountName);
}
public async Task CreateAccountAsync(string accountName, string password)
{
var account = new AccountInfo
{
AccountName = accountName,
Password = password
};
await _context.AccountInfo.AddAsync(account);
}
public async Task SaveChangesAsync()
{
await _context.SaveChangesExAsync();
}
}
이 코드를 작성 후에 Dependency Injection(DI) 설정도 해야합니다. (보통 Program.cs or Startup.cs)
전체적인 변경 요약
- Controller : 요청을 처리하고 ,Service에 위임
- Service : 비즈니스 로직을 처리하여 컨트롤러를 단순화
- Repository : 데이터베이스 엑세스 로직을 캡슐화하여 재사용성 및 유지보수성을 향상
- DI : IAccountRepository 와 IAccountService 를 주입받아 의존성을 관리
예전 회사 프로젝트에서 Golang, Python을 사용하였을 때, 사용하던 방식이였는데, 개인 프로젝트를 하면서
이번에 게임 Web Server 쪽에 적용 시켜봤습니다.
회사에서는 여러 명이 코드를 작성하다보니, 가독성과 유지보수성에 대해 많이 신경쓰려고 노력합니다.
일단, 기존 인원들에서 많은 분들이 나갔다 들어왔다 반복하기 때문에 추후에 누가 보더라도 금방 파악할 수 있게
사람들과 이야기하며 코드 리펙토링 하는 것도 재밌습니다 +_+
일단 여기까지 끝냅니다 ~
'C#' 카테고리의 다른 글
[ASP.NET CORE] Unity와 Protobuf로 통신하기 (0) | 2024.12.24 |
---|---|
[C#] Lock Free 하게 써보기 (Concurrent, Interlocked 활용) (0) | 2024.12.04 |
[C#] 인터페이스와 추상 클래스 (1) | 2024.11.18 |
[C#] Csv 파일을 Json 파일로 바꾸기 (3) | 2024.11.14 |
[C#] Unity에서 ProtoBuf 사용하는 방법 (1) | 2024.11.04 |