Problem
이전 문서에서 GCS서버(시뮬레이션 진행을 위한 중앙 제어 서버)에 대한 동시 처리를 웹 서버 측에서 담당하는 것으로 결정하였습니다. 바로 개발에 들어갔지만 추가된 제어 로직이 기존 제어 로직과 잘 맞지 않다는 것을 발견했습니다.
Analysis
세션 관리 주체(FastAPI) ≠ 실제 리소스 제어 주체(React, Unity)
원인
개발 도중 다양한 변화가 있었고, 웹 서버에서 GCS 서버의 세션처리를 담당할 것을 염두에 두지 않았기 때문에 이러한 문제가 발생했다고 생각합니다.
기존 제어 로직들을 다 그대로 적용하다 보니, FastAPI는 Redis 락만 관리하고, 실제 GCS/Unity 리소스는 신경 쓰지 않는 상황을 당면 했습니다.
주요 문제 분석
1. 제어 로직의 분산
제어 로직이 여러 계층에 흩어져 있었습니다.
위의 다이어그램에서도 볼 수 있지만 Lock을 획득하고 세션을 시작하는 것은 클라이언트의 요청을 받은 FastAPI 측이지만, GCS 서버를 제어하고, 시뮬레이션을 시작하는 건 클라이언트의 요청을 받은 Unity입니다.
종료 로직에서도 비슷합니다. GCS 자원을 해제하는 건 Unity를 통하고, 락을 해제하는 건 FastAPI를 통합니다.
이러다보니 책임 소재가 불명확하고 문제 발생 시 어느 계층을 먼저 확인하고 수정해야 할지 모호하다는 문제가 가장 크게 체감되었습니다.
2. 세션과 실제 리소스의 불일치
동시에 당연하게도 FastAPI의 세션 상태와 실제 GCS/Unity 상태가 안정적으로 동기화되지도 않았습니다. 아래와 같이 세션 해제를 요청하면, 락만 해제되고, 따로 GCS 자원을 해제하는 로직을 먼저 호출해야합니다.
물론 하드 코딩을 한다면 해결이 가능할 수도 있습니다만, 불안정하기 그지 없습니다. 가장 대표적인 예로
1. 사용자 A: 시뮬레이션 시작
- FastAPI: Redis 락 획득 ✅
- Unity: GCS 연결 ✅
- GCS: 시뮬레이션 실행 ✅
2. 사용자 A: 브라우저 강제 종료 (Ctrl+W)
- Unity: 종료됨 (브라우저와 함께)
- GCS: Unity WebSocket Close로 GCS 자원 해제 ✅
- FastAPI: Redis 락은 20분 간 유지 ❌
3. 사용자 B: 시뮬레이션 시작
- FastAPI: 실제 가용 서버가 있지만 "세션 불가능"
이런 경우가 있었죠. beforeunload 이벤트를 감지하여 락을 해제한다면 이 문제를 해결할 수 있습니다. 하지만 이는 임시방편일 뿐, 근본 원인(세션-리소스 분리)을 해결하지 못합니다.
세션 관리와 실제 리소스 관리가 온전히 동기화 되지 않는 이러한 문제는, 세션 메타데이터를 제어하는 레이어와 실제 리소스를 제어하는 레이어가 분리되어있기 때문이라고 생각하였습니다.
3. Unity의 과도한 책임
동시에 앞선 다이어그램에서도 볼 수 있듯이 Unity가 너무 많은 것을 알고 처리하고 있었습니다.
// NetworkSystem.cs
public void SetGCSUrl(string url) {
gcsURL = url + "/simulator";
ConnectToGCS();
}
private void ConnectToGCS() {
simulatorWebSocket = new WebSocket(gcsURL);
simulatorWebSocket.OnOpen += WebSocketOnOpen;
simulatorWebSocket.Connect();
}
private async void WebSocketOnOpen() {
// Unity가 스스로 초기화 결정
byte[] resetmessage = Encoding.UTF8.GetBytes("Reset");
await WebSocketSendMessage(resetmessage);
}
public void CloseConnection() {
// Unity가 스스로 종료 처리
simulatorWebSocket?.Close();
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}초기 개발에서는 유니티가 GCS 서버의 기능까지 담당하고 있었습니다. 이후 GCS가 Unity와 분리되어 독립적인 서버로 작동하게 되었지만, 분리의 과정에서 Unity에게 과도한 역할이 남겨지게 되었습니다.
Unity는 사실 “시뮬레이션을 렌더링 해준다”가 본인의 역할이어야합니다. 그러나 현재의 유니티는 시뮬레이션을 시작할를 알려주는 역할과 리소스를 어떻게 관리할지도 결정하고 있습니다.
4. 테스트 및 상태 추적 어려움
마지막으로 각 레이어의 역할이 명확하지 못하고, 복잡하게 섞여있다보니 문제가 발생했을 때 체크해야할 곳이 너무 많아서 문제 지점을 특정하기도도 어려웠습니다.
주요 제약 사항
해결을 하기 전에, 생각해봐야한 제약사항은 크게 3가지가 있습니다.
- WebSocket은 클라이언트(Unity)가 먼저 연결 시작해야함
- 실시간 시뮬레이션 데이터 지연 최소화 필요
- Unity는 브라우저에서 실행됨 (서버 측 제어 제한)
³ Solution
각 계층의 역할 제정의
해결 방향
문제 해결을 위해 여러 방향을 고려해보았습니다.
- React가 Unity 직접 제어
- FastAPI가 모든 제어
- FastAPI가 모든 제어 및 중계
1번은 현재 상태 유지한 채로 필요한 부분만 추가, 수정하는 방향이라고 할 수 있습니다. 구현은 간단할 수 있으나, 코드 수정이 거듭될 수록 기술적 부채가 늘어나는 방향이라고 생각했습니다.
2번은 분리되어 있었던 리소스 제어의 결정권을 FastAPI에게 쥐어주는 방식입니다. 근본적인 문제를 해결하여 중앙 집중식 관리를 할 수 있다는 장점이 있으나, 당장의 구현이 어려울 수 있다는 문제와 제어로직에 FastAPI가 끼어드는 모양새이기 때문에 약간의 지연이 있을 수 있다는 단점이 있습니다.
마지막 3번은 FastAPI를 프록시로 사용하는 것입니다. 모든 통통신이 FastApi를 경유하는 방식이죠. Unity ↔ GCS 간 실시간 데이터도 FastAPI 프록시를 통해 전달됩니다. 이전에 사실 ‘보안적 측면’을 고민하다가 나왔던 이야기이긴 합니다. GCS 주소를 완전히 숨길 수 있고, 실시간 통신의 인증/권한 검증도 강화할 수 있는 장점이 있었기 때문입니다. 그러나, 구현의 복잡도가 높고 무엇보다 FastAPI의 부하가 크게 증가하고, 통신의 지연시간 또한 증가한다는 점에서 채택되지 못했습니다. 실시간 데이터의 양이 아주 많기 때문입니다.
최종 선택: 2번 (FastAPI 중심 제어)
최종적으로 선택된 방향은 제어 로직을 ‘FastAPI’가 담당하는 안입니다. 근본적인 문제를 해결할 수도 있고, 파악된 단점이 제어 로직의 약간의 지연과 당장의 구현 복잡도이었기 때문입니다.
전략
우선 각 계층의 역할을 재정의하는 것이 먼저라고 생각하여 다음과 같이 역할을 명확히 했습니다.
-
React: UI
- UI 렌더링
- 버튼 클릭 → API 호출
- Unity에게 gcs url 정보 중계
-
FastAPI: 중심 제어
- 세션 생명주기 관리
- GCS 제어 (시작/종료/초기화)
- JSON 생성 및 전송
-
GCS: 시뮬레이션 제어
- 시뮬레이션 로직 실행
- 명령 중계 (FastAPI → Unity)
- Unity와 실시간 데이터 송수신(시뮬레이션 실행용)
-
Unity: 렌더링
- 시뮬레이션 렌더링
- 명령 수행 (Reset, Start 등)
- GCS와의 WebSocket 연결 시작
- 제약: WebSocket 특성상 클라이언트가 먼저 연결해야함
- 연결 후 대기 (자동 Reset 제거)
- GCS와 실시간 데이터 송수신
이렇게 각 레이어의 정해진 역할을 기반으로 각 로직의 플로우를 다음과 같이 정리했습니다.
-
제어는 FastAPI가 담당
- 시작: FastAPI → GCS → Unity
- 종료: FastAPI → GCS → Unity
- 초기화: FastAPI → GCS → Unity
-
데이터는 직접 연결
- Unity ↔ GCS: 실시간 데이터
-
Unity는 명령만 수행
- Reset/Start 받으면 실행
- 자동 판단 금지
-
모든 종료는 같은 경로
- 정상 종료 = 비정상 종료 = 타임아웃
- 단일 코드 경로
Result
결과
결과적으로 시작로직과 종료 로직을 모두 FastAPI가 담당하도록 했습니다. 이전 다이어그램에 비해 Unity이 확연히 줄어든 것을 알 수있습니다. 시뮬레이션 렌더링이라는 역할에 맞는 최소한의 책임을 지게 했습니다.
또한 아래 시퀀스 다이어그램을 보면, 세션 메타데이터와 실제 리소스 간의 연관성을 높인 것도 파악할 수 있습니다. 세션의 획득화 함께 리소스가 초기화되고 세션의 종료와 함께 리소스들이 해제됩니다.
시퀀스 다이어그램 비교
Row
Col
Before
오른쪽 단
After
개선 지표
| 항목 | Before | After |
|---|---|---|
| 제어 주체 | 3곳 분산 | FastAPI 중심 |
| 세션-리소스 동기화 | ❌ | ✅ |
| 비정상 종료 처리 | ❌ | ✅ |
| 타임아웃 의미 | Redis만 | GCS까지 |
| 테스트 | 불가능 | 계층별 가능 |
회고
세션 관리 = 리소스 관리
우선 가장 먼저 세션을 관리할 때, 락과 같은 메타데이터만 관리하는 행태의 위험성을 깨닫게 되었습니다. Redis 락만 관리하는 것이 아니라 동시에 모든 리소스를 초기화하고 또 정리하는 책임을 함께 져야한다는 것을 체감할 수 있었습니다. Pintos 운영체제 프로젝트를 진행할 때에도 Process를 종료하기 이전에 파일 핸들, 메모리 등을 먼저 정리했듯이, 세션 종료 시에도 관련된 모든 리소스(Redis 락, GCS 상태, Unity Scene)를 함께 정리해야 한다는 것을 깨달았습니다.
제어 로직의 중앙화
개선 이전에 가장 어려움을 체감했던 부분입니다. 제어 로직이 분산되어있다보니 버그 발생 시 원인 파악이 아주 어려웠습니다. 원인 파악뿐만 아니라, 수정 시에도 누가, 어떤 제어를 담당하고 있는지 명확하지 않다보니 수정 또한 오래 걸렸습니다. 그리고 제어의 타이밍 또한 중요한데, 분산되어 있으니 뭐가 먼저 진행되고 뭐가 나중에 진행되는지 순서를 파악하는 것도 어려웠고 요구된 제어 순서가 제대로 지켜진다고 확신하는 것도 어려웠습니다. 명확하게 한 곳에서 제어를 담당하는 것이 얼마나 중요한지를 깨달을 수 있었습니다.
각 레이어의 명확한 역할
Unity는 단순히 렌더링 레이어의 역할을 담당해야했습니다. 그러나, 언제 Reset할지 결정하는 등 본인 역할에 벗어나는 과도한 책임을 진 것 또한 이번 개선의 주요한 원인이었다고 생각합니다.