서버 캠퍼스의 과정 중, 두 권의 책을 받았습니다. 하나는 '읽기 좋은 코드가 좋은 코드다'이며, 다른 하나는 '면접을 위한 CS 전공지식 노트'입니다.
이 중 '읽기 좋은 코드가 좋은 코드다'는 프로젝트 진행 중 부딪혔던 여러 가지 고민들, 예를 들어
등에 대한 접근법을 제시해주었습니다.
책은 주요 주제를 몇 가지 핵심 부분으로 나누어 소개하였습니다. 변수의 명명, 형태, 배치, 주석 같은 시각적 요소와 논리적 흐름, 표현 범위, 중복 코드, 단일 기능, 코드 볼륨 감소 등의 논리적인 접근법이 포함되었습니다.
그러나 이런 지식들을 습득하는 것만으로 좋은 코드를 만들 수는 없습니다. 좋은 코드를 만들기 위해서는 지식을 실제로 적용하는 과정, 지속적으로 코드를 검토하고 개선하는 것이 필수적입니다. 이는 실장님께서 멘토링 중 강조하셨던 내용입니다.
멘토링을 계기로 자기소개서나 포트폴리오, 블로그 글 작성 시에는 여러 번 읽고 수정하면서 왜 프로그래밍을 할 때는 기능이 작동하기만 하면 되돌아보지 않았는지 반성하게 되었습니다.
결국 제 경우에
좋은 코드를 작성하지 못하는 이유
는작성한 코드를 충분히 검토하지 않아서
였습니다...
진행 중인 프로젝트에서는 지속적으로 코드를 검토하고 리팩토링을 하고 있습니다.
그 중 하나의 예를 가져왔습니다.
GameDatabase
클래스가 2000줄 가까이 되어 있어서 다음과 같은 데이터베이스 접근 목적에 따라 클래스를 분리하였습니다.
public interface IMailService
{
public Task<(ErrorCode, List<Mail>)> LoadMailListAsync(Int32 gameUserId, Int32 pageNumber);
public Task<ErrorCode> MarkMailAsReceiveAsync(Int32 gameUserId, Int64 mailId);
public Task<ErrorCode> RollbackMarkMailItemAsReceiveAsync(Int32 gameUserId, Int64 mailId);
public Task<ErrorCode> DeleteMailAsync(Int32 gameUserId, Int64 mailId);
public Task<(ErrorCode, Int64 mailId)> InsertMailAsync(Int32 gameUserId, Mail mail);
public Task<ErrorCode> RollbackInsertMailAsync(Int64 mailId);
//...
//책임과 맞지 않는 함수, 구현부에 중복 코드 존재
public Task<ErrorCode> ReceiveMailItemAsync(Int64 mailId, Int32 gameUserId);
}
public interface IDungeonStageService
{
public Task<(ErrorCode, Int32)> LoadStageListAsync(Int32 gameUserId);
public Task<ErrorCode> RollbackUpdateExpAsync(Int32 gameUserId, Int32 level, Int32 exp);
public Task<(ErrorCode, Boolean isIncrement)> IncreaseMaxClearedStageAsync(Int32 gameUserId, Int32 clearLevel);
public Task<ErrorCode> RollbackIncreaseMaxClearedStageAsync(Int32 gameUserId, Boolean isIncrement);
//책임과 맞지 않는 함수, 구현부에 중복 코드 존재
public Task<ErrorCode> ReceiveDungeonRewardItemAsync(Int32 gameUserId, Boolean isIncrement);
}
분리를 하고 나서 보니 DungeonStage, Mail 관련된 클래스에서 Item을 생성하고 있었고 Item 추가를 위한 다음과 같은 코드가 두 클래스에 중복해서 정의되어 있었습니다.
public async Task<ErrorCode> InsertItemsAsync(Int32 gameUserId, List<(Int32, Int32)> itemCodeList)
{
//아이템 생성에 관한 코드
// AddItemBasedOnCodeAsync 호출
}
private async Task<ErrorCode> AddItemBasedOnCodeAsync(Int32 gameUserId, Int32 itemCode ,Int32 itemCount, List<Func<Task>> rollbackActions)
{
ErrorCode errorCode= ErrorCode.None;
if (itemCode == (int)ItemCode.Gold)
{
errorCode = await IncreaseGoldAsync(gameUserId, itemCount, rollbackActions);
}
else if (itemCode == (int)ItemCode.Potion)
{
errorCode = await IncreasePotionAsync(gameUserId, itemCount, rollbackActions);
}
else
{
errorCode = await InsertOwnedItemAsync(gameUserId, itemCode, itemCount,
0, rollbackActions);
}
return errorCode;
}
private async Task<ErrorCode> IncreaseGoldAsync(Int32 gameUserId, Int32 itemCount, List<Func<Task>> rollbackActions)
{
// 골드 증가
}
private async Task<ErrorCode> IncreasePotionAsync(Int32 gameUserId, Int32 itemCount,
List<Func<Task>> rollbackActions)
{
// 포션 증가 혹은 추가
}
private async Task<ErrorCode> InsertOwnedItemAsync(Int32 gameUserId, Int32 itemCode, Int32 itemCount,
Int32 enhancementCount, List<Func<Task>> rollbackActions)
{
//다른 아이템 추가
}
private async Task RollbackReceiveItemAsync(List<Func<Task>> rollbackActions)
{
//실패 시 등록된 롤백 함수 호출
foreach (var action in rollbackActions)
{
await action();
}
}
이를 해결하기 위해 Item 관련된 데이터를 관리하는 클래스를 만들어 중복 코드를 제거 하고 책임을 분리하였습니다.
public interface IItemService
{
public Task<(ErrorCode, OwnedItem)> LoadItemAsync(Int32 gameUserId, Int64 itemId);
public Task<ErrorCode> DestroyItemAsync(Int32 gameUserId, Int64 itemId);
public Task<ErrorCode> InsertItemsAsync(Int32 gameUserId, List<MailItem> items);
public Task<ErrorCode> InsertItemsAsync(Int32 gameUserId, List<(Int32, Int32)> itemCodeList);
public Task<ErrorCode> RollbackDestroyItemAsync(Int64 itemId);
public Task<ErrorCode> InsertNonStackableItemsAsync(Int32 gameUserId, List<OwnedItem> items);
}
그러나 다수의 데이터베이스 접근 클래스를 생성하면서 하나의 요청에 다수의 연결이 발생하는 문제가 따라왔습니다.
(기존 코드와 부딪히는 부분)
Mail Item 받기 요청 :Mail과 Item,
Dungeon Clear 요청 :Dungeon과 Item 등
//생성자 예시
public MailService(ILogger<MailService> logger, IOptions<DatabaseConfiguration> configurationOptions, MasterDataManager masterData)
{
_configurationOptions = configurationOptions;
_logger = logger;
//생성자에서 연결
_databaseConnection = new MySqlConnection(configurationOptions.Value.GameDatabase);
_databaseConnection.Open();
var compiler = new MySqlCompiler();
_queryFactory = new QueryFactory(_databaseConnection, compiler);
}
이에 따라, QueryFactory를 Scoped 범위로 데이터베이스 접근 클래스에 주입하였습니다. 이 방식을 통해, 단일 요청에서 데이터베이스 연결 객체는 오직 하나만 생성됩니다. 또한, 컨트롤러에서 await를 사용하여, 비동기 작업 처리 시 완료될 때까지 다음 작업이 진행되지 않으므로, 데이터베이스 연결 객체에 동시에 접근하는 상황은 발생하지 않습니다.
유저의 요청 또한 미들웨어에서 관리하여 한 번에 하나의 요청을 처리합니다.
// 주입하는 부분
builder.Services.AddScoped<QueryFactory>(provider =>
{
var config = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
var connection = new MySqlConnection(config.Value.GameDatabase);
connection.Open();
var queryFactory = new QueryFactory(connection, new MySqlCompiler());
return queryFactory;
});
//변경된 생성자
public MailService(ILogger<MailService> logger, QueryFactory queryFactory)
{
_logger = logger;
_queryFactory = queryFactory;
}
클래스를 분리하고 단일 연결만을 유지함으로써, MailService와 DungeonStageService에서 발생하던 아이템 생성 중복코드를 제거하고 단일 책임 원칙을 지킬 수 있었습니다.
이후 다른 데이터베이스 접근을 관리하는 클래스들을 생성 및 수정 하면서 중복 코드가 많아 유지보수성이 떨어지는 것을 느끼고 Base 클래스를 하나 만들어 상속 받게 하였습니다.
public class DatabaseAccessBase
{
protected readonly ILogger _logger;
protected readonly QueryFactory _queryFactory;
protected DatabaseAccessBase(ILogger logger, QueryFactory queryFactory)
{
_logger = logger;
_queryFactory = queryFactory;
}
}
리팩토링은 즐거운 작업이었습니다.
진행 중인 프로젝트, 앞으로 수행할 프로젝트 역시, 코드를 반복적으로 검토하고 개선하는 과정을 통해 효율적인 코드, 성능이 좋은 코드 뿐 아니라 같이 협업하기 좋은 가독성 높은 코드를 작성하는 개발자로 성장할 것입니다.