게임패드 핫플러그 입력 장치 재할당 오류, 자동 재현 테스트 시나리오

게임패드 핫플러그에서 발생하는 입력 장치 재할당 오류를 자동화 테스트로 재현하는 방법을 정리합니다. Unity Input System의 이벤트 순서, 검증 조건, 로그 설계와 회귀 테스트를 통해 잘못된 플레이어 매핑과 크래시를 빠르게 찾습니다.

✓ 복사 완료!
챕터자동화 테스트 & 회귀 검증
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
게임패드 핫플러그 입력 장치 재할당 오류, 자동 재현 테스트 시나리오

핵심 답변: 게임패드 핫플러그 오류는 장치가 제거된 뒤 새 deviceId로 다시 등록되는 과정에서 기존 플레이어 매핑과 입력 액션 바인딩이 분리될 때 발생합니다. 물리적인 USB 분리 대신 테스트 더블로 Removed → Added → Reassigned → InputSystem.Update() 순서를 주입하고 재연결 전후의 플레이어 슬롯·장치 식별자·액션 상태를 단계별로 검증하면 CI에서 반복 재현할 수 있습니다.

게임패드 핫플러그 입력 장치 재할당 오류는 왜 발생하는가?

핫플러그는 단순히 게임패드가 사라졌다가 나타나는 기능이 아닙니다. 운영체제와 엔진 입장에서는 기존 장치가 제거되고 새로운 장치 인스턴스가 등록되는 이벤트에 가깝습니다.

특히 deviceId는 런타임 장치 인스턴스를 식별하는 값이므로 재연결 후 바뀔 수 있습니다. 이를 플레이어의 영구 식별자로 사용하면 다음과 같은 문제가 생깁니다.

구분잘못된 구현안정적인 구현
장치 식별런타임 deviceId만 저장하드웨어 식별 정보와 정책 기반 슬롯 사용
제거 이벤트장치 매핑을 즉시 삭제플레이어 슬롯을 유지하고 장치만 일시 해제
재연결 이벤트먼저 들어온 장치를 빈 슬롯에 배정기존 물리 장치 키와 연결
입력 액션전역 액션 맵에 의존InputUser 또는 플레이어별 액션 맵 사용
검증 시점최종 화면에서 한 번 확인각 이벤트와 프레임 경계마다 확인

하드웨어 식별 정보는 컨트롤러의 시리얼 번호, vendorId, productId, 제품명 등을 조합할 수 있습니다. 다만 시리얼 번호가 없는 장치에서 제품명만 사용하면 동일 모델 두 대를 구분할 수 없으므로 로컬 슬롯 정책이나 사용자의 재선택 절차를 함께 정의해야 합니다.

자동 재현 테스트는 어떻게 설계하는가?

테스트의 핵심은 실제 장치를 뽑는 행위가 아니라 장치 생명주기와 입력 매핑 정책을 분리하는 것입니다. 물리 장치에 의존하면 운영체제 드라이버, USB 허브, 테스트 머신의 포트 순서에 따라 결과가 달라집니다.

테스트 시나리오는 다음 네 계층으로 나누면 원인을 빠르게 좁힐 수 있습니다.

  1. 장치 생명주기: Added, Removed, Reconnected 이벤트
  2. 플레이어 페어링: 어느 플레이어가 어떤 장치를 소유하는지
  3. 액션 바인딩: 이동·확인·취소 같은 입력 액션이 올바른 장치에 연결되는지
  4. 게임 상태: 일시정지, 메뉴 포커스, 씬 전환, 네트워크 세션 상태가 유지되는지

기본 재현 순서

단계주입 이벤트기대 결과오류 신호
1Pad-A와 Pad-B 등록플레이어 1과 2에 각각 배정두 플레이어가 한 장치를 공유
2Pad-A 제거플레이어 1 슬롯은 유지되고 입력만 비활성화플레이어 1이 즉시 플레이어 2로 변경
3Pad-C 등록정책에 따라 대기 슬롯 또는 새 플레이어로 처리Pad-C가 플레이어 1을 가로챔
4Pad-A 재연결기존 플레이어 1에 복귀먼저 등록된 빈 슬롯으로 이동
5입력 이벤트 주입플레이어 1 액션만 실행플레이어 2 캐릭터가 움직이거나 크래시 발생

Unity Input System에서 핫플러그 이벤트를 어떻게 주입할까?

아래 코드는 Unity Input System의 테스트 환경에서 사용할 수 있는 개념 검증용 예시입니다. assignment는 프로젝트의 플레이어 배정 서비스 또는 InputUser 래퍼를 의미합니다. 핵심은 deviceId와 물리 장치 키를 분리해 검증하는 것입니다.

using NUnit.Framework;
using UnityEngine.InputSystem;

