
자바를 처음 배우기 시작하면서 가장 어렵고 생소한 파트 중 하나가 오버플로우였다. 자바스크립트에서는 단순히 MAX_SAFE_INTEGER 이내에서 산술 연산을 하면 Number를 사용하고, MAX_SAFE_INTEGER보다 큰 값에서는 BigInt 자료형을 사용하여 알고리즘을 풀었던 기억이 있다. (물론 MAX_SAFE_INTEGER보다 큰 값을 사용할 일이 많지도 않았다.)
그런데 자바에서는 표현 범위 별로 숫자를 나타내는 비트 수가 세분화되어 있는 것은 물론이고, 부호가 있는 정수 자료형과 부호가 없는 정수 자료형으로까지 세분화되어 있다. 게다가 산술 연산의 결과가 특정 비트 수의 표현 범위 밖으로 넘어가면 오버플로우가 발생하기도 한다. 오늘은 자바의 오버플로우 현상에 대해 나와 같은 자바 초보자 입장에서 이해할 수 있는 글을 써보려고 한다.
먼저 정수 자료형의 부호 여부에 따라 오버플로우의 발생 양상이 다르므로, 부호 있는 정수와 부호 없는 정수를 구분하면서 시작해보겠다.
자바에는 양수, 음수, 0을 나타낼 수 있는 부호 있는 정수 자료형이 있고, 양수와 0의 범위까지만 나타낼 수 있어서 음수는 표현할 수 없는 자료형이 있다. 기본자료형에서는 char를 제외하고 byte, short, int, long, float, double이 모두 부호 있는 자료형이다. 부호 없는 자료형에는 char가 있다. (자바에는 부호 없는 정수 타입이 기본적으로 없지만, Integer 클래스의 메서드를 통해 부호 없는 정수를 처리할 수 있긴 하다.) 각각의 표현 범위를 정리하면 아래와 같다.
부호 있는 정수 (맨 앞의 비트는 부호를 나타낸다. 1이 음수, 0은 양수)
byte-128 ~ 127 (또는 -2^7 ~ 2^7 - 1)
short-32,768 ~ 32,767 (또는 -2^15 ~ 2^15 - 1)
int-2,147,483,648 ~ 2,147,483,647 (또는 -2^31 ~ 2^31 - 1)
long-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 또는 (-2^63 ~ 2^63 - 1)
부호 없는 정수
char0 ~ 65,535 (또는 0 ~ 2^8 - 1)
부호 있는 정수의 경우 표현범위를 넘어가면 음수로 향하고, 부호 없는 정수의 경우 표현 범위를 넘어가면 0으로 향한다.
부호 있는 정수인 int 자료형과 부호 없는 자료형인 char 자료형을 예로 들어보자.
먼저 더 간단하게 설명할 수 있는 부호 없는 자료형부터 설명해보겠다.
// 부호 없는 정수 char
char c = 65535;
System.out.println(c + 1); // 65536
System.out.println((char)(c + 1)); // "\u0000" (유니코드 0에 해당하는 공백)
char는 산술 연산을 할 때 자동으로 int로 형변환된다고 한다. char 자료형에 할당된 65535에 단순히 1을 더하면 int 자료형으로 형변환되어 65536이 출력되지만, char로 타입 캐스팅하여 출력하면 유니코드 0에 해당하는 빈문자열이 출력된다.

2^16에 해당하는 1111 1111 1111 1111 이라는 숫자에 1을 더하면 1 0000 0000 0000 0000이 된다. 이것은 char 자료형의 크기인 16비트로 나타낼 수 없는 범위이다. 따라서 최상위 비트인 1을 무시하고 하위 16개 비트로만 숫자를 나타낸다. 이는 0000 0000 0000 0000, 즉 0이다. 따라서 부호 없는 정수의 오버플로우는 최소값인 0으로 향한다. (참고로 뒤에서 설명하겠지만, 부호 없는 정수에는 2의 보수법이 적용되지 않는다.)
// 부호 있는 정수 int
int n = Integer.MAX_VALUE; // 2,147,483,647 (== 2^32 - 1)
System.out.println(n + 1); // -2,147,483,648
부호 있는 정수의 경우 2의 보수법이 적용된다. 2의 보수법이란 음수인 이진수를 10진수로 변환하는 방법이다. 순서는 다음과 같다.
2의 보수법
- 부호 결정 (맨앞의 부호를 통해 최종적으로 음수가 될지 양수가 될지 결정한다.)
- 모든 비트를 반전시킨다.
- 1을 더한다.
- 1.에서 결정한 부호를 적용한다.
아래와 같이 그림으로 살펴보자.

최근에 Comparator를 배웠는데, 오버플로우를 고려하지 않았을 때 정렬 기준에 어떤 오류가 날 수 있는지 점검할 수 있는 예시를 들어보려 한다. 물론 아래 WrongAscending이라는 Comparator를 사용하지 않아도 Integer 내부의 comparable 메서드에 의해 알아서 오름차순 정렬해주겠지만, 오류를 보여주기 위해 굳이 오름차순 정렬해주는 (논리적 오류가 있는) Comparator를 만들어보았다.
class Mistake {
public Integer[] mistake() {
Integer[] arr = {-2147483648, 1};
Arrays.sort(arr, new WrongAscending());
return arr; // [1, -2147483648]
}
class WrongAscending implements Comparator<Integer> {
public int compare(Integer a, Integer b){
return a - b; // -2147483648 - 1 > 0 😈 음수 오버플로우에 의해 양수가 되어 내림차순 정렬됨
}
}
Comparator의 compare 메서드를 오버라이딩할 때에는 앞의 수가 더 작을 경우 음수를 return해야 오름차순 정렬된다. 하지만 int범위의 최소값인 -2147483648 - 1을 했더니 음수 오버플로우가 발생하여 양수를 return하게 되고 결국 내림차순 정렬된다. 따라서 오버플로우를 고려하여 a - b가 아니라 compareTo 메서드를 사용하여 비교하는 것이 바람직하다.
class CorrectMistake {
public Integer[] correctMistake() {
Integer[] arr = {-2147483648, 1};
Arrays.sort(arr, new CorrectAscending());
System.out.println(Arrays.toString(arr));
return arr; // [-2147483648, 1]
}
}
class CorrectAscending implements Comparator<Integer> {
public int compare(Integer a, Integer b){
return a.compareTo(b); //단순 뺄셈이 아니라 메서드를 사용하여 오버플로우에 의한 오류 방지
}
}
여기까지 오버플로우에 대해 살펴보았다. 처음에는 와닿지 않았지만, 부호 있는 자료형과 부호 없는 자료형을 구분한 다음 2의 보수법이 부호 있는 자료형에만 적용된다는 사실을 염두에 두니 이해가 되었다. 특히 Comparator를 적절히 사용하지 않을 경우 오버플로우에 의해 잘못된 결과를 초래할 수 있음을 살펴보며 주의를 기울이지 않을 경우 언제든 오버플로우에 의한 의도치 않은 결과를 만나게 될 수 있음을 실감하게 되었다.
영어 버전
https://medium.com/@woooooooow22/java-integer-overflow-dde7359d67d8