콘텐츠로 이동

Unity 클라이언트 이벤트 로깅·성능 모니터링 시스템 구성

앱의 SDK 초기화와 이벤트 로깅은 하나의 UserTracking 클래스가 MonoBehaviour Awake에서 처리했고, SDK마다 대기 이벤트를 따로 다뤘다. 이를 SDK 라이프사이클 관리, 통합 이벤트 로깅, 성능 모니터링으로 나누어 다시 구성했다. FPS·메모리 판정 기준과 알림 정책은 별도 문서(“클라이언트 성능 모니터링의 slow/frozen frame 판정과 알림 정책”)에서 다룬다.

graph TB
subgraph "Log System"
LSM[LogSystemManager<br/>SDK 라이프사이클 관리]
AEL[AppEventLogger<br/>통합 이벤트 로깅]
end
subgraph "Monitoring System"
MS[MonitoringSystem<br/>FPS/Memory 모니터링]
end
subgraph "SDK Wrapper"
FS[FirebaseSystem]
AFS[AppsFlyerSystem]
ES[EventSdkSystem]
end
subgraph "External SDK"
FB[Firebase Analytics]
AF[AppsFlyer]
EV[Event Collection SDK]
end
LSM -.->|초기화| FS
LSM -.->|초기화| AFS
LSM -.->|초기화| ES
LSM -->|관리| AEL
AEL -->|이벤트 전송| FS
AEL -->|이벤트 전송| AFS
AEL -->|이벤트 전송| ES
MS -->|이벤트 전송| AEL
FS --> FB
AFS --> AF
ES --> EV
컴포넌트 역할 타입
LogSystemManager SDK 초기화 및 라이프사이클 관리 MonoBehaviour
AppEventLogger 앱 이벤트 통합 로깅, 오프라인 큐 static class
MonitoringSystem 성능 모니터링 (FPS, Memory) Singleton
FirebaseSystem Firebase Analytics 래퍼 static class
AppsFlyerSystem AppsFlyer 래퍼 static class
EventSdkSystem 이벤트 수집 SDK 래퍼 static class

폴더 구조는 다음과 같다. AppEventLogger는 partial class로 나눠 메인 로직, 진행 단계 기록, 데이터 타입을 분리했다.

Assets/.../Scripts/System/
├── LogSystem/
│ ├── LogSystemManager.cs # SDK 라이프사이클 관리
│ ├── AppEventLogger/
│ │ ├── AppEventLogger.cs # 메인 로직, 오프라인 큐
│ │ ├── AppEventLogger.Progress.cs # RecordStepStart/End/Info
│ │ ├── AppEventLogger.Data.cs # EventInfo, OfflineEvent, enum
│ │ └── LogTarget.cs # LogTarget Flags enum
│ └── SDK/
│ ├── FirebaseSystem.cs
│ ├── AppsFlyerSystem.cs
│ └── EventSdkSystem.cs
└── Monitoring/
└── MonitoringSystem.cs

SDK 초기화는 BeforeSceneLoad 시점에 앱 시작 최초 1회 수행한다. MonoBehaviour 인스턴스와 모니터링은 AfterSceneLoad에서 만든다.

sequenceDiagram
participant Unity
participant LSM as LogSystemManager
participant AEL as AppEventLogger
participant FS as FirebaseSystem
participant AFS as AppsFlyerSystem
participant ES as EventSdkSystem
participant MS as MonitoringSystem
rect rgb(240, 248, 255)
Note over Unity,ES: BeforeSceneLoad (앱 시작 최초 1회)
Unity->>LSM: InitializeSDKs()
LSM->>AEL: Initialize()
LSM->>FS: Initialize()
LSM->>AFS: Initialize()
LSM->>ES: Initialize()
end
rect rgb(255, 248, 240)
Note over Unity,MS: AfterSceneLoad
Unity->>LSM: CreateInstance()
Unity->>MS: AutoInitialize()
end
rect rgb(240, 255, 240)
Note over LSM,ES: Update Loop (SDK 초기화 완료까지)
loop Until SDK Ready
LSM->>FS: Tick()
LSM->>ES: Tick()
end
FS-->>LSM: IsInitialized = true
ES-->>LSM: IsInitialized = true
Note over LSM: sdkInitCompleted = true<br/>Tick 호출 중단
end
rect rgb(255, 240, 255)
Note over LSM,AEL: Runtime
loop 매 3초
LSM->>AEL: TryFlushOfflineQueue()
end
loop 매 5초
LSM->>AEL: ProcessPendingEvents()
end
end

