프로젝트 Final

9일 일지

rabbit97 2024. 12. 9. 21:59

# 오늘 개발 진행 상황

 

금요일에 중간 발표가 끝나고 아직까지 의문인 점

 

왜 클라이언트는 패킷을 똑바로 받지 못하는가...??

 

오늘도 그 문제에 봉착했다.

 

혹시나 하면서 생각해본 내용

 

1. 서버 문제?

 

2. 코드 구조 최적화?

 

3. 너무 많은 패킷을 보내 밀려서 안되는거라면 우선순위를 정해보기?

 

코드 최적화는 천천히 해 나가면 되는거고

 

1, 3번을 한번에 확인 할 수 있는 방법!

 

브렌치를 따로 파고 서버를 내가 열어보자!

 

일단 브렌치를 따로 테스트로 하나 만들었다.

 

import CustomError from './../utils/errors/customError.js';
import ErrorCodes from './../utils/errors/errorCodes.js';
import config from './../config/config.js';
import validateSequence from './../utils/socket/sequence.js';
import packetParser from './../utils/packet/parser/packetParser.js';
import { getHandlerByPacketType } from './../handlers/index.js';
import handleError from './../utils/errors/errorHandler.js';

// 패킷 우선순위 정의
const PACKET_PRIORITY = {
  HIGH: 1,
  MEDIUM: 2,
  LOW: 3,
};

// 패킷 타입별 우선순위 매핑
const getPacketPriority = (packetType) => {
  const { packetType: PACKET_TYPE } = config.packet;

  // 높은 우선순위: 공격, 카드 사용, 리액션 등 즉각적인 응답이 필요한 패킷
  if (
    [
      PACKET_TYPE.USE_CARD_REQUEST,
      PACKET_TYPE.REACTION_REQUEST,
      PACKET_TYPE.PASS_DEBUFF_REQUEST,
    ].includes(packetType)
  ) {
    return PACKET_PRIORITY.HIGH;
  }

  // 중간 우선순위: 게임 상태 변경, 방 참가/퇴장 등
  if (
    [
      PACKET_TYPE.GAME_PREPARE_REQUEST,
      PACKET_TYPE.GAME_START_REQUEST,
      PACKET_TYPE.JOIN_ROOM_REQUEST,
      PACKET_TYPE.LEAVE_ROOM_REQUEST,
    ].includes(packetType)
  ) {
    return PACKET_PRIORITY.MEDIUM;
  }

  // 낮은 우선순위: 위치 업데이트 등 지연 처리 가능한 패킷
  return PACKET_PRIORITY.LOW;
};

class PacketQueue {
  constructor() {
    this.queues = {
      [PACKET_PRIORITY.HIGH]: [],
      [PACKET_PRIORITY.MEDIUM]: [],
      [PACKET_PRIORITY.LOW]: [],
    };
    this.processing = false;
  }

  enqueue(packet) {
    const priority = getPacketPriority(packet.packetType);
    this.queues[priority].push(packet);
    this.processQueue();
  }

  async processQueue() {
    if (this.processing) return;
    this.processing = true;

    try {
      // 우선순위 순서대로 처리
      for (const priority of Object.values(PACKET_PRIORITY)) {
        const queue = this.queues[priority];
        while (queue.length > 0) {
          const packet = queue.shift();
          try {
            const handler = getHandlerByPacketType(packet.packetType);
            await handler(packet);
          } catch (error) {
            handleError(packet.socket, error);
          }
        }
      }
    } finally {
      this.processing = false;
      // 큐에 남은 패킷이 있다면 계속 처리
      if (Object.values(this.queues).some((queue) => queue.length > 0)) {
        this.processQueue();
      }
    }
  }
}

// 소켓별 패킷 큐 저장
const socketQueues = new Map();

