← 문제로

27. 좌표·각도 양자화(quantization) 직렬화의 경계 처리 결함 (C#)

난이도 하
내 리뷰 · C#
해설 · C#

해설 — 좌표·각도 양자화(quantization) 직렬화의 경계 처리 결함 (C#)

난이도: 하

요약

좌표/각도를 16비트로 양자화하면서 입력을 유효 범위로 클램프/정규화하지 않는다. 그래서 월드 경계를 조금만 벗어난 좌표가 정수 변환에서 망가져 정반대 위치로 순간이동하고, 정규화되지 않은 각도는 변환 결과가 미정의/불특정 값이 된다.

문제점

  • [경계 미클램프] (A)(ushort)(x / WorldSize * 65535f)x[0, WorldSize] 안에 있다고 가정한다. 그런데 넉백/추락/부동소수 오차로 x가 음수이거나 WorldSize를 조금 넘으면, 스케일 결과가 [0, 65535] 밖이 된다.
    • C#에서 범위를 벗어난 float→ushort 변환은 **unchecked 컨텍스트에서 예외 없이 "불특정 결과"**다(checked면 OverflowException). .NET에서 깔끔하게 modulo wrap 되지도 않아, 보통 0이나 잘린 쓰레기 값이 나온다.
    • 증상: x = WorldSize + 3 처럼 3 유닛만 넘쳐도 복원 좌표가 맵 반대편/원점 근처로 튄다(텔레포트 글리치). 검증 로직이 이 좌표를 신뢰하면 벽 통과·속도핵 오탐/미탐으로 번진다.
  • [각도 미정규화] (B)(ushort)(radians / 2π * 65535f)radians ∈ [0, 2π) 를 가정한다. 누적 회전으로 radians가 음수거나 2π를 넘으면 스케일 결과가 16비트 범위를 벗어나 또 불특정 변환이 된다.
    • 정수 wrap이 우연히 "각도 modulo 2π"와 비슷해 보일 수 있으나, 이는 구현이 wrap 한다는 보장이 없는 동작에 기댄 것이라 플랫폼/런타임이 바뀌면 깨진다.
  • 공통 근본원인: 직렬화 경계에서 도메인 불변식(좌표 범위·각도 정규화)을 강제하지 않음. 양자화는 "정해진 범위를 N비트에 매핑"하는 연산인데 범위를 벗어난 입력의 처리를 정의하지 않았다.

수정안

양자화 전에 입력을 도메인으로 클램프/정규화하고, 정수 도메인에서 한 번 더 클램프한다.

public static QuantPos QuantizePos(float x, float y, float z)
{
    return new QuantPos {
        X = QuantizeAxis(x), Y = QuantizeAxis(y), Z = QuantizeAxis(z)
    };
}

private static ushort QuantizeAxis(float v)
{
    float c = Math.Clamp(v, 0f, WorldSize);           // 도메인 클램프
    int q = (int)MathF.Round(c / WorldSize * 65535f); // 반올림으로 양자화 오차↓
    return (ushort)Math.Clamp(q, 0, 65535);           // 정수 도메인 클램프
}

public static ushort QuantizeAngle(float radians)
{
    float a = radians % TwoPi;                        // 정규화
    if (a < 0) a += TwoPi;                             // [0, 2π)
    int q = (int)MathF.Round(a / TwoPi * 65535f);
    return (ushort)Math.Clamp(q, 0, 65535);
}
  • 클램프할지 거부(reject)할지는 정책이다. 경계 살짝 초과(부동소수 오차)는 클램프, 명백히 비정상(NaN/Inf/말도 안 되는 값)은 패킷 거부/로깅이 안전하다. float.IsFinite(v) 선검사를 권장.

더 나은 설계

  • 양자화기를 범위 명시 타입으로: RangeQuantizer(min, max, bits) 가 클램프·반올림·역양자화를 캡슐화. 좌표/체력/속도 등 모든 양자화가 같은 안전 경로를 타게 한다.
    • 트레이드오프: 추상화 비용은 작지만, 축마다 다른 범위(예: 높이 z는 다른 스케일)를 쓰려면 파라미터 관리가 필요.
  • 각도는 부호 없는 회전 단위로 다루면(예: ushort 자체가 0..65535 = 0..2π) 경계가 자연히 wrap 되어 정규화가 단순해진다. 단 "정수 wrap에 의존"이 아니라 "도메인을 0..2π로 정의"라는 의도를 코드로 드러내야 한다.

면접 포인트

  • "양자화는 손실 압축"이다. 핵심 파라미터는 범위(min/max)·비트수·반올림 방식이고, 범위를 벗어난 입력의 처리(clamp/reject)를 반드시 정의해야 한다.
  • float→정수 변환의 함정: 범위 밖 변환은 C#에서 unchecked면 불특정 결과(예외 없음), C++에선 미정의 동작. "우연히 동작"에 기대면 안 된다.