비동기로 초기화되는 Firebase와 이벤트 수집 SDK는 LogSystemManager.Update에서 Tick()으로 완료 여부를 확인한다. 둘 다 초기화되면 sdkInitCompleted를 세우고 Tick 호출을 멈춘다. 이와 별개로 3초마다 오프라인 큐 flush, 5초마다 대기 이벤트 처리를 시도한다.

flowchart TD
A[Update] --> B{sdkInitCompleted?}
B -->|No| C[FirebaseSystem.Tick]
C --> D[EventSdkSystem.Tick]
D --> E{Both Initialized?}
E -->|Yes| F[sdkInitCompleted = true<br/>Tick 호출 중단]
E -->|No| G[Continue]
B -->|Yes| H[Skip Tick]
A --> I{3초 경과?}
I -->|Yes| J[TryFlushOfflineQueue]
A --> K{5초 경과?}
K -->|Yes| L[ProcessPendingEvents]

앱 시작부터 로비 진입까지, 게임 선택부터 퇴장까지의 각 단계를 start_*/end_* 쌍과 단독 info_* 이벤트로 기록한다.

sequenceDiagram
participant User
participant App
participant Server as Config/API Server
participant Analytics as Firebase/Event SDK
User->>App: 앱 실행
App->>Analytics: app_launch (Event SDK)
App->>Analytics: start_init_fb_sdk
App->>Analytics: end_init_fb_sdk
App->>Analytics: start_load_bi_scene
App->>Analytics: end_load_bi_scene
Note over App,Analytics: 콜드 스타트 완료
App->>Analytics: start_load_intro_scene
App->>Analytics: end_load_intro_scene
App->>Server: Config API 요청
App->>Analytics: start_download_config
Server-->>App: Config 응답 (모니터링/이벤트 SDK 설정 포함)
App->>Analytics: end_download_config
Note over App: MonitoringSystem/EventSdkSystem 설정 적용
App->>Analytics: info_init_system
alt 버전 업데이트 필요
App->>Analytics: info_version_check
App->>Analytics: info_update_popup
App->>Analytics: info_update_action
end
alt 점검/제재
App->>Analytics: info_maintenance_notice
App->>Analytics: info_user_sanction
end
App->>Analytics: start_login
App->>Server: 로그인 요청
Server-->>App: 로그인 응답
App->>Analytics: end_login
App->>Analytics: start_connect_server
App->>Server: 게임 서버 연결
Server-->>App: 연결 완료
App->>Analytics: end_connect_server
App->>Analytics: start_api_lobby_info
App->>Server: 로비 정보 요청
Server-->>App: 로비 정보 응답
App->>Analytics: end_api_lobby_info
App->>Analytics: start_load_lobby_scene
App->>Analytics: end_load_lobby_scene
App->>Analytics: info_lobby_ready
Note over App,Analytics: 로비 진입 완료
sequenceDiagram
participant User
participant App
participant Server as Game Server
participant Analytics as Firebase/Event SDK
User->>App: 게임 선택
App->>Analytics: info_select_game
App->>Analytics: info_check_game_asset
App->>Analytics: start_api_join_room
App->>Server: 방 입장 요청
Server-->>App: 방 정보 응답
App->>Analytics: end_api_join_room
alt 에셋 다운로드 필요
App->>Analytics: start_download_asset
App->>Server: 에셋 번들 다운로드
Server-->>App: 에셋 데이터
App->>Analytics: end_download_asset
end
App->>Analytics: start_load_game_scene
App->>Analytics: end_load_game_scene
App->>Analytics: info_game_ready
Note over App,Analytics: 게임 플레이
User->>App: 게임 나가기
App->>Analytics: info_game_exit