const onData = (socket) => async (data) => {
  if (!socket) {
    throw new CustomError(
      ErrorCodes.SOCKET_ERROR,
      `소켓을 찾을 수 없거나 연결이 끊겼다.`,
      socket.sequence,
    );
  }

  socket.buffer = Buffer.concat([socket.buffer, data]);

  const totalHeaderLength = config.packet.totalHeaderLength;
  while (socket.buffer.length >= totalHeaderLength) {
    let offset = 0;
    const packetType = socket.buffer.readUInt16BE(offset);
    offset += config.packet.payloadOneofCaseLength;

    const versionLength = socket.buffer.readUInt8(offset);
    offset += config.packet.versionLength;

    const version = socket.buffer.subarray(offset, offset + versionLength).toString('utf-8');
    offset += versionLength;

    if (version !== config.client.version) {
      throw new CustomError(
        ErrorCodes.CLIENT_VERSION_MISMATCH,
        '클라이언트 버전이 일치하지 않습니다.',
        socket.sequence,
      );
    }

    const sequence = socket.buffer.readUInt32BE(offset);
    offset += config.packet.sequenceLength;
    const isValidSequence = validateSequence(socket, sequence);
    if (!isValidSequence) {
      throw new CustomError(
        ErrorCodes.INVALID_SEQUENCE,
        `패킷이 중복되거나 누락되었다: 예상 시퀀스: ${socket.sequence + 1}, 받은 시퀀스: ${sequence}`,
        socket.sequence,
      );
    }

    const payloadLength = socket.buffer.readUInt32BE(offset);
    offset += config.packet.payloadLength;

    if (socket.buffer.length >= payloadLength + totalHeaderLength) {
      const payloadBuffer = socket.buffer.subarray(offset, offset + payloadLength);

      try {
        const { payload } = packetParser(payloadBuffer);
        socket.buffer = socket.buffer.subarray(offset + payloadLength);

        // 소켓별 패킷 큐 가져오기 또는 생성
        if (!socketQueues.has(socket)) {
          socketQueues.set(socket, new PacketQueue());
        }
        const packetQueue = socketQueues.get(socket);

        // 패킷을 큐에 추가
        packetQueue.enqueue({
          socket,
          packetType,
          payload,
          sequence,
        });

        break;
      } catch (error) {
        handleError(socket, error);
        break;
      }
    } else {
      break;
    }
  }
};

export default onData;

 

패킷을 큐로 지정해 우선순위를 정해보았다.

 

그리고 이 브렌치로 서버를 파서 내 게임용 컴퓨터랑 맥북이랑 두 클라이언트로 테스트를 해보았다.

 

굳이 테스트를 파서 서버까지 따로 판 이유는 배포된 서버에서 확인 하려면 메인으로 올려야하는데 단순히 테스트 용을 메인에 올리고 아니면 빼는 작업이 더 귀찮을 것 같다는 생각

 

결과는??

 

큰 차이가 없었다.

 

우선 순위가 문제가 아니였다.

 

이후로 팀원들끼리 회의를 통해 알게 된 사실

 

저번 주 까지는 모두가 패킷에 대한 반응이 늦었었는데 그 동안 뭘 건드렸는진 모르겠지만 일부 클라이언트는 동작이 바로바로 되는 이유모를 해결 사항이 생겼다.

 

하지만 일부는 아직 안되기에 완벽한 해결은 아니지만...

 

이게 정말 이상한게 처음에 테스트로 판 서버에서는 1명은 아에 안되고 한명은 빌드한 클라이언트에서 안되었다가 유니티 환경에서 실행하면 또 잘 되었다.

 

그 다음 비교를 위해 메인에 이미 배포가 되어있는 서버로 이동해서 테스트를 하니 이제는 내가 패킷 반응이 늦었다.

 

클라이언트 버전은 동일하고 서버 로직도 큐로 관리하냐 마냐의 차이인데 이런 이유는.......... 뭐지?

 

아무튼 공통적으로 잘 작동하는 2명이 있기에 일단 이 문제보단 진도를 좀 더 나가다가 원인을 찾아보기로 하고

 

나는 저번 주에 아직 해결하지 못한 테스트 코드를 작성하기로 했다.

 

제스트랑 다른 부하테스트때 쓸 jmeter 나 artillery말고 오직 노드에 내장되어있는 모듈로 테스트 코드 짜기!

 

import assert from 'assert';
import { Buffer } from 'buffer';
import config from '../../config/config.js';

// 테스트용 패킷 생성 함수
function createTestPacket(packetType, payload) {
  const version = '1.0.0';
  const versionBuffer = Buffer.from(version);
  const sequence = 1;

  const payloadBuffer = Buffer.from(JSON.stringify(payload));

  const headerSize = 2 + 1 + versionBuffer.length + 4 + 4;
  const buffer = Buffer.alloc(headerSize + payloadBuffer.length);

  let offset = 0;
  buffer.writeUInt16BE(packetType, offset);
  offset += 2;

  buffer.writeUInt8(versionBuffer.length, offset);
  offset += 1;

  versionBuffer.copy(buffer, offset);
  offset += versionBuffer.length;

  buffer.writeUInt32BE(sequence, offset);
  offset += 4;

  buffer.writeUInt32BE(payloadBuffer.length, offset);
  offset += 4;

  payloadBuffer.copy(buffer, offset);

  return buffer;
}

