저는 AI를 적극적으로 활용하는 개발자입니다.
코드 구현은 AI 도구와 협업하고, 저는 문제 분석, 기술 설계, 트러블슈팅, 최종 검증에 집중합니다. 모든 기술적 의사결정과 트러블슈팅은 제가 직접 수행한 것이며, AI는 그 과정을 가속화하는 도구였습니다.
이 블로그는 그 판단과 사고의 기록입니다.
"어떤 도구를 쓰느냐"보다 "어떤 문제를 해결하느냐"가 진짜 개발자의 가치라고 믿습니다.
I believe a developer's value lies in "what problems they solve," not "what tools they use."
LAN 테스트에서는 이상 없던 멀티플레이어 게임이 인터넷으로 나가는 순간
캐릭터가 끊기고, 유닛이 순간이동하고, 분명히 맞췄는데 빗나간다.
코드를 아무리 뜯어봐도 로직에는 문제가 없다.
클라이언트 예측도 구현했고, RPC 호출도 최소화했다. 그런데 왜?
답은 의외로 코드 바깥에 있었다.
ini 파일 속에 숨어있는 기본값들.
이것들이 2025년 고속 네트워크 환경에서 조용히 병목을 만들고 있었다.
기본값의 함정
언리얼 엔진의 네트워크 기본 설정은 놀라울 정도로 보수적이다.
TotalNetBandwidth(서버 총 대역폭)의 기본값은 32,000 바이트/초다. 32KB/s...
동시 접속자가 5명만 넘어가도 패킷 손실이 발생할 수 있는 수치다.
왜 이렇게 낮게 설정되어 있을까? Epic Games의 의도는 명확하다.
대부분의 프로젝트는 대규모 멀티플레이어가 아니고,
최적화되지 않은 넷코드에서 대역폭을 무한정 허용하면 오히려 문제가 커진다.
그래서 안전한 하한선을 기본값으로 잡은 것이다.
하지만 이 "안전한 기본값"이 실제 게임에서는 발목을 잡는다.
Deep Rock Galactic 같은 상용 게임들이 이 값을 크게 올려서 출시하는 데는 이유가 있다.
대역폭과 트래픽 제어의 이해
GameNetworkManager는 서버가 사용할 수 있는 총 데이터 용량과 개별 유저에게 할당되는 범위를 지정한다.
TotalNetBandwidth는 서버가 전체 클라이언트에게 전송할 수 있는 초당 최대 바이트다.
이 값이 낮으면 액터가 많은 지역에서 동기화가 끊기거나 유닛이 순간이동하는 현상이 발생한다.
현대적인 서버 환경에서는 최소 100,000에서 500,000 이상으로 설정하는 것이 일반적이다.
MaxDynamicBandwidth와 MinDynamicBandwidth는 한 명의 유저가 동적으로 할당받는 대역폭의 범위다.
네트워크가 혼잡할 때는 Min까지 줄여서 연결을 유지하고,
쾌적할 때는 Max까지 사용해서 동기화 정밀도를 높인다.
이 메커니즘이 특정 유저의 과도한 데이터 점유를 방지하고 공정한 동기화 환경을 만든다.
한 가지 오해하지 말아야 할 점이 있다.
TotalNetBandwidth=500,000으로 설정한다고 해서 항상 500KB를 쓰는 게 아니다.
동기화할 내용이 적으면 평소엔 아주 적은 양만 사용한다.
이 값은 "최대치"일 뿐이므로, 단순히 수치를 높이는 것만으로 비용이 늘어나지 않는다.
Tick Rate : 게임의 '손맛’을 결정하는 숫자
NetServerMaxTickRate는 서버가 초당 네트워크 업데이트를 수행하는 횟수다.
이 숫자가 게임의 반응성을 직접적으로 좌우한다.
60Hz로 설정하면 패킷 전송 간격은 약 16.67ms가 된다.
30Hz면 33.33ms. 차이가 미미해 보일 수 있지만, 빠른 액션 게임에서 이 16ms의 차이는 히트박스 판정의 정확도를 결정한다.
TPS, FPS처럼 정밀한 피격 판정이 필요한 장르에서는 60 이상의 틱 레이트가 사실상 필수다.
30 이하에서는 클라이언트가 분명히 맞췄다고 생각한 공격이...
서버에서는 빗나간 것으로 처리되는 "히트박스 어긋남"이 체감될 수 있다.
다만 틱 레이트를 높이면 서버 CPU 부하가 증가한다.
언리얼 포럼의 한 개발팀은 대규모 전략 게임에서 의도적으로 10Hz로 낮춰 서버 비용을 절감한 사례도 공유했다.
결국 장르와 게임의 특성에 맞는 균형점을 찾아야 한다.
스피드핵을 막는 엔진 내장 기능
bMovementTimeDiscrepancyDetection은
클라이언트가 서버로 보내는 이동 타임스탬프의 오차를 검사하는 기능이다. 기본값은 꺼져 있다.
작동 원리는 이렇다.
언리얼 엔진의 CharacterMovementComponent는 클라이언트의 이동을 서버에서 재현할 때,
클라이언트가 보낸 타임스탬프가 아니라 서버의 타임스탬프를 기준으로 델타 타임을 계산한다.
이 설계 자체가 클라이언트의 로컬 시간 조작을 무력화하기 위한 것이다.
공식 문서에서도 명확히 언급한다.
“To provide an extra layer of security, the majority of these calculations are performed with the server’s timestamps instead of the client’s, preventing clients from hacking their speed by speeding up their local game clock.”
추가 보안 계층을 제공하기 위해 이러한 계산의 대부분은 클라이언트의 타임스탬프 대신 서버의 타임스탬프를 사용하여 수행되므로 클라이언트가 로컬 게임 클럭 속도를 높여 속도를 해킹하는 것을 방지할 수 있습니다.
bMovementTimeDiscrepancyDetection을 활성화하면 이 검증이 더 엄격해진다.
허용 오차를 넘어서는 시간 차이가 발생하면
서버는 해당 클라이언트의 위치를 강제로 이전 위치로 되돌리거나(Correction) 연결을 종료할 수 있다.
Reddit의 Dead by Daylight 커뮤니티에서 한 개발자가 이 기능의 활성화 방법을 상세히 설명하며,
많은 언리얼 기반 게임들이 이 내장 기능을 활용하지 않아 스피드핵에 취약하다고 지적한 바 있다.
우리 프로젝트는 현재 UCharacterMovementComponent를 상속받아 사용하고 있지만,
특정한 타임스탬프 조작 방지 로직이 개별적으로 구현되어 있지 않다.
bMovementTimeDiscrepancyDetection을 활성화하면
엔진 수준에서 클라이언트의 시간 가속을 감지하므로 보안성이 즉시 강화된다.
중요한 점: 단순히 감지만 하는 게 아니라 실제로 위치를 되돌리는 처리까지 수행하려면 추가 옵션이 필요하다.
bMovementTimeDiscrepancyDetection=true
bMovementTimeDiscrepancyResolution=true
bMovementTimeDiscrepancyForceCorrect=true
⚙️권장 설정값
일반적인 데디케이티드 서버 환경 설정
[/Script/Engine.GameNetworkManager]
TotalNetBandwidth=500000
MaxDynamicBandwidth=15000
MinDynamicBandwidth=5000
bMovementTimeDiscrepancyDetection=true
[/Script/OnlineSubsystemUtils.IpNetDriver]
NetServerMaxTickRate=60
MaxInternetClientRate=15000
Steam 환경 설정
Steam 라이브러리를 통해 통신할 경우 별도의 섹션에 동일한 값을 추가해야 엔진이 인식한다.
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetServerMaxTickRate=60
MaxClientRate=15000
MaxInternetClientRate=15000
Deep Rock Galactic의 실제 설정값을 FModel로 추출한 개발자에 따르면,
이 게임은 TotalNetBandwidth=600000, MaxDynamicBandwidth=25000을 사용한다.
4인 협동 플레이에서 안정적인 네트워크 경험을 제공하는 것으로 유명한 게임이다.
참고 : Steam P2P를 사용한다면 비용 측면에서 큰 이점이 있다. 유저 한 명이 호스트가 되는 P2P 방식에서는 서버 비용이 0원이다. 데이터 전송 부담은 유저의 인터넷 회선이 지게 되므로, 비용 부담 없이 성능을 높일 수 있다.
프로젝트 적용 전, 검증 단계
현재 진행 중인 GuardianAndSeeker 프로젝트의 DefaultEngine.ini를 확인했을 때,
별도의 네트워크 대역폭 및 틱 레이트 설정이 없었다.
이 말은 엔진 기본값인 30Hz(TickRate)와 32,000 Byte/sec(TotalBandwidth)를 그대로 사용하고 있다는 뜻이다.
우리 게임은 TPS(Seeker)와 RTS(Guardian)가 혼합된 장르다.
다수의 유닛이 등장하고 액션성이 강조된다. 이 조합에서 기본값은 좋지 않다.
- 32KB/s의 대역폭으로 수십 개의 유닛 위치를 동기화하려고 하면?
패킷은 쌓이고, 우선순위가 낮은 유닛의 업데이트는 밀리고, 결국 화면에서는 유닛들이 순간이동하는 것처럼 보인다. - 30Hz의 틱 레이트로 빠른 액션의 히트박스를 판정하면?
클라이언트에서 분명히 맞은 것 같은 공격이 서버에서는 빗나간 것으로 처리된다.
우리 프로젝트처럼 TPS와 RTS가 혼합된 장르에서는 60Hz가 사실상 필수다.
Seeker 플레이어는 정밀한 피격 판정이 필요하고,
Guardian(RTS 모드) 플레이어는 다수의 유닛이 동시에 이동할 때 위치 동기화가 정확해야 한다.
30Hz에서는 클라이언트가 분명히 맞췄다고 생각한 공격이
서버에서는 빗나간 것으로 처리되는 "히트박스 어긋남"이 체감될 수 있다.
그러나! 틱 레이트를 30에서 60으로 높이면 분명히 비용이 증가한다. 두 가지 측면에서다.
첫째, CPU 연산 비용.
서버가 물리 연산, 충돌 판정, AI 로직 등을 초당 2배 더 많이 수행해야 한다.
기존에 서버 한 대당 4개의 방을 돌릴 수 있었다면, 60Hz로 올릴 경우 2~3개로 줄어들 수 있다.
동일한 유저 수를 수용하려면 더 많은 서버 인스턴스가 필요하다.
둘째, 네트워크 전송 비용.
패킷을 더 자주 보내기 때문에 데이터 패킷의 헤더 정보가 쌓여 전체 트래픽이 약 20~30% 증가한다.
대부분의 클라우드 서비스는 Egress(서버에서 외부로 나가는 데이터)에 대해 비용을 청구한다.
우리 프로젝트 상황에서는 몇 가지 완화 요소가 있다.
- Steam P2P 방식을 사용하면 서버 비용 자체가 없다.
- 그리고 RTS 모드에서 유닛이 수백 마리씩 등장하는 경우, 중요한 액터만 60Hz로 보내고 멀리 있는 유닛은 낮은 빈도로 업데이트하는 'Replication Graph' 최적화를 병행하면 비용을 방어할 수 있다.
현실적인 전략은 이렇다.
일단 60Hz로 개발 및 QA를 진행하고,
정식 출시 전 가입자당 수익 대비 서버 유지비를 계산해서 45Hz나 50Hz로 타협점을 찾는 것.
개발 단계에서 품질을 먼저 확보하는 것이 나중에 네트워크 코드를 다시 고치는 것보다 비용이 훨씬 적게 든다.
[/Script/Engine.GameNetworkManager]
TotalNetBandwidth=262144 // 256KB (약 16명까지 안전하게 커버)
MaxDynamicBandwidth=15360 // 15KB (인당 비용 방어선)
MinDynamicBandwidth=4096 // 4KB
bMovementTimeDiscrepancyDetection=true // 네트워크 시간 불일치 감지
bMovementTimeDiscrepancyResolution=true // 네트워크 시간 불일치 해결
[/Script/OnlineSubsystemUtils.IpNetDriver]
NetServerMaxTickRate=30 // 틱 레이트 30
MaxInternetClientRate=15360 // 15KB
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName=OnlineSubsystemSteam.SteamNetConnection // 네트워크 연결 클래스
NetServerMaxTickRate=30 // 틱 레이트 30
MaxClientRate=15360 // 15KB
MaxInternetClientRate=15360 // 15KB
적용 후 반드시 해야 할 것
설정을 바꿨다고 끝이 아니다.
stat net 명령어로 서버 FPS가 60을 안정적으로 유지하는지 확인해야 한다.
특히 RTS 모드에서 유닛이 대량으로 등장하는 상황을 시뮬레이션하면서 체크해야 한다.
Network Profiler로 실제 대역폭 사용량과 패킷 손실률을 측정하고,
데이터에 기반해 설정값을 미세 조정하는 과정이 필요하다. 설정 변경은 시작일 뿐이다.
⚠️주의사항 : 이것은 만능 해결책이 아니다
Reddit 댓글들에서 반복적으로 지적되는 포인트가 있다.
“대역폭 설정을 올리는 건 제대로 최적화되지 않은 넷코드의 땜빵이다.”
맞는 말이다. 클라이언트 사이드 예측이 없는 상태에서 대역폭만 늘리면 스케일 문제가 발생한다.
기본값이 낮게 설정된 이유도 최적화되지 않은 프로젝트에서 대역폭을 무한정 쓰는 것을 방지하기 위해서다.
이 설정 변경이 효과를 발휘하려면 다음 전제가 필요하다.
우선, 리플리케이션 버퍼를 넘치게 하지 않아야 한다.
모든 것을 RPC로 처리하려 하지 말고, 가능하면 리플리케이티드 변수(bool, float, vector 등)로 가볍게 유지해야 한다.
그리고 클라이언트 예측 시스템이 구현되어 있어야 한다. 대역폭 설정은 레이턴시 문제를 해결하지 못한다.
레이턴시는 클라이언트 예측으로 처리해야 한다.
🌟인사이트
기본값을 의심하라.
언리얼의 네트워크 기본 설정은 "안전한 최소값"이다.
프로젝트의 규모와 장르에 맞게 조정이 필요하다.
틱 레이트는 장르에 맞게.
액션 게임은 60Hz 이상, 전략 게임은 낮춰도 괜찮다.
서버 비용과 반응성 사이의 균형점을 찾아야 한다.
보안은 공짜로 얻을 수 있다.
bMovementTimeDiscrepancyDetection은 엔진 내장 기능인데 기본값이 꺼져 있다.
활성화만 해도 기본적인 스피드핵을 막을 수 있다.
설정 변경은 최적화를 대체하지 않는다.
대역폭을 늘리기 전에 리플리케이션 파이프라인을 먼저 정리하라.
설정 변경은 최적화된 넷코드 위에서 효과를 발휘한다.
네트워크 프로파일러를 활용하라.
Unreal Insights의 Network Insights 기능으로
실제 대역폭 사용량과 패킷 손실률을 측정하고, 데이터에 기반해 설정값을 미세 조정해야 한다.
마무리
멀티플레이어 게임의 "느낌"은 코드의 로직만으로 결정되지 않는다.
ini 파일 속 숫자 하나가 플레이어의 체감을 완전히 바꿀 수 있다.
GuardianAndSeeker 프로젝트에서 이 설정을 적용하면,
특히 RTS 모드에서 다수의 유닛이 동시에 이동할 때 발생하는 위치 동기화 오차(Rubber-banding)가 획기적으로 줄어들 것이다. TPS 모드에서는 히트박스 판정이 더 정확해지고, "분명히 맞췄는데 안 맞음" 같은 유저 불만이 줄어들 것이다.
기본값을 그대로 두지 마라. 하지만 무작정 올리지도 마라.
프로젝트의 특성을 이해하고, 프로파일링 데이터를 보면서, 적절한 균형점을 찾아가는 것. 그게 네트워크 최적화의 본질이다.
결국 좋은 멀티플레이어 경험은 “보이지 않는 곳에서 얼마나 세심하게 조율했는가”에서 나온다.
'Dev. > UE 언리얼 엔진' 카테고리의 다른 글
| [GAS] 이펙트가 프레임을 떨어뜨리는 이유, 오버드로 | 컷아웃 기법 (1) | 2026.01.03 |
|---|---|
| [GAS] 렌더링 최적화 : 30 FPS에서 60 FPS까지 (2) | 2025.12.30 |
| [GAS] 함정 시스템 개선 (0) | 2025.12.24 |
| [GAS] RTS 미니맵 구현 & 성능 최적화 (0) | 2025.12.05 |
| 언리얼 엔진 5.4 + Visual Studio 2026 호환성 문제 해결 (0) | 2025.12.02 |