public class GamepadHotplugTests
{
    [Test]
    public void Reconnect_keeps_original_player_slot()
    {
        var padA = InputSystem.AddDevice<Gamepad>("Pad-A");
        var padB = InputSystem.AddDevice<Gamepad>("Pad-B");

        assignment.Assign(playerId: 1, device: padA, stableKey: "usb-a");
        assignment.Assign(playerId: 2, device: padB, stableKey: "usb-b");
        InputSystem.Update();

        var oldRuntimeId = padA.deviceId;

        InputSystem.RemoveDevice(padA);
        assignment.OnDeviceRemoved(stableKey: "usb-a");
        InputSystem.Update();

        var padAReconnected = InputSystem.AddDevice<Gamepad>("Pad-A");
        assignment.OnDeviceAdded(
            device: padAReconnected,
            stableKey: "usb-a");
        InputSystem.Update();

        Assert.That(
            padAReconnected.deviceId,
            Is.Not.EqualTo(oldRuntimeId));
        Assert.That(
            assignment.PlayerFor(padAReconnected),
            Is.EqualTo(1));
        Assert.That(
            assignment.PlayerFor(padB),
            Is.EqualTo(2));
    }
}

실제 프로젝트에서는 InputSystem.onDeviceChange를 구독하는 코드와 플레이어 페어링 코드를 테스트 대상에 포함해야 합니다. InputUser를 사용한다면 장치 제거 시 InputUser 자체를 폐기할지, 페어링만 해제할지 정책을 먼저 고정해야 합니다. 두 정책이 섞이면 재연결 시 액션 맵이 중복 활성화될 수 있습니다.

테스트 픽스처가 지원하는 경우에는 테스트 전후에 장치 목록과 전역 이벤트 구독을 초기화해야 합니다. 이전 테스트에서 남은 InputUser, 액션 맵, 콜백이 다음 테스트의 결과에 영향을 주면 재현성이 사라집니다.

장치 재할당 오류를 단계별로 자동 재현하는 방법

1단계. 안정적인 테스트 식별자를 정의한다

테스트 장치에는 다음과 같은 메타데이터를 부여합니다.

public record DeviceIdentity(
    string StableKey,
    int VendorId,
    int ProductId,
    string ProductName);

StableKey는 실제 장치의 시리얼 번호 또는 플랫폼별 하드웨어 키를 추상화한 값입니다. 테스트에서는 usb-a, usb-b처럼 고정된 값을 사용합니다. 이렇게 하면 재연결된 객체의 런타임 deviceId가 달라도 같은 물리 장치로 판단할 수 있습니다.

2단계. 이벤트 순서를 의도적으로 흔든다

가장 자주 발생하는 오류는 이벤트 순서에 대한 가정입니다. 다음 변형을 별도 테스트 케이스로 둡니다.

  • 제거 이벤트 직후 같은 프레임에 재연결
  • 제거 후 한 프레임을 건너뛴 뒤 재연결
  • Pad-A 제거 후 Pad-C가 먼저 등록
  • 두 장치가 동시에 제거된 뒤 등록 순서가 역전
  • 재연결 직후 씬 전환
  • 메뉴 입력 액션 맵이 활성화된 상태에서 재연결
  • 일시정지 상태에서 재연결 후 게임 복귀

실제 시간 대기를 넣기보다는 가짜 시계나 명시적인 InputSystem.Update() 호출로 프레임 경계를 제어하는 편이 좋습니다. 디바운스 정책이 250ms라면 테스트에서도 가짜 시계를 249ms, 250ms, 251ms로 이동해 경계값을 검증합니다.

3단계. 입력 이벤트를 재연결 장치에 주입한다

재연결이 성공했는지는 장치 목록만으로 판단하면 안 됩니다. 재연결된 게임패드의 버튼 이벤트가 올바른 플레이어의 액션으로 전달되는지 확인해야 합니다.

[Test]
public void Reconnected_pad_does_not_trigger_other_player()
{
    var padA = InputSystem.AddDevice<Gamepad>("Pad-A");
    var padB = InputSystem.AddDevice<Gamepad>("Pad-B");

    assignment.Assign(1, padA, "usb-a");
    assignment.Assign(2, padB, "usb-b");
    InputSystem.Update();

    InputSystem.RemoveDevice(padA);
    assignment.OnDeviceRemoved("usb-a");
    InputSystem.Update();

    var reconnectedPadA = InputSystem.AddDevice<Gamepad>("Pad-A");
    assignment.OnDeviceAdded(reconnectedPadA, "usb-a");
    InputSystem.Update();

    using (StateEvent.From(reconnectedPadA, out var eventPtr))
    {
        GamepadState state = default;
        state = state.WithButton(GamepadButton.South, true);
        InputSystem.QueueStateEvent(reconnectedPadA, state);
    }

    InputSystem.Update();

    Assert.That(actionSpy.WasTriggered("Player1/Confirm"), Is.True);
    Assert.That(actionSpy.WasTriggered("Player2/Confirm"), Is.False);
}