console.log('패킷 핸들러 유닛 테스트 시작');

// 1. 패킷 타입 테스트
try {
  const testPayload = { test: 'data' };
  const packet = createTestPacket(config.packet.packetType.USE_CARD_REQUEST, testPayload);

  assert.strictEqual(
    packet.readUInt16BE(0),
    config.packet.packetType.USE_CARD_REQUEST,
    '패킷 타입이 일치해야 합니다',
  );
  console.log('✓ 패킷 타입 테스트 통과');
} catch (error) {
  console.error('✗ 패킷 타입 테스트 실패:', error.message);
}

// 2. 버전 테스트
try {
  const testPayload = { test: 'data' };
  const packet = createTestPacket(1, testPayload);

  const versionLength = packet.readUInt8(2);
  const version = packet.toString('utf8', 3, 3 + versionLength);

  assert.strictEqual(version, '1.0.0', '버전이 일치해야 합니다');
  console.log('✓ 버전 테스트 통과');
} catch (error) {
  console.error('✗ 버전 테스트 실패:', error.message);
}

// 3. 시퀀스 번호 테스트
try {
  const testPayload = { test: 'data' };
  const packet = createTestPacket(1, testPayload);

  const versionLength = packet.readUInt8(2);
  const sequence = packet.readUInt32BE(3 + versionLength);

  assert.strictEqual(sequence, 1, '시퀀스 번호가 일치해야 합니다');
  console.log('✓ 시퀀스 번호 테스트 통과');
} catch (error) {
  console.error('✗ 시퀀스 번호 테스트 실패:', error.message);
}

console.log('패킷 핸들러 유닛 테스트 완료');
import net from 'net';
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import { Buffer } from 'buffer';
import protobuf from 'protobufjs';
import path from 'path';
import config from '../../config/config.js';

const HOST = '0.0.0.0';
const PORT = 5555;
const NUM_CLIENTS = 10;
const TEST_DURATION = 30000;
const CONNECTION_DELAY = 1000;
const RECONNECT_DELAY = 3000;
const DB_OPERATION_TIMEOUT = 5000;
const MAX_RETRY_COUNT = 3;
const MAX_RECONNECT_ATTEMPTS = 3;

