지난주, 내가 개발 중인 프로젝트의 코드 아키텍처에 대해 뤼튼에게 리뷰를 요청했다. 결과는 충격적이었다. 무려 55가지의 구조 개선사항을 지적받았다. 그 지적들을 마주하고 있으니 이른바 '내글구려병'에 깊이 걸린 기분이었다. 이 문제들을 해결하지 않고서는 도저히 다음 단계로 넘어갈 수 없을 것 같다는 생각이 들었다.
그래서 아주 살짝 프로젝트의 목표를 수정하기로 했다. 단순히 첫 작품을 만드는 것을 넘어, 앞으로 내가 개발할 모든 게임의 튼튼한 토대가 될 '게임 개발 프레임워크'를 만드는 것으로 목표를 조금 수정했다. 이 과정에서 지난 포스팅에서 "추후에 반영하겠다"고 미뤄뒀던 VContainer와 Addressables 같은 핵심 기술들을 지금 당장 도입하기로 마음먹었다.
도입 자체는 나름 빠르게 진행되었지만, 예상대로 여러 에러들이 나를 기다리고 있었다.
InvalidKeyException: No Location found for Key=PlayerRoom첫 번째 에러는 Addressables 관련 InvalidKeyException이었다.
GameManager에서 _sceneTransitionService.FadeAndLoadScene("PlayerRoom"); 와 같이 씬 전환을 호출하고 있었고, 그 내부에서는 Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single, true); 로 Addressables를 사용하고 있었다.
하지만 에러 메시지는 'PlayerRoom'이라는 키를 찾을 수 없다는 것이었다. 찾아보니 유니티 Addressables를 사용할 때, 씬 파일의 Address는 보통 Assets/Scenes/PlayerRoom.unity와 같이 .unity 확장자까지 포함된 전체 경로로 지정된다고 한다. 하지만 관례적으로는 가시성을 위해 .unity를 제거하고 "PlayerRoom"처럼 짧게 주소를 지정하는 경우가 많다고 한다. 내 코드에서는 .unity를 제외한 이름으로 씬을 호출하고 있었는데, Addressables 그룹에는 Assets/Scenes/PlayerRoom.unity로 등록되어 있었던 것이 문제였다.
이 문제는 Addressables Groups 창에서 PlayerRoom 씬의 Address를 "PlayerRoom"으로 변경하고 Addressables 빌드를 다시 수행하는 것으로 간단하게 해결할 수 있었다.
Database connection is not open. Call OpenConnection() first.두 번째 문제는 데이터베이스 연결 관련 이슈였다. PlayerStatsRepository에서 데이터베이스에 접근하려고 할 때 Database connection is not open이라는 에러가 발생했다.
내 프로젝트의 데이터 관리 구조는 다음과 같았다. DatabaseAccess가 SQLite와의 저수준 연결을 직접 담당하고, DataManager가 이 DatabaseAccess를 통해 전체적인 데이터를 관리하며, PlayerStatsRepository와 같은 개별 Repository들이 IDatabaseAccess를 주입받아 특정 엔티티(예: 플레이어 스탯)의 영속성(저장/로드)을 처리하는 방식이었다. (이 Repository 패턴은 뤼튼의 지적 중 하나를 개선하면서 도입하게 된 구조이다. DataManager가 모든 데이터 관련 책임을 지는 것을 피하고, 특정 도메인 객체의 영속성 관리를 분리하기 위함이었다.)
그런데 PlayerStatsRepository에서 _dbAccess를 통해 DB 쿼리를 날리는데도 DB 연결이 끊어져 버린 것이다. 도저히 이해할 수 없는 상황이었다. 문제의 원인은 Mono.Data.Sqlite의 SqliteConnection 객체의 스레드가 안전하지 않다는 것이라고 했다. Task.Run을 사용하여 백그라운드 스레드에서 데이터베이스 작업을 시도하면, Main Thread에서 열린 연결을 다른 스레드가 사용하려 할 때 연결이 닫힌 것으로 인식되어 이런 오류가 발생한다는 것이었다.
이 상황에 Gemini가 제안한 해결책은 크게 두 가지였다.
DatabaseAccess의 각 호출 쿼리 메서드 내부에서 필요할 때마다 DB 연결을 열고 닫는 방식으로 변경하는 것.using (var connection = new SqliteConnection(connectionString)) { connection.Open(); ... })PlayerStatsRepository의 메서드에서 return await Task.Run(() => { ... }); 와 같은 Task.Run 부분을 제거하고, DB 쿼리가 Main Thread에서 동기적으로 실행되도록 하는 것.처음에는 2번 해결책(Task.Run 제거)이 나름 합리적으로 보였다. 하지만, await를 제거하자
Assets\Scripts\Core\Data\Impl\PlayerStatsRepository.cs(39,44): warning CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
라는 경고 메시지가 뜨는 것을 확인했다. 단순하게 이야기해서 Task를 리턴하는 메서드에서 await를 제거하지 말라는뜻이었다. 순간 아찔해졌다. 실제로 저번에 저 경고를 무시했다가 에디터상에서 풀리지않는 프리징을 경험했던 적이 있었기에 이는 절대 할수 없는 선택이었다. 또한 Task를 제거하는것이 모든 DB연결 레포지터리에 도입할수 있는것도 아니었기에 프레임워크의 확장성에 좋지 않다는 생각이 들었다. 또한, 1번 해결책은 이미 리소스 관리 측면에서 통합적인 DB 연결 관리를 위해 개선했던 초기 구조였던지라 논외였다.
결국, 내 프레임워크의 목표(견고함, 확장성)를 고려한 새로운 해결책에 도달했다. 해결책은 나름 명쾌했다. 쓰레드별로 접근을 할수 없는게 문제라면 쓰레드별로 접근하게 해주면 되는것이었다! 각 스레드에서 DatabaseAccess를 통해 DB에 접근할 때, 스레드별로 독립적인 DB 연결을 관리하도록 만드는 것이었다. DatabaseAccess 클래스를 다시 리팩토링하여 SqliteConnection 필드를 ThreadLocal<SqliteConnection>으로 변경했다. ThreadLocal은 각 스레드가 자신만의 SqliteConnection 인스턴스를 가지도록 보장하며, Task.Run으로 생성된 백그라운드 스레드도 안전하게 DB 연결을 열고 사용할 수 있게 해주었다.
물론, 이 과정에서 VContainer의 IInitializable, IDisposable 인터페이스 구현 및 등록 오류, MonoBehaviour가 아닌 클래스 등록 오류, 엔트리 포인트 등록 오류 등 예상치 못한 수많은 잡다한 VContainer 관련 문제들도 함께 해결해야 했다.
마침내, 모든 트러블슈팅을 완료했다!
뤼튼에게 지적받은 사항들을 리팩토링하면서 정말 많은 생각이 들었다. 내가 100% 이해하지 못한 소스코드를 사용하는 것의 위험성 또한 뼈저리게 느꼈다. 그럼에도 불구하고, 현재 내 수준에서는 당장 짤 수 없는 소스코드들을 억지로라도 사용하고 배워가면서 시야가 어마무시하게 넓어지는 경험은 그보다 더 값진 것이었다.
Velog에는 참 대단한 개발자들이 많다. 10대임에도 수백 개의 게임을 개발한 사람, 엄청난 아키텍처와 개발에 대한 시야와 능력을 가지고 많은 경력과 뛰어난 실력을 지닌 기술자들까지. 하루는 그런 사람들을 보며 조금 위축되기도 했다. 그리고 그런 생각이 들었다.
내가 원하는 나는 기술자가 아닌데 내가 왜 저 사람들에게 위축되고 있는거지..? 그리고 나는 기술자가 아니라면서 지금 뭘하고 있는거지..?
하지만 내가 바라는 나의 길은 단순한 기술자도, 단순한 창작자도 아닌, 기술과 창작자의 페르소나를 모두 지닌 것이라는 생각을 다시 떠올렸다. 내 20대는 단순히 버려진 것이 아니라 창작자로서의 나를 세우고, 기술자라는 또 다른 페르소나를 가질 수 있게 해준 감사한 시간이었다.
마지막으로, AI가 확장해준 내가 갈 수 있는 길과 지식, 해낼 수 있는 결과물, 그리고 넓어진 시야는 결코 두려워해야 하는 것이 아니라 감사한 것이며, 내가 오히려 적극적으로 활용해야 하는 부분이라는 점을 다시 한번 깨달았다.
이건 AI가 내게 전해준 기회이며, AI가 있기때문에 기존에는 난이도로 인해, 구현의 복잡성으로 인해, 혹은 시간적 제약으로 인해 포기해야만 했던 기술들을 사용할수 있게되었고, 되려 사용해야만 하는 시기가 되었다는것이다.
AI가 아니었다면 도입할 생각도 할수 없었고, 도입할 수도 없었던 높은 기술들과 패턴들이 내 손안에서 나만의 프레임워크로 구현이 되고 있는데 이 어찌 감사한 일이 아닐 수 있을까.
이 경험으로 인해 나는 더더욱 견고해진 듯한 기분이 든다.