산술 연살을 할 때 주의할 점은 표현 가능한 산출 타입인지 확인해야 한다. 산출 타입으로 표현할 수 없는 값인 경우 오버플로우가 발생하여 쓰레기 값이 반환된다. 쉽게 말해, 정수형 오버플로우는 정수 값이 증가하면서 허용된 가장 큰 값보다 커져 실제 저장되는 값이 의도치 않게 아주 작은 수 이거나 음수가 되는 것
이다. 특히 반복문 제어, 메모리 할당, 메모리 복사 등을 위한 조건으로 사용자가 제공하는 입력값을 사용하고 그 과정에서 정수형 오버플로우가 발생하는 경우 보안상 문제를 유발 할 수 있다.
// 정수 오버플로우 예시
public class OverflowEx{
public static void main(String[] args){
int x = 50000;
int y = 50000;
int z = x * y;
System.out.println(z); // -1794967296
}
}
변수 x, y, z 모두 int 타입이다. 컴파일 에러도 없지만 변수 z에 예상한 값이 저장되지 않았다. 그 이유는 int 타입 범위를 초과하였기 때문인데, 올바른 값을 얻기 위해서는 변수가 long 타입이어야 한다.
정수형 오버플로우는 컴파일러가 오버플로우를 무시하기 때문에 잘못된 값이 저장될 뿐, 문제가 발생하여도 발생했는지 발견할 수 없기 때문에 매우 위험하다. 만약 정수형 오버플로우가 발생하더라도 프로그램은 수행 결과가 올바른지 알 수 없지만 정해진 연산을 처리할 것이다. 이러한 잘못된 결과는 프로그램의 오작동을 발생할 수 있으며, 보안에 취약한 부분을 내포하게 된다.
(*언더 플로우란, 오버플로우의 반대 개념으로 최소 범위보다 작은 수를 발생시키는 경우 발생하는 현상으로 언더플로우의 문제 또한 오버플로우와 같다.)
❗️ 오버플로우를 고려한 안전한 코딩기법
위의 3가지를 고려하여 코드를 짠다면 오버플로우를 방지할 수 있다.
- 허용범위 확인
각 타입별 허용 범위는 각 타입별 클래스의 MIN_VALUE(최소값), MAX_VALUE(최대값) 메소드를 통해 타입의 범위를 확인할 수 있다.
System.out.println("char: " + (int)Character.MIN_VALUE + " ~ " + (int)Character.MAX_VALUE); System.out.println("byte: " + Byte.MIN_VALUE + " ~ " + (int)Byte.MAX_VALUE); System.out.println("short: " + Short.MIN_VALUE + " ~ " + Short.MAX_VALUE); System.out.println("int: " + Integer.MIN_VALUE + " ~ " + Integer.MAX_VALUE); System.out.println("long: " + Long.MIN_VALUE + " ~ " + Long.MAX_VALUE); System.out.println("float: " + Float.MIN_VALUE + " ~ " + Float.MAX_VALUE); System.out.println("double: " + Double.MIN_VALUE + " ~ " + Double.MAX_VALUE); // 결과값 char: 0 ~ 65535 byte: -128 ~ 127 short: -32768 ~ 32767 int: -2147483648 ~ 2147483647 long: -9223372036854775808 ~ 9223372036854775807 float: 1.4E-45 ~ 3.4028235E38 double: 4.9E-324 ~ 1.7976931348623157E308
- BigInteger 클래스 활용
Java에서는 이런 정수 타입의 저장 한계를 극복하기 위해 BigInteger 클래스
를 제공하고 있다. BigInteger 클래스는 정수를 문자열 형태로 저장하기 때문에 메모리가 부족하지 않은 이상 무한한 숫자를 저장할 수 있다.
- BigInteger 선언 및 생성
BigInteger bigInteger1 = new BigInteger("1234"); BigInteger bigInteger2 = BigInteger.valueOf("1234");
- BigInteger 연산
BigInteger bigInteger1 = new BigInteger("123"); BigInteger bigInteger2 = new BigInteger("234"); // 더하기 System.out.println(bigInteger1.add(bigInteger2)); // 빼기 System.out.println(bigInteger1.subtract(bigInteger2)); // 곱하기 System.out.println(bigInteger1.multiply(bigInteger2)); // 나누기 System.out.println(bigInteger1.divide(bigInteger2)); // 나머지 System.out.println(bigInteger1.remainder(bigInteger2));
- 타입 변환
int intNum = bigInteger.intValue(); // int 타입으로 변환 long longNum = bigInteger.longValue(); // long 타입으로 변환 float floatNum = bigInteger.floatValue(); // float 타입으로 변환 double doubleNum = bigInteger.doubleValue(); // double 타입으로 변환 String stringValue = bigInteger.toString(); // 문자열 타입으로 변환
- 비교 연산
BigInteger bigInteger1 = new BigInteger("7777"); BigInteger bigInteger2 = new BigInteger("8888"); // 같으면 0, 입력한 인자가 더 크면 -1, 입력한 인자가 더 작으면 +1 int compare = bigInteger2.compareTo(bigInteger2); System.out.println(compare);
- 예외 처리: Math 클래스 정적 메소드 활용
더 큰 값을 허용하지 않거나 오버플로가 발생하는 것을 원하지 않을 경우 예외를 던지고 싶은 경우,
Java8부터 정확한 산술 연산을 위해 Math클래스의 정적 메소드를 사용할 수 있음.
정적 메서드 Math.addExact()등은 작업 결과 오버플로우 또는 언더플로우가 발생하면 예외가 발생한다.
- 예외를 발생시키는 메소드 종류 : 'Exact'가 포함된 메소드
// 인수값으로 int형 혹은 long형 값이 들어 올 수 있음 addExact(x, y) // x + y subtractExactTest(x, y) // x - y Math.multiplyExactTest(x, y) // x * y Math.incrementExact(a) // a++ Math.decrementExact(a) // a-- Math.negateExact(a) // -a Math.toIntExact(x) // long 타입을 int로 변경
- 사용 예제
int x = Integer.MAX_VALUE; int y= 1; System.out.println(Math.addExact(x, y)); //Exception in thread "main" java.lang.ArithmeticException: integer overflow 에러 발생 long x = Integer.MAX_VALUE; long y= 1; System.out.println(Math.addExact(x, y)); // 2147483648
Ref.
해답을 찾기위해...
Effective Data Engineering
Developer TABLE/Java
터프남 - Java Math 클래스 정적 메소드