API 및 실시간 통신 테스트

API 테스트

API 테스트는 서버가 클라이언트의 요청을 올바르게 처리하고, 명세에 정의된 응답을 반환하는지 검증하는 과정이다.

게임 플랫폼에서는 로그인, 회원 정보 조회, 게임 결과 저장, 포인트 정산, 매칭 요청 등 다양한 기능이 API를 통해 처리된다. API의 오류는 단순한 응답 실패에 그치지 않고 데이터 불일치, 보상 중복 지급, 사용자 인증 실패 등의 문제로 이어질 수 있다.

API 테스트의 핵심 검증 항목

  • HTTP 상태 코드: 요청 성공, 인증 실패, 권한 부족, 잘못된 입력, 서버 오류에 적절한 상태 코드를 반환하는지 확인한다.
  • 응답 데이터: 응답 JSON의 필드, 데이터 타입, 필수 값, 중첩 구조가 기대한 형태인지 검증한다.
  • 입력값 검증: 필수 필드 누락, 잘못된 데이터 타입, 허용 범위를 벗어난 값에 적절하게 대응하는지 확인한다.
  • 예외 처리: 존재하지 않는 리소스, 중복 요청, 데이터베이스 오류 등의 상황에서 일관된 오류 응답을 반환하는지 확인한다.
  • 데이터 정합성: API 호출 이후 데이터베이스의 상태가 의도한 대로 변경되었는지 검증한다.
  • 멱등성: 동일한 요청이 반복되더라도 중복 결제나 보상 중복 지급과 같은 문제가 발생하지 않는지 확인한다.

API 테스트에서는 정상적인 요청뿐 아니라 비정상적인 입력과 실패 상황을 함께 검증해야 한다.

Jest와 Supertest를 활용한 API 테스트

Node.js 환경에서는 Jest로 테스트를 실행하고, Supertest로 HTTP 요청을 보내 API를 검증할 수 있다.

import request from 'supertest';
import { app } from '../app.js';

describe('GET /api/users/me', () => {
  it('인증된 사용자의 정보를 반환한다', async () => {
    const response = await request(app)
      .get('/api/users/me')
      .set('Authorization', 'Bearer test-token');

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('id');
    expect(response.body).toHaveProperty('nickname');
  });
});

위 예제는 인증된 사용자의 정보를 조회하는 API를 테스트한다. 다만 test-token이 실제 인증 미들웨어에서 유효하게 처리되도록 테스트 환경을 구성해야 한다.

테스트를 실행할 때는 외부 운영 서버에 의존하지 않고, 테스트용 데이터베이스와 격리된 환경을 사용하는 것이 좋다. 테스트마다 필요한 데이터를 준비하고 실행 후 정리하면 테스트 간 간섭을 줄일 수 있다.

API 계약 테스트

API 계약 테스트는 클라이언트와 서버가 약속한 인터페이스를 양쪽에서 동일하게 준수하는지 검증하는 테스트다.

API가 정상적으로 응답하더라도 응답 필드의 이름이나 데이터 타입이 변경되면 클라이언트에서 오류가 발생할 수 있다. 계약 테스트는 이러한 호환성 문제를 조기에 발견하는 데 목적이 있다.

API 계약의 구성 요소

  • 요청 계약: URL, HTTP 메서드, 헤더, 쿼리 매개변수, 요청 본문 구조
  • 응답 계약: 상태 코드, 응답 필드, 데이터 타입, 필수 필드, 오류 응답 구조
  • 인증 계약: 인증 토큰 전달 방식과 인증 실패 시의 응답
  • 오류 계약: 오류 코드, 메시지, 필드별 검증 오류 형식
  • 호환성 계약: 기존 클라이언트가 서버 변경 이후에도 정상적으로 동작하기 위한 조건

예를 들어 게임 결과 조회 API가 다음과 같은 응답을 반환한다고 가정한다.

{
  "matchId": "match-abc",
  "result": "WIN",
  "rewardPoints": 100
}

서버에서 rewardPoints를 rewardPoint로 변경하면 응답 자체는 정상적으로 반환되더라도 기존 클라이언트는 보상 포인트를 읽지 못할 수 있다.