프로젝트 버전에 따라 테스트 이벤트 생성 API가 다를 수 있으므로 테스트 코드에서는 QueueStateEvent 호출을 별도 어댑터로 감싸는 것이 안전합니다. 중요한 검증 대상은 버튼을 눌렀다는 사실이 아니라 어느 플레이어의 액션이 실행됐는지입니다.

회귀 테스트에서 무엇을 검증해야 하는가?

최종 매핑만 검증하면 중간 상태의 오류를 놓칠 수 있습니다. 다음 불변 조건을 이벤트 단계마다 기록하고 확인합니다.

  • 한 장치는 동시에 두 플레이어에게 배정되지 않는다.
  • 장치가 제거되어도 플레이어 슬롯 번호는 정책에 따라 유지된다.
  • 재연결된 장치의 런타임 deviceId가 달라도 안정적인 장치 키는 유지된다.
  • 플레이어별 액션 맵은 한 번만 활성화된다.
  • 제거된 장치에서 들어온 입력은 게임 로직을 실행하지 않는다.
  • 재연결 중인 장치를 참조하는 콜백은 예외를 발생시키지 않는다.
  • 씬 전환 후에도 이전 장치의 이벤트 구독이 중복되지 않는다.

로그에는 최소한 다음 필드를 포함합니다.

로그 필드목적
frame이벤트 순서와 프레임 경계 확인
eventTypeAdded, Removed, Reassigned 구분
runtimeDeviceId인스턴스 변경 확인
stableKey동일 물리 장치 여부 확인
playerId배정 대상 확인
actionMap활성 액션 맵 확인
pairedDeviceCount중복 페어링 확인

CI에서는 같은 시나리오를 여러 번 반복해 장치 등록 순서와 지연 시간을 바꿔보는 방식도 유용합니다. 다만 무작위성만 추가하면 실패 원인을 재현하기 어려우므로 난수 시드와 전체 이벤트 시퀀스를 반드시 로그에 남겨야 합니다.

패치 전에 확인할 수 있는 실패 판정 기준

버그 수정 패치의 통과 조건은 다음처럼 구체적으로 작성합니다.

  1. Pad-A를 제거하고 Pad-C를 등록해도 플레이어 1의 슬롯이 임의로 변경되지 않는다.
  2. Pad-A가 재연결되면 안정적인 장치 키에 따라 플레이어 1로 복귀한다.
  3. 재연결 전후의 플레이어 2 입력 결과가 변하지 않는다.
  4. 100회 연속 핫플러그 시나리오에서 중복 액션 콜백과 예외가 발생하지 않는다.
  5. 장치 제거 직후 메뉴, 일시정지, 씬 전환을 수행해도 크래시가 발생하지 않는다.

이 기준은 기능 테스트와 크래시 분석을 연결해 줍니다. 실패 시에는 마지막으로 정상 처리된 이벤트와 최초로 잘못된 매핑이 관찰된 이벤트를 함께 출력하면 원인 추적 시간이 크게 줄어듭니다.

자주 묻는 질문 (FAQ)

물리적으로 게임패드를 뽑았다가 꽂는 테스트가 더 정확하지 않은가?

하드웨어 통합 테스트에는 필요하지만 일반적인 회귀 테스트에는 부적합합니다. 운영체제와 드라이버 상태에 따라 결과가 달라지므로 자동화 테스트는 가상 장치와 이벤트 주입으로 구성하고 실제 장치 테스트는 별도 테스트 단계로 분리하는 편이 안정적입니다.

deviceId를 플레이어 식별자로 사용하면 안 되는가?

재연결 후 값이 바뀔 수 있으므로 영구 식별자로 사용하면 안 됩니다. 런타임 객체 추적에는 사용할 수 있지만 플레이어 슬롯 복원에는 시리얼 번호, 플랫폼 키, 사용자 확인 절차 같은 안정적인 식별 정책이 필요합니다.

장치 재연결 후 입력이 두 번 실행되는 이유는 무엇인가?

기존 액션 맵이나 onDeviceChange 콜백이 해제되지 않은 상태에서 새 장치용 구독을 추가했을 가능성이 큽니다. 테스트에서 콜백 호출 횟수, 활성 액션 맵 수, 플레이어별 페어링 장치 수를 함께 검증해야 합니다.

정리

게임패드 핫플러그 입력 장치 재할당 오류는 장치 제거와 재등록을 하나의 단순한 연결 상태로 처리할 때 쉽게 발생합니다. 자동 재현 테스트에서는 런타임 deviceId와 안정적인 물리 장치 키를 분리하고 이벤트 순서를 제어하며 재연결 뒤 실제 플레이어 액션이 올바르게 실행되는지 확인해야 합니다. 이 구조를 회귀 테스트에 포함하면 패치 이후의 잘못된 입력 전달, 중복 콜백, 씬 전환 크래시까지 함께 탐지할 수 있습니다.