if (!isMainThread) {
  class TestClient {
    constructor(id) {
      this.id = id;
      this.socket = new net.Socket();
      this.messageCount = 0;
      this.startTime = null;
      this.reconnectAttempts = 0;
      this.connected = false;
      this.sequence = 0;
      this.expectedSequence = 1;
      this.buffer = Buffer.alloc(0);
      this.root = null;
      this.GamePacket = null;

      this.setupSocketHandlers();
      this.initializeProtobuf();
    }

    async initializeProtobuf() {
      try {
        // protobuf 초기화
        this.root = await protobuf.load(
          path.resolve(process.cwd(), 'src/protobufs/common/gamePacket.proto'),
        );
        this.GamePacket = this.root.lookupType('packet.GamePacket');
        console.log(`클라이언트 ${this.id} protobuf 초기화 완료`);
      } catch (err) {
        console.error(`클라이언트 ${this.id} protobuf 초기화 실패:`, err);
      }
    }

    setupSocketHandlers() {
      this.socket.on('connect', () => {
        console.log(`클라이언트 ${this.id} 연결됨`);
        this.connected = true;
        this.startGameFlow().catch((err) => {
          console.error(`클라이언트 ${this.id} 게임 플로우 오류:`, err.message);
        });
      });

      this.socket.on('error', (err) => {
        console.error(`클라이언트 ${this.id} 오류:`, err.message);
        this.connected = false;

        if (err.code === 'ECONNREFUSED' && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
          this.reconnectAttempts++;
          console.log(
            `클라이언트 ${this.id} 재연결 시도 ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`,
          );
          setTimeout(() => this.connect(), RECONNECT_DELAY);
        } else {
          parentPort.postMessage({
            id: this.id,
            error: err.message,
          });
        }
      });

      this.socket.on('close', () => {
        this.connected = false;
        console.log(`클라이언트 ${this.id} 연결 종료`);
      });

      this.socket.on('data', (data) => {
        this.buffer = Buffer.concat([this.buffer, data]);
        this.processBuffer();
      });
    }

    processBuffer() {
      const headerSize = config.packet.totalHeaderLength;

      while (this.buffer.length >= headerSize) {
        const payloadLength = this.buffer.readUInt32BE(headerSize - 4);
        const totalLength = headerSize + payloadLength;

        if (this.buffer.length >= totalLength) {
          const packet = this.buffer.slice(0, totalLength);
          this.buffer = this.buffer.slice(totalLength);

          this.handlePacket(packet);
        } else {
          break;
        }
      }
    }

    handlePacket(packet) {
      try {
        const packetType = packet.readUInt16BE(0);
        const versionLength = packet.readUInt8(2);
        const version = packet.slice(3, 3 + versionLength).toString('utf8');
        const sequence = packet.readUInt32BE(3 + versionLength);
        const payloadLength = packet.readUInt32BE(3 + versionLength + 4);
        const payload = packet.slice(3 + versionLength + 8, 3 + versionLength + 8 + payloadLength);

        if (this.GamePacket) {
          const decodedPayload = this.GamePacket.decode(payload);
          console.log(`클라이언트 ${this.id} 패킷 수신:`, {
            type: packetType,
            version,
            sequence,
            payloadType: Object.keys(decodedPayload)[0],
          });
        }
      } catch (err) {
        console.error(`클라이언트 ${this.id} 패킷 처리 오류:`, err);
      }
    }

    createPacket(packetType, payload) {
      if (!this.GamePacket) {
        throw new Error('Protobuf가 초기화되지 않았습니다');
      }

      const version = config.client.version;
      const versionBuffer = Buffer.from(version);

      // Protobuf로 페이로드 인코딩
      const message = this.GamePacket.create(payload);
      const payloadBuffer = this.GamePacket.encode(message).finish();

      const headerSize = 2 + 1 + versionBuffer.length + 4 + 4;
      const buffer = Buffer.alloc(headerSize + payloadBuffer.length);

      let offset = 0;
      // 1. 패킷 타입 (2 bytes)
      buffer.writeUInt16BE(packetType, offset);
      offset += 2;

      // 2. 버전 길이 (1 byte)
      buffer.writeUInt8(versionBuffer.length, offset);
      offset += 1;

      // 3. 버전
      versionBuffer.copy(buffer, offset);
      offset += versionBuffer.length;

      // 4. 시퀀스 (4 bytes)
      this.sequence++;
      buffer.writeUInt32BE(this.sequence, offset);
      offset += 4;

      // 5. 페이로드 길이 (4 bytes)
      buffer.writeUInt32BE(payloadBuffer.length, offset);
      offset += 4;

      // 6. 페이로드
      payloadBuffer.copy(buffer, offset);

      return buffer;
    }

    async startGameFlow() {
      try {
        if (!this.GamePacket) {
          console.log(`클라이언트 ${this.id} protobuf 초기화 대기 중...`);
          await new Promise((resolve) => setTimeout(resolve, 1000));
          if (!this.GamePacket) {
            throw new Error('Protobuf 초기화 실패');
          }
        }

        await this.sendRegisterRequest();
        await new Promise((resolve) => setTimeout(resolve, 2000));

        await this.sendLoginRequest();
        await new Promise((resolve) => setTimeout(resolve, 2000));

        await this.sendRoomRequest();
        await new Promise((resolve) => setTimeout(resolve, 2000));

        await this.sendGamePrepareRequest();
        await new Promise((resolve) => setTimeout(resolve, 2000));

        this.startPositionUpdates();
      } catch (err) {
        console.error(`클라이언트 ${this.id} 게임 플로우 오류:`, err.message);
        this.socket.end();
      }
    }

    async sendRegisterRequest() {
      return await this.retryOperation(async () => {
        const timestamp = Date.now();
        const registerPayload = {
          C2SRegisterRequest: {
            email: `test${this.id}_${timestamp}@test.com`,
            nickname: `Player${this.id}_${timestamp}`,
            password: 'testpass',
          },
        };

        console.log(
          `클라이언트 ${this.id} 회원가입 요청:`,
          registerPayload.C2SRegisterRequest.email,
        );
        this.socket.write(
          this.createPacket(config.packet.packetType.REGISTER_REQUEST, registerPayload),
        );

        // 회원가입 응답 대기
        await new Promise((resolve, reject) => {
          const timeout = setTimeout(() => {
            this.socket.removeListener('data', handler);
            reject(new Error('회원가입 응답 타임아웃'));
          }, DB_OPERATION_TIMEOUT);

          const handler = (data) => {
            try {
              const packet = this.parsePacket(data);
              if (packet && packet.type === config.packet.packetType.REGISTER_RESPONSE) {
                clearTimeout(timeout);
                this.socket.removeListener('data', handler);
                // 이메일과 닉네임 저장
                this.email = registerPayload.C2SRegisterRequest.email;
                this.nickname = registerPayload.C2SRegisterRequest.nickname;
                resolve();
              }
            } catch (err) {
              console.error(`클라이언트 ${this.id} 회원가입 응답 처리 오류:`, err);
            }
          };
          this.socket.on('data', handler);
        });
      }, '회원가입');
    }

    async sendLoginRequest() {
      return await this.retryOperation(async () => {
        const loginPayload = {
          C2SLoginRequest: {
            email: this.email,
            password: 'testpass',
          },
        };

        console.log(`클라이언트 ${this.id} 로그인 요청:`, loginPayload.C2SLoginRequest.email);
        this.socket.write(this.createPacket(config.packet.packetType.LOGIN_REQUEST, loginPayload));

        // 로그인 응답 대기
        await new Promise((resolve, reject) => {
          const timeout = setTimeout(() => {
            this.socket.removeListener('data', handler);
            reject(new Error('로그인 응답 타임아웃'));
          }, DB_OPERATION_TIMEOUT);

          const handler = (data) => {
            try {
              const packet = this.parsePacket(data);
              if (packet && packet.type === config.packet.packetType.LOGIN_RESPONSE) {
                clearTimeout(timeout);
                this.socket.removeListener('data', handler);
                resolve();
              }
            } catch (err) {
              console.error(`클라이언트 ${this.id} 로그인 응답 처리 오류:`, err);
            }
          };
          this.socket.on('data', handler);
        });
      }, '로그인');
    }

    async retryOperation(operation, operationName) {
      for (let attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++) {
        try {
          await operation();
          console.log(
            `클라이언트 ${this.id} ${operationName} 성공 (시도 ${attempt}/${MAX_RETRY_COUNT})`,
          );
          return;
        } catch (err) {
          console.error(
            `클라이언트 ${this.id} ${operationName} 실패 (시도 ${attempt}/${MAX_RETRY_COUNT}):`,
            err.message,
          );
          if (attempt < MAX_RETRY_COUNT) {
            const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // 지수 백오프
            console.log(`클라이언트 ${this.id} ${delay}ms 후 재시도...`);
            await new Promise((resolve) => setTimeout(resolve, delay));
          } else {
            throw new Error(`${operationName} 최대 재시도 횟수 초과`);
          }
        }
      }
    }

    parsePacket(data) {
      try {
        let offset = 0;

        // 1. 패킷 타입 (2 bytes)
        const packetType = data.readUInt16BE(offset);
        offset += 2;

        // 2. 버전 길이 (1 byte)
        const versionLength = data.readUInt8(offset);
        offset += 1;

        // 3. 버전
        const version = data.slice(offset, offset + versionLength).toString('utf8');
        offset += versionLength;

        // 4. 시퀀스 (4 bytes)
        const sequence = data.readUInt32BE(offset);
        offset += 4;

        // 5. 페이로드 길이 (4 bytes)
        const payloadLength = data.readUInt32BE(offset);
        offset += 4;

        // 6. 페이로드
        const payload = data.slice(offset, offset + payloadLength);

        if (this.GamePacket) {
          const decodedPayload = this.GamePacket.decode(payload);
          return {
            type: packetType,
            version,
            sequence,
            payload: decodedPayload,
          };
        }
      } catch (err) {
        console.error(`클라이언트 ${this.id} 패킷 파싱 오류:`, err);
      }
      return null;
    }

    async sendRoomRequest() {
      const isCreator = this.id % 2 === 0;
      const roomPayload = isCreator
        ? {
            createRoomRequest: {
              name: `Room${this.id}`,
              maxUserNum: 8,
            },
          }
        : {
            joinRandomRoomRequest: {},
          };

      const packetType = isCreator
        ? config.packet.packetType.CREATE_ROOM_REQUEST
        : config.packet.packetType.JOIN_RANDOM_ROOM_REQUEST;

      this.socket.write(this.createPacket(packetType, roomPayload));
    }

    async sendGamePrepareRequest() {
      const preparePayload = {
        gamePrepareRequest: {},
      };
      this.socket.write(
        this.createPacket(config.packet.packetType.GAME_PREPARE_REQUEST, preparePayload),
      );
    }

    startPositionUpdates() {
      this.startTime = Date.now();
      const interval = setInterval(() => {
        if (!this.connected || Date.now() - this.startTime >= TEST_DURATION) {
          clearInterval(interval);
          if (this.connected) {
            this.socket.end();
            parentPort.postMessage({
              id: this.id,
              messageCount: this.messageCount,
              duration: Date.now() - this.startTime,
            });
          }
          return;
        }

        const positionPayload = {
          positionUpdateRequest: {
            x: Math.random() * 100,
            y: Math.random() * 100,
          },
        };

        try {
          this.socket.write(
            this.createPacket(config.packet.packetType.POSITION_UPDATE_REQUEST, positionPayload),
          );
          this.messageCount++;
        } catch (err) {
          console.error(`클라이언트 ${this.id} 패킷 전송 오류:`, err.message);
        }
      }, 100);
    }

    connect() {
      this.socket.connect(PORT, HOST);
    }
  }

  // 클라이언트 시작
  const client = new TestClient(workerData.id);
  setTimeout(() => client.connect(), workerData.id * CONNECTION_DELAY);
} else {
  // 메인 스레드 코드
  console.log('서버 부하 테스트 시작');
  console.log(`동시 접속자 수: ${NUM_CLIENTS}`);
  console.log(`테스트 시간: ${TEST_DURATION / 1000}초`);
  console.log(`연결 지연 시간: ${CONNECTION_DELAY}ms`);

  const workers = new Array(NUM_CLIENTS);
  const results = new Map();
  let completedWorkers = 0;

  function checkTestCompletion() {
    completedWorkers++;
    if (completedWorkers === NUM_CLIENTS) {
      let totalMessages = 0;
      let errors = 0;
      let connectedClients = 0;

      results.forEach((result) => {
        if (result.error) {
          errors++;
        } else {
          totalMessages += result.messageCount;
          connectedClients++;
        }
      });

      console.log('\n테스트 결과:');
      console.log(`연결된 클라이언트: ${connectedClients}/${NUM_CLIENTS}`);
      console.log(`총 메시지 수: ${totalMessages}`);
      if (connectedClients > 0) {
        console.log(`클라이언트당 평균 메시지: ${(totalMessages / connectedClients).toFixed(2)}`);
        console.log(`초당 평균 메시지: ${(totalMessages / (TEST_DURATION / 1000)).toFixed(2)}`);
      }
      console.log(`에러 발생 클라이언트: ${errors}`);
      console.log(`성공률: ${((connectedClients / NUM_CLIENTS) * 100).toFixed(2)}%`);

      process.exit(0);
    }
  }

  let currentWorker = 0;

  function createNextWorker() {
    if (currentWorker >= NUM_CLIENTS) return;

    const worker = new Worker(new URL(import.meta.url), {
      workerData: { id: currentWorker },
    });

    workers[currentWorker] = worker;

    worker.on('message', (result) => {
      results.set(result.id, result);
      checkTestCompletion();
    });

    worker.on('error', (err) => {
      console.error(`워커 ${currentWorker} 오류:`, err);
      results.set(currentWorker, { error: err.message });
      checkTestCompletion();
    });

    worker.on('exit', (code) => {
      if (code !== 0) {
        console.error(`워커 ${currentWorker} 비정상 종료: ${code}`);
        if (!results.has(currentWorker)) {
          results.set(currentWorker, { error: `Worker exited with code ${code}` });
          checkTestCompletion();
        }
      }
    });

    currentWorker++;
    if (currentWorker < NUM_CLIENTS) {
      setTimeout(createNextWorker, CONNECTION_DELAY);
    }
  }

  createNextWorker();
}

 

일단 패킷을 주고 받는 단순 유닛 테스트랑 서버가 얼마나 버티는지에 대한 부하 테스트 둘을 작성했다.

 

부하 테스트는 지금 로그인 중 서버가 뻗는 현상이 있어서 아직 보류고

추가로 경계 테스트?? 라는걸 더 작성 해볼 예정이다.