계약 테스트는 필수 필드의 누락, 이름 변경, 데이터 타입 변경 등을 검증하여 이러한 문제를 방지한다.

OpenAPI를 활용한 계약 검증

OpenAPI는 HTTP API의 엔드포인트, 요청 및 응답 구조, 상태 코드 등을 명세하는 표준이다.

OpenAPI 명세를 기준으로 다음과 같은 검증을 수행할 수 있다.

  • 실제 응답이 명세의 스키마를 준수하는지 확인한다.
  • 필수 필드가 누락되지 않았는지 검사한다.
  • 정의되지 않은 상태 코드나 잘못된 응답 형식을 탐지한다.
  • 명세와 실제 구현이 달라진 부분을 확인한다.
  • 클라이언트 SDK 생성에 사용하는 계약이 서버 구현과 일치하는지 검증한다.

계약 테스트는 단순히 JSON의 형태를 검사하는 것만으로 끝나지 않는다. 클라이언트가 실제로 의존하는 필드와 동작을 중심으로 호환성을 검증해야 한다.

또한 OpenAPI 명세가 존재한다고 해서 계약 테스트가 자동으로 수행되는 것은 아니다. 명세를 기반으로 검증 도구와 테스트 절차를 구성해야 한다.

WebSocket 테스트

WebSocket은 클라이언트와 서버가 연결을 유지하면서 양방향으로 메시지를 주고받는 통신 방식이다.

일반적인 HTTP API와 달리 연결 수립 이후에도 메시지가 지속적으로 전달되므로 연결 상태, 메시지 순서, 동시 접속, 예외 상황 등을 별도로 검증해야 한다.

게임 플랫폼에서는 실시간 채팅, 매칭 상태 변경, 게임 이벤트 전달, 온라인 상태 동기화 등에 WebSocket을 활용할 수 있다.

WebSocket 테스트의 핵심 검증 항목

  • 연결 수립: 올바른 요청으로 연결이 성공하고 잘못된 요청은 거부되는지 확인한다.
  • 메시지 형식: 메시지 타입, 필수 필드, 데이터 타입이 프로토콜에 정의된 규칙을 준수하는지 검증한다.
  • 메시지 전달: 요청한 메시지가 올바른 사용자나 채널에 전달되는지 확인한다.
  • 메시지 순서: 순서가 중요한 이벤트가 의도한 순서로 처리되는지 검증한다.
  • 동시 접속: 여러 클라이언트가 동시에 연결하고 메시지를 주고받을 때 문제가 없는지 확인한다.
  • 연결 종료: 정상 종료와 비정상 종료가 모두 적절하게 처리되는지 확인한다.
  • 부하 및 자원 사용량: 동시 연결 수가 증가할 때 메모리, CPU, 네트워크 사용량과 지연 시간이 허용 범위에 있는지 측정한다.

WebSocket 테스트에서는 메시지가 전달되었다는 사실만으로 성공을 판단해서는 안 된다. 메시지가 올바른 대상에게 전달되었는지, 중복으로 처리되지 않았는지, 연결이 종료된 사용자에게 불필요한 메시지를 계속 보내지 않는지도 검증해야 한다.

WebSocket 메시지 테스트 예시

import WebSocket from 'ws';

test('연결된 클라이언트가 서버 메시지를 수신한다', async () => {
  const socket = new WebSocket('ws://localhost:3000/ws');

  try {
    await new Promise((resolve, reject) => {
      socket.once('open', resolve);
      socket.once('error', reject);
    });

    const messagePromise = new Promise((resolve, reject) => {
      socket.once('message', (data) => {
        resolve(JSON.parse(data.toString()));
      });

      socket.once('error', reject);
    });

    socket.send(JSON.stringify({
      type: 'PING'
    }));

    const message = await messagePromise;

    expect(message.type).toBe('PONG');
  } finally {
    socket.close();
  }
});

이 예제는 로컬 WebSocket 서버에 연결한 뒤 PING 메시지를 보내고 PONG 응답을 확인한다.