이벤트를 보낼 SDK를 Flags enum으로 고른다.

[Flags]
public enum LogTarget
{
None = 0,
Firebase = 1 << 0,
AppsFlyer = 1 << 1,
EventSdk = 1 << 2,
FirebaseAndEventSdk = Firebase | EventSdk,
All = Firebase | AppsFlyer | EventSdk,
}
flowchart TD
A[LogEvent 호출] --> B{ShouldQueueEvent?}
B -->|네트워크 끊김| C[EnqueueOfflineEvent]
B -->|SDK 미초기화| C
B -->|정상| D[SendToSDK]
C --> E[offlineQueue 저장<br/>최대 20개, FIFO]
D --> F{LogTarget}
F -->|Firebase| G[FirebaseSystem]
F -->|AppsFlyer| H[AppsFlyerSystem]
F -->|EventSdk| I[EventSdkSystem]
subgraph "오프라인 큐 Flush"
J[3초마다 / 포그라운드 복귀] --> K[TryFlushOfflineQueue]
K --> L{네트워크 + SDK Ready?}
L -->|Yes| D
L -->|No| M[대기]
end

네트워크가 끊겼거나 SDK가 아직 초기화되지 않았을 때 이벤트를 메모리에 임시 저장한다. 기존에는 SDK마다 따로 처리하던 대기 이벤트를 AppEventLogger.offlineQueue 하나로 합쳤다.

설정 값 설명
MAX_OFFLINE_QUEUE_SIZE 20 최대 보관 이벤트 수 (FIFO)
Flush 주기 3초 큐 전송 시도 주기
Flush 트리거 포그라운드 복귀 OnApplicationPause(false)
앱 종료 시 유실 영구 저장 없음
private class OfflineEvent
{
public string EventName;
public Dictionary<string, object> Parameters;
public LogTarget Target;
public float EnqueueTime;
}
  • PC 플랫폼은 Firebase SDK를 지원하지 않아 GA4 Measurement Protocol로 대체한다.
  • 모바일은 Firebase SDK를 직접 쓴다.
  • CheckAndFixDependenciesAsync()로 비동기 초기화하고 Tick()에서 완료를 처리한다.
stateDiagram-v2
[*] --> Initializing: Initialize()
Initializing --> PC_Skip: Platform == PC
Initializing --> Pending: CheckAndFixDependenciesAsync()
PC_Skip --> Ready: OnInitializationComplete(true)
Pending --> Ready: Tick() + Success
Pending --> Failed: Tick() + Error
FirebaseSystem.Initialize();
FirebaseSystem.LogEvent(eventName, parameters);
FirebaseSystem.SetUserId(userId);
FirebaseSystem.SetUserProperty(key, value);
FirebaseSystem.IsInitialized; // 초기화 완료 여부

initSDK() + startSDK()로 동기 초기화되어 즉시 완료된다. 기존 UserTracking에서 구조만 분리했다.

내부 분석용 이벤트 수집 SDK를 감싼다.

  • InitializeAsync()로 비동기 초기화한다.
  • 앱의 현재 서버 환경 값을 읽어 SDK 환경으로 쓴다. qa·dev는 dev로 정규화한다.
  • 슬롯 스핀, 테이블 게임 등 게임별 특화 이벤트를 제공한다.
stateDiagram-v2
[*] --> Initializing: Initialize()
Initializing --> Pending: InitializeAsync(env)
Pending --> Ready: Tick() + Success
Pending --> Failed: Tick() + Error
state Initializing {
[*] --> LoadEnv: AppEnvironment.Instance
LoadEnv --> NormalizeEnv: qa/dev → dev
}
EventSdkSystem.Initialize();
EventSdkSystem.LogCustomEvent(eventName, parameters);
EventSdkSystem.LogAppLaunch();
EventSdkSystem.LogAppExit(exitType);
EventSdkSystem.LogSlotSpin(params);
EventSdkSystem.SetUserId(userId);
EventSdkSystem.IsInitialized; // 초기화 완료 여부

