덧셈 뒤 8비트 결과가 작아졌다고 모두 같은 오버플로는 아닙니다. unsigned 값에서는 최상위 자리의 carry-out을 보고, signed 2의 보수에서는 같은 부호의 두 입력이 반대 부호 결과를 만드는지 봅니다. 두 신호를 하나로 취급하면 정상 연산을 오류로 막거나 실제 오류를 놓칩니다.
2026년 10월 5일 Node.js 24.19.0에서 1비트 전가산기를 여덟 번 연결한 함수를 만들고 0~255의 모든 순서쌍 65,536개를 실행했습니다. 모듈러 합, 최종 캐리, signed 범위를 각각 독립 계산해 196,611개 조건이 통과했습니다.
한 비트의 합과 올림을 먼저 분리한다
sumBit = a XOR b XOR carryIn
carryOut = (a AND b) OR
(a AND carryIn) OR
(b AND carryIn)
0번째 비트의 carry-out을 1번째 비트의 carry-in으로 넘기는 과정을 7번째 비트까지 반복했습니다. 결과 바이트는 각 sumBit를 제자리에 놓아 만들고, 마지막 carry는 별도 값으로 보존했습니다. 구현 결과는 모든 입력에서 (a + b) & 0xFF와 일치했습니다.

carry-out과 signed overflow가 다른 네 사례
| 입력 바이트 | 8비트 결과 | carry | signed overflow | 해석 |
|---|---|---|---|---|
| 200 + 100 | 44 | 1 | false | unsigned 범위 초과, signed는 -56+100=44 |
| 100 + 50 | 150(-106) | 0 | true | 양수+양수가 음수로 바뀜 |
| 156(-100) + 206(-50) | 106 | 1 | true | 음수+음수가 양수로 바뀜 |
| 255(-1) + 1 | 0 | 1 | false | unsigned는 carry, signed 합은 정상 0 |
특히 100+50은 최종 carry가 없지만 signed 오버플로입니다. carry 플래그만 검사해 signed 범위를 보호할 수 없는 이유입니다. 반대로 -1+1은 carry가 있지만 signed 결과는 정확합니다.
전체 입력에서 두 신호의 발생 수가 달랐다
65,536쌍 중 unsigned carry가 발생한 쌍은 32,640개였고 signed overflow는 16,384개였습니다. 전자는 수학적 합이 255를 넘는지, 후자는 -128~127 범위를 벗어나는지로 대조했습니다.
signed overflow는 다음 비트식으로도 검사했습니다. 입력 두 개의 부호 비트가 같고 결과 부호가 달라지는 경우입니다.
overflow = (~(a ^ b) & (a ^ sum) & 0x80) != 0;
이 식이 65,536쌍에서 수학적 범위 검사와 일치하는지 확인했습니다. 비트 공식만 단독으로 믿지 않고, 더 넓은 정수로 계산한 기대값과 비교한 것이 핵심입니다.
초과 후 동작은 계산법이 아니라 제품 정책이다
UInt8에서 250+10의 수학적 합은 260입니다. 같은 입력을 세 정책으로 실행했습니다.
| 정책 | 결과 | 적합한 예 | 주의점 |
|---|---|---|---|
| wrap | 4 | 순환 시퀀스·저수준 체크섬 | 일반 카운터에서는 감소처럼 보임 |
| saturate | 255 | 픽셀·오디오 출력 한계 | 초과 크기 정보가 사라짐 |
| checked | RangeError | 길이·잔액·재고 | 호출자가 오류를 처리해야 함 |
어떤 정책도 항상 정답은 아닙니다. 필드가 무엇을 의미하는지에 따라 선택하고 테스트 이름에 정책을 드러냅니다. 암묵적 wrap이 필요한 코드와 데이터 손실을 금지해야 하는 코드를 같은 헬퍼에 넣지 않는 편이 안전합니다.
앱에서 자주 만나는 판단표
| 상황 | 권장 기본 정책 | 관찰할 값 |
|---|---|---|
| 패킷 시퀀스 번호 | 명세가 허용하면 wrap | 최댓값 전후 순서 |
| RGB 채널 합성 | saturate 또는 명시적 clamp | 0과 255 경계 |
| PCM 믹싱 | 넓은 타입에서 합산 후 clamp | 클리핑 횟수 |
| 다운로드 길이 | checked | 헤더와 실제 바이트 수 |
| 포인트·금액 | checked와 상위 범위 | 오류 및 트랜잭션 롤백 |
테스트는 세 층으로 나눈다
- 비트 단계: 각 자리의 sum과 carry trace를 확인합니다.
- 표현 단계: 전체 결과가 모듈러 값과 맞는지 확인합니다.
- 도메인 단계: wrap·saturate·checked 중 요구한 정책이 적용됐는지 확인합니다.
이 구분이 있으면 “덧셈기는 맞지만 제품 정책이 틀린 문제”를 분리할 수 있습니다. 100+50의 비트 결과 150은 계산 자체로는 맞지만 signed 온도라면 범위를 벗어난 결과입니다.
캐리 추적은 어느 자리에서 결과가 갈렸는지 보여 준다
255+1을 비트 0부터 계산하면 첫 자리에서 1+1이 sum 0, carry 1을 만들고 그 carry가 7번째 비트까지 연속 전달됩니다. 최종 바이트는 0이고 마지막 carry는 1입니다. 단순히 결과 0만 보면 입력이 원래 0이었는지 overflow로 돌아온 것인지 구분할 수 없습니다.
실행 스크립트는 네 대표 사례에 대해 각 비트의 a, b, carryIn, sum, carryOut을 저장했습니다. 프로덕션에서 모든 연산을 로깅할 필요는 없지만 실패 재현 테스트에는 이런 trace가 있으면 구현 오류와 정책 오류를 빠르게 분리할 수 있습니다.
오버플로 카운터를 운영 지표로 만들 수 있다
saturate가 화면 깨짐을 막아도 입력 범위 문제가 사라진 것은 아닙니다. 픽셀이나 오디오 값을 clamp했다면 발생 횟수를 집계하고, checked 연산이 실패했다면 필드 이름과 입력 범위를 개인정보 없이 남깁니다.
| 정책 | 로그할 신호 | 경보 기준 예 |
|---|---|---|
| wrap | 의도된 순환 횟수 | 예상 주기와 다를 때 |
| saturate | 상·하한 clamp 횟수 | 비율이 급증할 때 |
| checked | 거부된 필드와 범위 | 한 건이라도 데이터 계약 위반 |
이렇게 하면 “앱이 크래시하지 않았다”와 “데이터가 정상 범위였다”를 별도로 판단할 수 있습니다. 조용한 wrap이나 clamp는 장애를 늦게 발견하게 만들 수 있으므로 관찰 기준이 필요합니다.
검증 범위와 다음 읽을 글
- 고정 8비트 덧셈만 실행했으며 곱셈·부동소수점·언어별 자동 승격은 다루지 않았습니다.
- JavaScript 비트 연산의 32비트 변환 범위 안에서 검증했습니다.
- CPU 상태 플래그를 직접 읽은 것이 아니라 동등한 수학·비트 조건을 비교했습니다.
- 필드 폭 산정과 패킹은 비트 범위·엔디안 설계에서 분리해 다룹니다.