실제 테스트에서는 서버가 메시지를 처리하기 전에 타임아웃이 발생하지 않도록 제한 시간을 설정하고, 테스트 종료 시 연결과 서버 자원을 정리해야 한다. 서버가 요청에 대한 응답을 보장하지 않는 프로토콜이라면 실제 프로토콜에 맞는 이벤트를 검증해야 한다.

인증 및 권한 테스트

인증 및 권한 테스트는 사용자가 누구인지 확인하는 과정과 해당 사용자가 특정 작업을 수행할 권한이 있는지 확인하는 과정을 검증한다.

인증(Authentication)은 사용자의 신원을 확인하는 것이고, 인가(Authorization)는 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는지 판단하는 것이다.

두 개념을 혼동하면 로그인한 사용자라는 이유만으로 다른 사용자의 데이터에 접근할 수 있는 보안 취약점이 발생할 수 있다.

인증 테스트

  • 인증 정보가 없는 요청이 보호된 API에서 거부되는지 확인한다.
  • 유효하지 않거나 변조된 토큰이 거부되는지 검증한다.
  • 만료된 토큰이 허용되지 않는지 확인한다.
  • 로그아웃 또는 토큰 폐기 이후 해당 인증 정보가 정책에 맞게 무효화되는지 확인한다.
  • 인증 실패 시 민감한 정보가 응답에 포함되지 않는지 검증한다.

권한 테스트

  • 일반 사용자가 관리자 전용 기능을 실행할 수 없는지 확인한다.
  • 다른 사용자의 프로필이나 게임 기록을 임의로 조회하거나 수정할 수 없는지 검증한다.
  • 요청 본문에 사용자 ID를 임의로 지정해도 서버가 권한 검증을 우회하지 않는지 확인한다.
  • 역할이 변경되거나 권한이 회수되었을 때 변경된 정책이 적용되는지 검증한다.
  • WebSocket 연결에서도 메시지를 보낼 때마다 필요한 권한을 검증하는지 확인한다.

특히 IDOR(Insecure Direct Object Reference)는 사용자가 요청에 포함된 리소스 식별자를 변경해 다른 사용자의 데이터에 접근하는 취약점이다.

예를 들어 /api/users/user-abc를 요청할 수 있는 사용자가 경로의 사용자 ID를 다른 값으로 바꾸어도 서버는 해당 리소스에 접근할 권한이 있는지 검증해야 한다. 단순히 로그인 여부만 확인하는 방식으로는 이러한 공격을 방지할 수 없다.

게임 플랫폼에서는 포인트 차감, 아이템 지급, 매치 결과 수정 등 중요한 작업에 대해 서버 측 권한 검증과 데이터 무결성 검사를 함께 수행해야 한다.

재접속 및 타임아웃 테스트

실시간 통신에서는 네트워크 단절, 서버 지연, 클라이언트 종료, 일시적인 통신 장애가 발생할 수 있다. 재접속 및 타임아웃 테스트는 이러한 상황에서 시스템이 정상적으로 복구되는지 검증한다.

연결이 끊기지 않는 상황만 테스트하면 실제 운영 환경에서 발생하는 연결 유실과 중복 처리 문제를 발견하기 어렵다.

재접속 테스트

  • 네트워크가 일시적으로 끊겼을 때 클라이언트가 재접속을 시도하는지 확인한다.
  • 서버가 재시작된 후 클라이언트가 정상적으로 연결을 복구하는지 검증한다.
  • 재접속 이후 인증 상태와 세션 상태가 올바르게 복원되는지 확인한다.
  • 연결이 끊긴 동안 발생한 이벤트를 다시 전달할 필요가 있는지 확인한다.
  • 재접속 과정에서 동일한 이벤트나 보상이 중복 처리되지 않는지 검증한다.
  • 재접속이 반복될 때 서버의 연결 수와 자원 사용량이 정상적으로 관리되는지 확인한다.

재접속은 연결을 다시 만드는 것만으로 완성되지 않는다. 연결 단절 시점의 상태를 어떻게 복구할 것인지가 중요하다.

예를 들어 매칭 대기 중 연결이 끊겼다면 기존 대기열에 사용자를 유지할지, 매칭을 취소할지 정책이 필요하다. 게임 진행 중 연결이 끊겼다면 기존 게임 세션으로 복귀할 수 있는지, 복귀 가능 시간은 얼마인지 등을 정의해야 한다.

