본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
2장 : 값·식·분기·반복

스코프와 숫자 형변환

지역 변수의 생존 범위를 좁히고 자동·명시 형변환의 손실과 오버플로를 검사해 Study Log 계산 경계를 닫습니다.

지역 변수는 선언된 블록 안에서만 이름으로 접근할 수 있습니다. 숫자 값은 다른 타입으로 옮길 때 범위가 넓어지거나 정보가 잘릴 수 있습니다. 두 주제는 모두 값을 어디까지 안전하게 사용할 수 있는가라는 경계를 다룹니다. 변수의 코드 범위를 좁히고 숫자의 값 범위를 확인하면 잘못된 상태가 살아 있는 구간도 줄어듭니다.

지역 변수의 블록 범위

lab/OutOfScope.java
public final class OutOfScope {
    public static void main(String[] args) {
        boolean valid = true;

        if (valid) {
            int normalizedMinutes = 45;
            System.out.println("inside=" + normalizedMinutes);
        }

        System.out.println("outside=" + normalizedMinutes);
    }
}
error: cannot find symbol
        System.out.println("outside=" + normalizedMinutes);
                                          ^
  symbol:   variable normalizedMinutes

normalizedMinutesif 블록 안에서 선언돼 닫는 중괄호까지 생존합니다. 바깥 main 블록에서 선언한 valid는 안쪽 블록에서도 읽을 수 있지만 반대 방향은 불가능합니다.

임시 변수를 필요 이상으로 넓게 두면 컴파일 오류는 사라져도 검토 범위가 커집니다.

int normalizedMinutes = 0;
if (valid) {
    normalizedMinutes = 45;
}

이제 if가 실행되지 않았을 때의 0도 이후 코드가 처리해야 합니다. 임시 값이 조건 안에서만 필요하다면 그 블록 안에 선언해 유효하지 않은 경로에서 이름 자체를 사용할 수 없게 합니다.

반복 변수의 수명

src/ScopedCounter.java
public final class ScopedCounter {
    public static void main(String[] args) {
        int total = 0;

        for (int session = 1; session <= 3; session++) {
            int minutes = session * 20;
            total += minutes;
            System.out.println(session + "=" + minutes);
        }

        System.out.println("total=" + total);
    }
}

session과 각 회차의 minutes는 반복 안에서만 필요하고 total은 반복 결과를 밖에서 출력해야 하므로 main 블록에 둡니다. 변수마다 필요한 범위가 다릅니다. 모든 변수를 메서드 첫 줄에 선언하는 방식은 아직 값이 필요하지 않은 코드까지 이름과 초기 상태를 노출합니다.

자동 형변환

src/WideningConversions.java
public final class WideningConversions {
    public static void main(String[] args) {
        int minutes = 45;
        long longMinutes = minutes;
        double doubleMinutes = minutes;
        double fromLong = longMinutes;

        System.out.println("int=" + minutes);
        System.out.println("long=" + longMinutes);
        System.out.println("double=" + doubleMinutes);
        System.out.println("fromLong=" + fromLong);
    }
}
int=45
long=45
double=45.0
fromLong=45.0

int → long → double 방향은 더 넓은 표현 범위로 대입하므로 Java가 자동 형변환합니다. 개념적으로는 (long) minutes가 있지만 소스에 쓰지 않아도 됩니다. 다만 아주 큰 longdouble로 바꿀 때 모든 정수 자릿수가 정확히 보존된다는 뜻은 아닙니다. double은 범위는 넓지만 정밀도가 제한됩니다.

서로 다른 타입의 산술은 넓은 타입 쪽으로 변환한 뒤 계산합니다.

double result = 3.0 / 2; // 2가 2.0으로 변환된 뒤 1.5

반대로 double result = 3 / 2;는 정수 계산 1이 먼저 끝난 뒤 1.0으로 대입됩니다. 변환 위치가 계산 결과를 바꿉니다.

명시적 형변환

src/NarrowingConversions.java
public final class NarrowingConversions {
    public static void main(String[] args) {
        double preciseMinutes = 42.9;
        int wholeMinutes = (int) preciseMinutes;

        System.out.println("before=" + preciseMinutes);
        System.out.println("after=" + wholeMinutes);
    }
}
before=42.9
after=42

명시적 (int) 형변환은 소수 부분을 반올림하지 않고 버립니다. 원본 preciseMinutes의 타입과 값은 42.9로 그대로이며 읽어 온 복사 값만 변환됩니다. 반올림이 요구라면 Math.round처럼 의도를 나타내는 연산을 별도로 사용합니다.

캐스트를 빼면 컴파일러가 잠재적 손실을 막습니다.

