데이터 중심 동기화(Data-oriented Synchronization)는 네트워크 환경에서 데이터의 일관성을 유지하기 위해, 클라이언트와 서버 간의 데이터 동기화 방식을 최적화하는 기법입니다. 특히 실시간 멀티 플레이 게임이나 분산 시스템에서 데이터의 상태를 효율적으로 동기화하는 데 사용됩니다. 이 방법은 주로 상태의 변화를 기준으로 동기화하거나, 데이터의 중요한 부분만 전송하여 네트워크 자원의 효율성을 높입니다.
개념 설명
- 상태 기반(State-based): 클라이언트는 주기적으로 전체 상태를 전송하거나 동기화하고, 서버는 이를 바탕으로 상태를 업데이트합니다. 주기적인 스냅샷이 필요하므로, 자주 변경되는 상태에는 다소 비효율적일 수 있습니다.
- 변화 기반(Changed-based): 상태가 변화할 때만 해당 부분을 전송하는 방식입니다. 델타(변화량)만 전송하여 네트워크 대역폭을 절약할 수 있습니다. 일반적으로 실시간 애플리케이션에서 자주 사용됩니다.
구현 예시
게임에서 여러 사용자가 각자 캐릭터를 조작한다고 가정합시다. 각 캐릭터의 좌표와 상태를 서버와 클라이언트 사이에서 동기화해야 합니다.
1. 데이터 구조 설계 서버는 각 캐릭터의 상태를 저장하고 이를 주기적으로 모든 클라이언트에 전송합니다.
// 예시: 캐릭터 상태를 저장하는 데이터 구조
const characterStates = {
'player1': { x: 100, y: 200, health: 90 },
'player2': { x: 150, y: 250, health: 80 },
// ...
};
2. 클라이언트에서 상태 변경 시 이벤트 전송 클라이언트가 캐릭터의 위치를 변경할 때 서버에 해당 변화를 전송합니다.
socket.emit('updatePosition', {
playerId: 'player1',
x: 105,
y: 210,
});
3. 서버에서 상태 업데이트 및 동기화 서버는 클라이언트에서 수신한 데이터를 기반으로 상태를 업데이트하고, 나머지 클라이언트에 전파합니다.
socket.on('updatePosition', (data) => {
characterStates[data.playerId] = {
...characterStates[data.playerId],
x: data.x,
y: data.y,
};
// 모든 클라이언트에게 동기화 전송
io.emit('syncState', { playerId: data.playerId, x: data.x, y: data.y });
});
4. 클라이언트에서 상태 수신 및 적용 클라이언트는 서버로부터 상태를 수신할 때 해당 데이터를 업데이트합니다.
socket.on('syncState', (data) => {
updateCharacterPosition(data.playerId, data.x, data.y);
});
최적화 방법
- 압축 사용: 전송되는 데이터의 크기를 줄이기 위해 압축 알고리즘을 적용합니다.
- 보간(Interpolation): 서버에서 업데이트 사이에 캐릭터의 움직임을 부드럽게 하기 위해 클라이언트는 보간을 사용할 수 있습니다.
- 데드 리코닝(Dead Reckoning): 예상되는 위치를 기반으로 캐릭터를 예측적으로 이동시켜 네트워크 지연을 보완합니다.
압축 사용
네트워크를 통해 전송되는 데이터의 양을 줄여 대역폭을 절약할 수 있습니다. 이를 위해 데이터 압축 기술을 적용할 수 있습니다.
예시: JSON 데이터를 압축하여 전송
// JSON 데이터 예시
const data = { playerId: 'player1', x: 105, y: 210, health: 90 };
// 전송 전 압축 (Node.js zlib 사용)
const zlib = require('zlib');
const compressedData = zlib.gzipSync(JSON.stringify(data));
// 클라이언트 전송
socket.emit('syncState', compressedData);
// 서버에서 수신 후 압축 해제
socket.on('syncState', (compressedData) => {
const decompressedData = JSON.parse(zlib.gunzipSync(compressedData));
console.log(decompressedData); // { playerId: 'player1', x: 105, y: 210, health: 90 }
});
이 방식은 대량의 데이터를 전송할 때 효과적이며, 네트워크 지연을 줄이고 성능을 향상시킬 수 있습니다.
보간(Interpolation)
보간은 클라이언트에서 두 지점 사이의 상태를 부드럽게 연결해 시각적인 끊김을 줄이는 기술입니다. 예를 들어, 서버가 1초에 한 번씩 캐릭터의 위치를 업데이트할 때, 클라이언트는 중간 위치를 계산하여 자연스러운 움직임을 구현할 수 있습니다.
예시: 클라이언트에서 보간 처리
let lastPosition = { x: 100, y: 200 };
let targetPosition = { x: 150, y: 250 };
let currentTime = 0;
const updateInterval = 1000; // 서버 갱신 주기
function interpolatePosition(deltaTime) {
currentTime += deltaTime;
const t = currentTime / updateInterval; // 0에서 1 사이의 비율
const interpolatedX = lastPosition.x + t * (targetPosition.x - lastPosition.x);
const interpolatedY = lastPosition.y + t * (targetPosition.y - lastPosition.y);
// 캐릭터 위치 업데이트
updateCharacterPosition(interpolatedX, interpolatedY);
}
// 이 코드는 requestAnimationFrame 등을 사용하여 호출
이 코드로 클라이언트는 서버에서 새 데이터르 받을 때까지 자연스러운 애니메이션을 제공합니다.
데드 리코닝(Dead Reckoning)
데드 리코닝은 클라이언트가 서버의 데이터 전송 간격 동안 캐릭터의 예상 위치를 예측하여 업데이트하는 기술입니다. 이 방법은 네트워크 지연을 보완하고 사용자 경험을 개선하는 데 사용됩니다.
예시: 클라이언트에서 데드 리코닝 적용
let lastUpdateTime = Date.now();
let lastPosition = { x: 100, y: 200 };
let velocity = { x: 5, y: 3 }; // 서버에서 받은 속도 정보
function predictPosition() {
const currentTime = Date.now();
const timeElapsed = (currentTime - lastUpdateTime) / 1000; // 초 단위로 변환
const predictedX = lastPosition.x + velocity.x * timeElapsed;
const predictedY = lastPosition.y + velocity.y * timeElapsed;
// 캐릭터 위치 업데이트
updateCharacterPosition(predictedX, predictedY);
}
// 주기적으로 호출하여 위치 예측
setInterval(predictPosition, 16); // 약 60fps
이렇게 하면 서버가 갱신을 보내기 전까지 클라이언트가 부드러운 움직임을 유지할 수 있습니다. 새로운 서버 데이터를 받으면 lastUpdateTime과 lastPosition을 업데이트해 정확도를 맞춥니다.
요약
- 압축 사용은 데이터 전송 효율을 높여 네트워크 비용을 줄입니다.
- 보간(Interpolation)은 서버의 업데이트 간격 동안의 시각적 끊김을 부드럽게 연결합니다.
- 데드 리코닝(Dead Reckoning)은 클라이언트가 서버에서 받는 데이터 사이에 예상되는 위치를 보정하여 네트워크 지연 문제를 완화합니다.
데이터 구조 및 서버 설정 (Node.js)
Node.js 서버의 클라이언트로부터 위치 데이터를 수신하고, 변경된 위치를 다른 클라이언트들에게 BroadCasting합니다.
Node.js TCP 서버 코드:
const net = require('net');
let clients = [];
let playerStates = {}; // 모든 클라이언트의 상태를 저장
const server = net.createServer((socket) => {
console.log('새 클라이언트 연결됨');
clients.push(socket);
// 클라이언트가 위치를 업데이트할 때 데이터 수신
socket.on('data', (data) => {
const receivedData = data.toString();
console.log(`수신된 데이터: ${receivedData}`);
try {
const parsedData = JSON.parse(receivedData);
playerStates[socket.id] = {
x: parsedData.x,
y: parsedData.y,
timestamp: Date.now(),
};
// 모든 클라이언트에 동기화 브로드캐스트
broadcastState();
} catch (error) {
console.error('데이터 파싱 오류:', error);
}
});
socket.on('end', () => {
console.log('클라이언트 연결 종료');
clients = clients.filter((client) => client !== socket);
delete playerStates[socket.id];
});
socket.on('error', (err) => {
console.error(`소켓 에러: ${err.message}`);
});
});
function broadcastState() {
const stateUpdate = JSON.stringify(playerStates);
clients.forEach((client) => {
client.write(stateUpdate);
});
}
server.listen(3000, () => {
console.log('TCP 서버가 3000 포트에서 실행 중입니다...');
});
Unity C# 클라이언트 (상태 수신 및 처리)
Unity 클라이언트는 서버에서 주기적으로 보내는 상태 데이터를 수신하고 이를 반영합니다.
C# TCP 클라이언트 코드:
using System;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class TCPClient : MonoBehaviour
{
private TcpClient client;
private NetworkStream stream;
private byte[] buffer = new byte[4096];
private Vector3 targetPosition;
private float interpolationSpeed = 5f;
void Start()
{
ConnectToServer();
}
void Update()
{
// 사용자 입력으로 위치를 이동시킬 경우, 서버로 데이터 전송
if (Input.GetKey(KeyCode.W))
{
Vector3 newPosition = transform.position + Vector3.forward * Time.deltaTime * 5;
transform.position = newPosition;
string message = JsonUtility.ToJson(new { x = newPosition.x, y = newPosition.z });
byte[] data = Encoding.UTF8.GetBytes(message);
stream.Write(data, 0, data.Length);
}
// 위치 보간 적용
transform.position = Vector3.Lerp(transform.position, targetPosition, interpolationSpeed * Time.deltaTime);
}
void ConnectToServer()
{
try
{
client = new TcpClient("localhost", 3000);
stream = client.GetStream();
Debug.Log("서버에 연결됨");
// 데이터 수신 대기
stream.BeginRead(buffer, 0, buffer.Length, OnDataReceived, null);
}
catch (Exception e)
{
Debug.LogError($"서버 연결 오류: {e.Message}");
}
}
void OnDataReceived(IAsyncResult ar)
{
try
{
int bytesRead = stream.EndRead(ar);
if (bytesRead > 0)
{
string dataReceived = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Debug.Log($"서버로부터 수신된 데이터: {dataReceived}");
// 서버에서 수신한 JSON 데이터를 파싱하여 위치 업데이트
var stateData = JsonUtility.FromJson<ServerState>(dataReceived);
foreach (var state in stateData.PlayerStates)
{
if (state.Id == "player1") // 클라이언트의 ID에 따라 처리
{
targetPosition = new Vector3(state.X, 0, state.Y);
}
}
// 다음 수신 준비
stream.BeginRead(buffer, 0, buffer.Length, OnDataReceived, null);
}
}
catch (Exception e)
{
Debug.LogError($"데이터 수신 오류: {e.Message}");
}
}
void OnApplicationQuit()
{
stream?.Close();
client?.Close();
}
}
// 서버에서 수신한 상태 데이터를 파싱하기 위한 클래스
[Serializable]
public class ServerState
{
public PlayerState[] PlayerStates;
}
[Serializable]
public class PlayerState
{
public string Id;
public float X;
public float Y;
}
설명:
- Node.js 서버는 모든 클라이언트의 위치 데이터를 유지하고, 변경이 발생할 때마다 모든 클라이언트에게 전체 상태를 브로드캐스트합니다.
- Unity C# 클라이언트는 주기적으로 서버에서 수신한 데이터를 통해 다른 플레이어들의 위치를 업데이트하고 보간(Interpolation)을 통해 부드러운 이동을 제공합니다.
이 방식은 TCP의 안정적인 연결 특성을 이용하여 데이터 전송을 보장하고, 클라이언트 간의 실시간 동기화를 유지할 수 있도록 합니다.
'TIL' 카테고리의 다른 글
Tower Defense Online Project 트러블 슈팅 - 2 (0) | 2024.11.12 |
---|---|
Tower Defense Online Project 트러블 슈팅 - 1 (0) | 2024.11.12 |
상태 동기화에 대하여 (1) | 2024.11.07 |
스파르타 코딩 클럽 모의 면접 깔끔한 답변 준비 (0) | 2024.11.01 |
소켓 오류 난 너가 밉다!!! (1) | 2024.11.01 |