타임아웃 테스트

타임아웃은 일정 시간 안에 응답이나 이벤트가 도착하지 않을 경우 해당 작업을 실패로 처리하거나 연결 상태를 재평가하는 기준이다.

  • 요청 타임아웃: API 요청이 제한 시간 내에 응답하지 않을 때 클라이언트가 적절히 처리하는지 확인한다.
  • 연결 타임아웃: WebSocket 연결이 제한 시간 안에 수립되지 않을 때 자원을 정리하는지 검증한다.
  • 응답 타임아웃: 요청에 대한 응답이 오지 않을 때 작업이 무한정 대기하지 않는지 확인한다.
  • 유휴 연결 타임아웃: 장시간 메시지가 없는 연결이 정책에 따라 정리되는지 검증한다.
  • Heartbeat 타임아웃: Ping/Pong 또는 별도의 하트비트 신호가 일정 시간 동안 확인되지 않을 때 연결 상태를 판별하는지 확인한다.

타임아웃이 발생했다고 해서 서버에서 작업이 반드시 취소되었다는 의미는 아니다. 클라이언트가 응답을 받지 못했더라도 서버에서는 이미 데이터베이스 변경이나 보상 지급을 완료했을 수 있다.

따라서 타임아웃 이후 재시도하는 작업은 멱등성 키, 고유 제약 조건, 작업 상태 조회 등을 활용해 중복 실행을 방지해야 한다.

지수 백오프를 활용한 재접속

재접속이 실패할 때마다 즉시 연결을 시도하면 장애가 발생한 서버에 요청이 집중될 수 있다. 이를 재접속 폭주라고 한다.

지수 백오프는 재시도 사이의 대기 시간을 점진적으로 늘려 서버에 가해지는 부하를 줄이는 방식이다.

function getReconnectDelay(attempt) {
  const baseDelay = 500;
  const maxDelay = 30000;

  const exponentialDelay = Math.min(
    baseDelay * 2 ** attempt,
    maxDelay
  );

  const jitter = Math.random() * 500;

  return exponentialDelay + jitter;
}

위 코드는 재접속 시도 횟수에 따라 대기 시간을 늘리고 무작위 지연을 추가한다. 무작위 지연은 다수의 클라이언트가 동시에 재접속하는 현상을 완화하는 데 도움이 된다.

실제 구현에서는 최대 재시도 횟수, 전체 재접속 제한 시간, 인증 오류와 같은 재시도 불가능한 오류의 구분도 고려해야 한다.

테스트 자동화와 운영 환경 검증

API 및 실시간 통신 테스트는 단위 테스트만으로 충분하지 않다. 각 테스트가 검증하는 범위가 다르기 때문에 여러 수준의 테스트를 조합해야 한다.

  • 단위 테스트: 입력값 검증, 권한 판단, 재시도 정책 등 개별 로직을 검증한다.
  • 통합 테스트: API와 데이터베이스, 인증 미들웨어, 메시지 브로커 사이의 연동을 검증한다.
  • 계약 테스트: 실제 요청과 응답이 API 명세 및 클라이언트와의 계약을 준수하는지 검증한다.
  • 시스템 테스트: 서버, 데이터베이스, WebSocket 연결을 포함한 전체 흐름을 검증한다.
  • 부하 테스트: 동시 사용자와 메시지 처리량이 증가할 때 응답 시간, 처리량, 오류율을 측정한다.
  • 장애 주입 테스트: 네트워크 단절, 서버 재시작, 데이터베이스 지연 등의 상황에서 복구 동작을 검증한다.

테스트 자동화에서는 테스트 데이터 격리, 실행 순서에 대한 독립성, 타임아웃 설정, 자원 정리, 로그 수집을 함께 고려해야 한다.

또한 테스트 환경에서 성공한 기능이라도 운영 환경의 네트워크 지연, 동시성, 연결 수, 외부 서비스 장애에 따라 다른 결과가 나타날 수 있다. 따라서 실제 운영 환경을 반영한 부하 테스트와 장애 복구 검증이 필요하다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다