lab/LossyAssignment.java
public final class LossyAssignment {
    public static void main(String[] args) {
        double preciseMinutes = 42.9;
        int wholeMinutes = preciseMinutes;
        System.out.println(wholeMinutes);
    }
}
error: incompatible types: possible lossy conversion from double to int

명시적 캐스트는 안전을 보장하는 문법이 아니라 개발자가 손실 가능성을 인지했다고 컴파일러에 알리는 표식입니다. 실제 범위와 소수 처리 정책은 코드가 확인해야 합니다.

정수 축소와 오버플로

lab/NarrowingOverflow.java
public final class NarrowingOverflow {
    public static void main(String[] args) {
        long justOverMax = 2_147_483_648L;
        int narrowed = (int) justOverMax;

        System.out.println("source=" + justOverMax);
        System.out.println("narrowed=" + narrowed);
    }
}
source=2147483648
narrowed=-2147483648

예외가 발생하지 않고 하위 비트만 남아 Integer.MIN_VALUE가 됩니다. 시간이나 금액에서 이런 조용한 오버플로는 매우 위험합니다. 캐스트 전에 범위를 확인하거나 정확한 변환 API를 사용합니다.

src/CheckedNarrowing.java
public final class CheckedNarrowing {
    public static void main(String[] args) {
        long value = 2_147_483_648L;

        if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
            System.out.println("outside int range=" + value);
            return;
        }

        int narrowed = Math.toIntExact(value);
        System.out.println(narrowed);
    }
}
outside int range=2147483648

Math.toIntExact도 범위를 넘으면 ArithmeticException을 던져 조용한 잘림을 막습니다. 앞 조건으로 사용자 메시지를 제공하거나 예외를 호출자에게 전달할 수 있습니다.

연산 중간값의 오버플로

src/MultiplicationWidth.java
public final class MultiplicationWidth {
    public static void main(String[] args) {
        int minutes = 100_000;

        long wrongMillis = minutes * 60_000;
        long correctMillis = minutes * 60_000L;

        System.out.println("wrong=" + wrongMillis);
        System.out.println("correct=" + correctMillis);
    }
}
wrong=1705032704
correct=6000000000

첫 곱셈의 두 피연산자가 int라 결과도 int에서 먼저 오버플로한 뒤 잘못된 값을 long으로 넓힙니다. 둘째 식은 60_000L 때문에 minuteslong으로 자동 변환하고 long 곱셈을 수행합니다. 결과 변수가 long이라는 사실만으로 중간 계산이 안전해지지 않습니다.

안전한 계산 타입

src/SafeRecordMath.java
public final class SafeRecordMath {
    public static void main(String[] args) {
        String topic = args[0];
        int minutes = Integer.parseInt(args[1]);
        int target = Integer.parseInt(args[2]);

        if (minutes < 1 || minutes > 600 || target < 1 || target > 600) {
            System.out.println("minutes outside range");
            return;
        }

        long elapsedMillis = minutes * 60_000L;
        double progress = (double) minutes / target;

        System.out.println("topic=" + topic);
        System.out.println("elapsedMillis=" + elapsedMillis);
        System.out.println("progress=" + progress);
    }
}
java -cp out SafeRecordMath casting 90 120
topic=casting
elapsedMillis=5400000
progress=0.75

topic, minutes, targetmain에서 결과까지 필요합니다. 오류 메시지 외에 추가 값이 없는 유효성 실패 경로는 즉시 return해 이후 계산의 전제를 단순하게 합니다. long 리터럴은 곱셈 전 범위를 넓히고 double 캐스트는 나눗셈 전 정밀도를 바꿉니다.

연습 문제

long 초 값을 받아 분 단위 double과, 범위 안일 때만 int 초를 출력하는 SecondConversion을 작성하세요. 입력은 9_000_000_000을 사용하며 잘린 int를 출력하면 안 됩니다.

해설 보기
src/SecondConversion.java
public final class SecondConversion {
    public static void main(String[] args) {
        long seconds = 9_000_000_000L;
        double minutes = seconds / 60.0;

        System.out.println("minutes=" + minutes);
        if (seconds >= Integer.MIN_VALUE && seconds <= Integer.MAX_VALUE) {
            int exactSeconds = Math.toIntExact(seconds);
            System.out.println("intSeconds=" + exactSeconds);
        } else {
            System.out.println("intSeconds=outside-range");
        }
    }
}
minutes=1.5E8
intSeconds=outside-range

60.0double 리터럴이라 나눗셈 전에 seconds가 double로 변환됩니다. int 변환은 범위 조건이 참인 블록 안에서만 일어나며 변수도 그 블록 밖으로 노출되지 않습니다.