MonitoringSystem은 FPS와 메모리 사용량을 매 프레임 확인하고, 임계값을 넘거나 OS lowMemory 콜백이 오면 AppEventLogger를 통해 Firebase와 이벤트 수집 SDK에 이벤트를 보낸다. 백그라운드에서 포그라운드로 돌아온 뒤 첫 프레임까지의 시간(웜 스타트)도 측정한다.

graph TB
subgraph "MonitoringSystem"
INIT[Initialize<br/>AfterSceneLoad 자동 호출]
CONFIG[ApplyApiConfig<br/>CDN 설정 적용]
UPDATE[Update Loop<br/>매 프레임]
end
subgraph "Monitors"
FPS[FPS Monitor]
MEM[Memory Monitor]
WARM[Warm Start<br/>포그라운드 복귀 시간]
end
subgraph "Output"
AEL[AppEventLogger<br/>→ Firebase + Event SDK]
end
INIT --> CONFIG
CONFIG --> UPDATE
UPDATE --> FPS
UPDATE --> MEM
UPDATE --> WARM
FPS --> AEL
MEM --> AEL
WARM --> AEL
  • 씬 로드 직후와 포그라운드 복귀 직후에는 일시적인 FPS 저하를 무시하는 유예 기간을 둔다.
  • 메모리는 SystemInfo.systemMemorySize(시스템 메모리) 대비 Profiler.GetTotalReservedMemoryLong()(현재 예약 메모리) 비율로 판정하고, OS lowMemory 콜백도 따로 받는다.
  • 웜 스타트는 info_warm_start 이벤트에 warm_start_duration, pause_duration, current_scene을 담아 보낸다.
  • 임계값, 유예 시간, 샘플링 비율, 웜 스타트 측정 여부는 CDN에서 내려받은 설정을 ApplyApiConfig()로 적용한다.
// 모든 SDK에 로깅
AppEventLogger.LogEvent("event_name", parameters);
// 특정 SDK에만 로깅
AppEventLogger.LogEvent("event_name", parameters, LogTarget.Firebase);
AppEventLogger.LogEvent("event_name", parameters, LogTarget.FirebaseAndEventSdk);
// 편의 메서드
AppEventLogger.LogFirebaseOnly("event_name", parameters);
AppEventLogger.LogEventSdkOnly("event_name", parameters);
// 단계 시작 기록
AppEventLogger.RecordStepStart(AppStartProgress.LoadIntroScene);
// 단계 완료 기록
AppEventLogger.RecordStepEnd(AppStartProgress.LoadIntroScene);
// 정보 기록 (start/end 없이 단독)
AppEventLogger.RecordStepInfo(AppStartProgress.SelectGame, additionalParams);
// 모든 SDK에 User ID 설정
AppEventLogger.SetUserIdToAllSDK(userId);
// 씬 로드 알림 (SceneLoadManager에서 호출)
MonitoringSystem.Instance.OnSceneLoaded(sceneName);
// API Config 적용 (설정 로드 후 호출)
MonitoringSystem.Instance.ApplyApiConfig();
// Before
UserTracking.Instance.LogEventFirebase("event", params);
UserTracking.Instance.LogEventAppsFlyer("event");
// After
AppEventLogger.LogEvent("event", params, LogTarget.Firebase);
AppsFlyerSystem.LogEvent("event");
// 또는 통합 호출
AppEventLogger.LogEvent("event", params); // 모든 SDK에 전송
항목 Before After
SDK 초기화 UserTracking.Init() LogSystemManager.InitializeSDKs()
초기화 시점 MonoBehaviour Awake BeforeSceneLoad
Firebase 로깅 UserTracking.LogEventFirebase() FirebaseSystem.LogEvent()
AppsFlyer 로깅 UserTracking.LogEventAppsFlyer() AppsFlyerSystem.LogEvent()
대기 이벤트 각 SDK별 별도 처리 AppEventLogger.offlineQueue 통합