게임 서버 로직을 구현하는 과정에서 로그인할 경우와 사용자가 아이템을 획득했을 때, 데이터베이스에 접근해서 정보를 가져오거나 저장하는 동작을 처리하려고 합니다. 이때 데이터베이스를 조작하는 인터페이스를 통일하려고 하는데, 게임 로직을 처리하는 입장에서는 데이터베이스 조작의 내용을 구체적으로 알 필요가 없기 때문입니다.
이런 경우에 Command 패턴을 사용합니다. 요청하는 쪽에서는 구체적인 동작에 대해서는 관심을 가질 필요 없을 경우에 Command 패턴을 사용합니다. 호출 방법을 인터페이스를 통해서 통일해서 동일하게 취급하여 다양한 작업을 동일한 방법으로 요청할 수 있도록 합니다.
아래 코드는 예제를 위해서 만들어진 것으로 구체적인 동작은 하지 않는 의사코드입니다. Future 패턴을 이용해서 비동기적으로 결과를 가져와 사용할 수 있도록 하였습니다.
publicinterfaceICommand{Task Execute();}publicclassLoginCommand : ICommand{privatestring _username;privatestring _password;publicLoginCommand(string username, string password){_username = username;_password = password;}publicasync Task Execute(){// TODO: 로그인 처리returntrue;}}publicclassSaveItemCommand : ICommand{privatestring _username;privatestring _item;publicSaveItemCommand(string username, string item){_username = username;_item = item;}publicasync Task Execute(){// TODO: 아이템 저장 처리returntrue;}}
이번 예제에서는 간단하게 필요한 구현으로 마무리했지만, Command 패턴은 인터페이스를 통일하는 것 뿐만 아니라 몇 가지 추가적인 이점이 있습니다. 아래는 Command 패턴을 사용할 때 얻을 수 있는 이점들입니다.
- 분리 및 캡슐화: Command 패턴을 사용하면 요청을 수행하는 객체와 요청을 발생시키는 객체를 분리할 수 있습니다. 이를 통해 요청을 수행하는 로직을 캡슐화하여 다양한 요청을 동일한 방식으로 처리할 수 있습니다.
- 재사용 및 조합: Command 객체를 재사용하여 다양한 작업을 수행할 수 있습니다. 또한 여러 Command 객체를 조합하여 복잡한 작업을 수행할 수 있습니다.
- 지연 실행: Command 패턴을 사용하면 요청을 즉시 실행하지 않고 나중에 실행할 수 있습니다. 이를 통해 지연 실행, 예약 실행 등의 기능을 구현할 수 있습니다.
- 취소 및 재실행: Command 패턴을 사용하면 실행한 요청을 취소하거나 재실행할 수 있습니다. 이를 통해 Undo/Redo 기능을 구현할 수 있습니다.
- 로그 및 기록: Command 패턴을 사용하면 실행한 요청을 로그로 기록하거나 저장할 수 있습니다. 이를 통해 시스템의 상태를 복원하거나 문제를 진단할 수 있습니다.
- 병렬 및 비동기 처리: Command 패턴을 사용하면 요청을 병렬로 처리하거나 비동기로 처리할 수 있습니다. 이를 통해 시스템의 성능을 향상시킬 수 있습니다.
- 확장성: Command 패턴을 사용하면 새로운 요청을 쉽게 추가할 수 있습니다. 이를 통해 시스템의 확장성을 향상시킬 수 있습니다.
- 테스트 용이성: Command 패턴을 사용하면 요청을 독립적으로 테스트할 수 있습니다. 이를 통해 시스템의 테스트 용이성을 향상시킬 수 있습니다.
이러한 이유로 Command 패턴은 요청을 수행하는 로직을 캡슐화하고, 재사용하며, 확장하는 데 유용합니다.
Active Object 패턴
이제 지금까지 설명한 재료를 이용해서 Active Object 패턴에 대해서 알아보도록 하겠습니다.
Active Object 패턴은 동시성 문제를 해결하기 위해 사용되는 디자인 패턴입니다. 이 패턴은 객체의 메서드 호출을 비동기적으로 처리하도록 설계되어 있습니다. Active Object 패턴은 요청을 큐에 저장하고, 별도의 스레드에서 큐에 저장된 요청을 순차적으로 처리합니다. 이를 통해 동시성 문제를 해결하고, 객체의 상태를 안전하게 관리할 수 있습니다.
아래의 코드는 게임 서버라는 가상 시나리오에서 Active Object를 활용하는 예제입니다. 두 개의 이벤트를 처리하고 있는데요. 사용자가 게임 서버에 로그인을 할 때 발생하는 OnLogin()과 사용자가 아이텝을 획득했을 때 이를 데이터베이스에 백업하려는 OnSaveItem() 이벤트를 다루고 있습니다.
OnLogin() 이벤트에서는 사용자의 암호를 데이터베이스에서 확인하고 그 결과를 이용해서 로그인 처리를 진행하고 있습니다. 따라서 비동기적으로 실행되는 데이터베이스 확인 작업을 await를 이용해서 기다리고 그 결과를 사용하는 과정입니다.
반면에 OnSaveItem()의 경우에는, 굳이 데이터베이스 처리 결과를 기다릴 필요가 없는 경우를 설명하고 있습니다. 게임 로직은 메모리에서 이뤄지고, 나중을 위해서 데이터베이스에 백업을 하는 시나리오입니다. 데이터베이스에 저장하는 동안 발생하는 오류 등의 처리는 캡슐화하고 GameServer에서는 관심을 주지 않도록 합니다. 이를 통해서 관심사를 철저히 분리하고 있습니다.
ActiveObject에게 비동기적으로 작업을 요청할 뿐이기 때문에 OnSaveItem()에서는 async / await를 사용할 필요가 없습니다.
publicclassGameServer{privatestatic ActiveObject _activeObject = new ActiveObject();publicstaticasyncvoidOnLogin(Connection connection, string username, string password){var command = new LoginCommand(username, password);var loginTask = _activeObject.Enqueue(command);var result = await loginTask;if (!result) {connection.SendLoginFailed();return;}connection.SendLoginSuccess();// 로그인 완료 처리 필요}publicstaticvoidOnSaveItem(Connection connection, string username, string item){var command = new SaveItemCommand(username, item);_activeObject.Enqueue(command);// 캐릭터의 인벤토리에 아이템 추가}}
아래의 코드는 ActiveObject 클래스의 구현 코드입니다. 이 클래스는 Command 패턴을 사용하여 비동기적으로 명령을 실행하는 역할을 합니다.
using CommandTaskTuple = Tuple>;publicclassActiveObject{private BlockingCollection _queue = new BlockingCollection();publicActiveObject(){Task.Run(() =>{foreach (var tuple in _queue.GetConsumingEnumerable()){var command = tuple.Item1;var tcs = tuple.Item2;var result = command.Execute().Result;tcs.SetResult(result);}});}public Task Enqueue(ICommand command){var tcs = new TaskCompletionSource();_queue.Add(Tuple.Create(command, tcs));return tcs.Task;}}
- 이 코드는 Active Object 패턴을 구현한 ActiveObject 클래스입니다. 이 클래스는 비동기적으로 명령을 실행하는 역할을 합니다. ActiveObject 클래스는 다음과 같은 구성 요소로 이루어져 있습니다.
- BlockingCollection: 이 클래스는 스레드 안전한 컬렉션으로, 여러 스레드에서 동시에 접근할 때 발생할 수 있는 동시성 문제를 해결해줍니다. 이 클래스를 사용하면 스레드가 큐에 항목을 추가하거나 제거할 때 동기화를 자동으로 처리해줍니다. 이를 통해 요청되는 작업들을 안전하게 저장하고 처리할 수 있습니다.
- Guarded Suspension 패턴: 이 패턴은 스레드가 특정 조건이 충족될 때까지 대기하도록 하는 패턴입니다. ActiveObject 클래스에서는 BlockingCollection을 사용하여 이 패턴을 구현하고 있습니다. 큐에 명령이 추가될 때까지 스레드가 대기하게 됩니다.
- ActiveObject() 생성자: 이 생성자에서는 별도의 스레드를 시작하여 큐에 저장된 명령을 순차적으로 실행합니다. 큐에서 명령을 가져와 Execute 메서드를 호출하여 명령을 실행하고, 그 결과를 TaskCompletionSource 객체에 설정합니다. 이를 통해 비동기적으로 명령을 실행하고 그 결과를 반환할 수 있습니다.
- Enqueue() 메서드: 이 메서드는 명령을 큐에 저장하는 역할을 합니다. 명령과 그에 대한 결과를 나타내는 TaskCompletionSource 객체를 생성하여 큐에 저장합니다. 그리고 TaskCompletionSource를 이용하여, 아직 실행되지 않은 Command의 퓨처에 해당하는 Task 객체를 반환합니다.
- TaskCompletionSource: 이 클래스는 비동기 작업의 결과를 나타내는 Task 객체를 생성하고, 그 결과를 설정하는 데 사용됩니다. 이를 통해 비동기 작업의 완료를 표시하고 결과를 반환할 수 있습니다.
다음은 Active Object 패턴을 사용할 때 얻을 수 있는 이점들입니다.
- 동시성 제어: Active Object 패턴을 사용하면 동시성 문제를 해결하고, 객체의 상태를 안전하게 관리할 수 있습니다.
- 비동기 처리: Active Object 패턴을 사용하면 객체의 메서드 호출을 비동기적으로 처리할 수 있습니다. 이를 통해 시스템의 응답성을 향상시킬 수 있습니다.
- 코드 간결성: Active Object 패턴을 사용하면 동시성 제어와 비동기 처리를 위한 코드를 간결하게 작성할 수 있습니다.
- 확장성: Active Object 패턴을 사용하면 새로운 요청을 쉽게 추가할 수 있습니다. 이를 통해 시스템의 확장성을 향상시킬 수 있습니다.
이러한 이유로 Active Object 패턴은 동시성 문제를 해결하고, 비동기 처리를 위한 코드를 간결하게 작성하는 데 유용합니다.
동시성 문제를 해결하는 Active Object 패턴
예제를 최대한 간편하고 설명하기 쉽게 구성하려고 노력했는데요. 실전에서라면 Command 생성 부분까지도 감추고 아래와 같은 형태로 구성하는 것도 괜찮아 보입니다.
publicclassGameServer{privatestaticDatabaseController _databaseController = newDatabaseController();publicstaticasyncvoidonLogin(Connection connection, string username, string password){var result = await _databaseController.Login(username, password);if (!result){connection.SendLoginFailed();return;}connection.SendLoginSuccess();// 로그인 완료 처리 필요}publicstaticasyncvoidonSaveItem(Connection connection, string username, string item){_databaseController.SaveItem(username, item);// 캐릭터의 인벤토리에 아이템 추가}}
이번 글에서는 동시성 문제를 해결하는 Active Object 패턴에 대해 알아보았습니다. 이 패턴은 객체의 메서드 호출을 비동기적으로 처리하도록 설계되어 있으며, 요청을 큐에 저장하고 별도의 스레드에서 큐에 저장된 요청을 순차적으로 처리합니다. 이를 통해 동시성 문제를 해결하고, 객체의 상태를 안전하게 관리할 수 있습니다.
Active Object 패턴은 게임 서버와 같은 다중 사용자 환경에서 효과적으로 사용될 수 있습니다. 이 패턴을 사용하면, 사용자의 요청을 비동기적으로 처리하고, 동시에 발생하는 여러 요청을 안전하게 관리할 수 있습니다.
다음 글에서는 다른 동시성 패턴들을 소개하겠습니다. 계속해서 많은 관심 부탁드립니다. 감사합니다.