정수는 직선이 아니라 원이다
앞의 두 글에서 이런 걸 봤습니다. 타입의 크기가 담을 수 있는 범위를 정하고, 부호 있는 타입은 맨 앞 비트를 부호에 씁니다.
그러면 이 코드는 무엇을 찍을까요.
int max = int.MaxValue;
Console.WriteLine(max + 1);
0이 아닙니다. 오류도 아닙니다.
-2147483648
21억이 갑자기 -21억이 됩니다. 예외도 경고도 나지 않고, 로그도 남지 않습니다. 왜 하필 최솟값인지 보려면 머릿속의 그림 하나를 바꿔야 합니다.
자릿수가 정해지면 값은 돌 수밖에 없다
정수를 배울 때 대부분 수직선을 떠올립니다. 왼쪽 끝에 최솟값, 오른쪽 끝에 최댓값. 그 그림이면 최댓값에서 1을 더할 때 오른쪽 벽에 부딪혀 멈추거나 넘칠 것 같습니다.
실제로는 그렇지 않습니다. 정수는 원입니다.
위가 0이고 시계 방향으로 값이 커집니다. 그래서 오른쪽 반이 양수, 왼쪽 반이 음수입니다. 맨 아래 붉은 선에서 최댓값과 최솟값이 맞닿아 있습니다.
점이 거기를 지나는 순간을 보세요. 값이 뛰는 게 아니라 원이 원래 그렇게 생겼습니다. 칸이 8개뿐인데 계속 세면 처음 자리로 돌아오는 것 말고 갈 데가 없습니다.
시계와 같습니다. 12시에서 한 시간을 더 가면 13시가 아니라 1시입니다. 시계가 고장 난 게 아니라 자리가 12개뿐이라서 그렇습니다.
8비트로 보면 한눈에 보인다
원이 왜 그렇게 되는지는 비트를 직접 세어보면 바로 나옵니다. “부호 있음” 으로 두고 “자동으로 세기” 를 눌러 보세요.
126, 127 까지는 순조롭습니다. 그다음에 128 이 아니라 -128 이 나옵니다.
비트를 보면 이유가 그대로 보입니다.
0111 1111 127 = sbyte.MaxValue
+ 0000 0001 1
= 1000 0000 -128 = sbyte.MinValue
값 비트가 전부 1인 상태에서 1을 더하면 자리올림이 부호 비트까지 올라갑니다. 맨 앞이 1이 되는 순간 그 비트 묶음은 음수로 읽힙니다.
오버플로우는 값이 넘치는 게 아니라 자리올림이 부호 비트를 침범하는 일입니다.
int 도 똑같습니다. 32비트일 뿐 원리가 같습니다. 위 그림에서 int (32비트) 를 눌러도
원의 모양은 그대로고 이음매에 적힌 숫자만 커집니다.
반대로도 돕니다
최솟값에서 1을 빼면 최댓값이 됩니다. 원이니까 당연합니다.
int min = int.MinValue;
Console.WriteLine(min - 1); // 2147483647
실무에서 만나는 모습
여기까지는 퀴즈처럼 보입니다. 문제는 아무 신호도 없다는 점입니다. 예외도, 경고도, 로그도 없습니다. 값만 조용히 바뀝니다.
그래서 이런 모습으로 나타납니다.
- 누적 점수나 골드가 어느 날 갑자기 음수가 됩니다. 그날 뭘 고쳤는지 아무리 뒤져도 안 나옵니다
- 파일 크기 합계가 2GB 근처에서 깨집니다.
int로 바이트를 더하면 21억이 한계입니다 - 경과 시간을 밀리초로 재다가 25일쯤에서 음수가 됩니다
공통점은 한참 잘 돌다가 어느 순간 깨진다는 것입니다. 테스트에서 안 잡히는 이유도 이것입니다. 값이 그만큼 커지려면 시간이 걸립니다.
어떻게 막나
세 가지입니다. 상황에 맞는 걸 고르면 됩니다.
넓히기가 가장 단순합니다. 누적값처럼 계속 커지는 것은 처음부터 long 으로 잡는 게
낫습니다. 앞 글에서 봤듯이 8바이트로 담을 수 있는 건 922경이라 현실에서 안 넘칩니다.
검사하기는 게임에서 자주 씁니다. 골드가 상한에서 멈추는 편이 음수가 되는 것보다 훨씬 낫습니다.
checked 는 넘치면 OverflowException 을 던집니다.
“여기서 넘치면 그건 버그다” 라고 말하고 싶을 때 씁니다.
C# 에서 정수 연산은 기본이
unchecked입니다. 조용히 도는 게 기본 동작이고,checked는 그걸 켜는 스위치입니다. 프로젝트 전체에 켜려면.csproj에<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>를 넣습니다. 상수식은 예외로, 컴파일러가 컴파일할 때 잡아줍니다.
정리
- 정수는 직선이 아니라 원이다. 자릿수가 정해진 이상 값은 돌 수밖에 없다.
- 최댓값 다음은 최솟값, 최솟값 이전은 최댓값이다.
- 오버플로우는 값이 넘치는 게 아니라 자리올림이 부호 비트를 침범하는 일이다.
- 예외도 경고도 없다. 한참 잘 돌다가 어느 날 값만 바뀐다.
- 막는 방법은 넓히기 · 검사하기 ·
checked셋이다.