Study/Java

[Java] 스코프, 형변환

the.Dev.Cat 2026. 4. 4. 21:00
Java 입문 — 스코프, 형변환

김영한의 자바 입문 강의 내용을 정리한 글이다.

1. 스코프(Scope)

지역 변수와 코드 블록

변수는 선언한 위치에 따라 지역 변수(Local Variable), 클래스 변수(Class Variable), 인스턴스 변수(Instance Variable)로 분류된다. 지금 단계에서 다루는 것은 지역 변수다.

지역 변수는 자신이 선언된 코드 블록({}) 안에서만 존재한다. 블록이 끝나는 순간 메모리에서 제거되며, 이후에는 접근할 수 없다. 이처럼 변수에 접근할 수 있는 범위를 스코프(Scope), 즉 유효 범위라 한다.

package scope; public class Scope1 { public static void main(String[] args) { int m = 10; // m 생존 시작 if (true) { int x = 20; // x 생존 시작 System.out.println("if m = " + m); // 외부 블록 접근 가능 System.out.println("if x = " + x); } // x 생존 종료 // System.out.println("main x = " + x); // 컴파일 오류: x에 접근 불가 System.out.println("main m = " + m); } // m 생존 종료 }

mmain{} 블록 전체에서 살아 있으므로 스코프가 넓다. xif{} 블록 안에서만 살아 있으므로 스코프가 좁다. if{} 블록이 끝난 뒤 x에 접근하면 cannot find symbol 컴파일 오류가 발생한다.

main{} 블록 int m = 10 m 스코프 if{} 블록 int x = 20 x 스코프 x 소멸

for 문 초기식에서 선언한 변수도 마찬가지다.

package scope; public class Scope2 { public static void main(String[] args) { int m = 10; for (int i = 0; i < 2; i++) { // i 생존 시작 System.out.println("for m = " + m); System.out.println("for i = " + i); } // i 생존 종료 // System.out.println("main i = " + i); // 컴파일 오류 System.out.println("main m = " + m); } }

for (int i = 0; ...) 처럼 초기식에서 선언한 ifor{} 블록 안에서만 유효하다.


스코프가 존재하는 이유

변수를 선언 시점부터 끝까지 살려두면 간단하지 않을까 생각할 수 있다. 아래 두 코드를 비교해 보자.

스코프가 불필요하게 넓은 코드

public class Scope3_1 { public static void main(String[] args) { int m = 10; int temp = 0; // main 전체 스코프 if (m > 0) { temp = m * 2; System.out.println("temp = " + temp); } System.out.println("m = " + m); } }

스코프를 최소화한 코드

public class Scope3_2 { public static void main(String[] args) { int m = 10; if (m > 0) { int temp = m * 2; // if 블록 스코프로 제한 System.out.println("temp = " + temp); } System.out.println("m = " + m); } }

tempif 블록에서만 필요한 임시 변수다. 첫 번째 코드는 tempmain 전체 스코프에 선언해 두 가지 문제를 야기한다.

  • 비효율적인 메모리: tempif 블록 종료 후에도 main 종료까지 메모리에 유지된다. JVM 스택 프레임의 지역 변수 슬롯을 불필요하게 점유한다.
  • 코드 복잡성 증가: if 블록이 끝나도 temp를 어디서든 접근할 수 있다. 유지보수 시 m뿐 아니라 temp까지 신경 써야 한다. 코드가 복잡해질수록 이 부담은 기하급수적으로 커진다.

두 번째 코드는 tempif{} 안으로 이동시켰다. 블록이 끝나는 즉시 JVM이 해당 스택 슬롯을 회수하고, 코드를 읽는 사람도 temp의 유효 범위를 즉시 파악할 수 있다.

Joshua Bloch의 Effective Java에서도 "지역 변수의 스코프를 최소화하라(Minimize the scope of local variables)"를 명시적 원칙으로 제시한다. 변수를 처음 사용하는 지점에서 선언하고, 선언과 동시에 초기화하는 것이 핵심이다. 좋은 프로그램은 무한한 자유가 있는 프로그램이 아니라 적절한 제약이 있는 프로그램이다.


while vs for — 스코프 관점 비교

카운터 변수 i를 예로 들면 whilefor의 스코프 차이가 명확하다.

// while: i의 스코프가 main 전체 int i = 1; int endNum = 3; while (i <= endNum) { System.out.println("i=" + i); i++; } // 이 시점에도 i에 접근 가능 — 불필요한 노출 // for: i의 스코프가 for 블록으로 한정 int endNum = 3; for (int i = 1; i <= endNum; i++) { System.out.println("i=" + i); } // 이 시점에서 i는 존재하지 않음

루프 카운터처럼 루프 내부에서만 필요한 변수라면 for를 선택해 스코프를 제한하는 것이 메모리와 가독성 양쪽에서 유리하다.


JVM 스택 프레임과 스코프

스코프가 메모리와 어떻게 연결되는지 이해하려면 JVM 스택 구조를 알아야 한다. 메서드가 호출될 때마다 JVM은 스레드 전용 스택에 스택 프레임(Stack Frame)을 하나 생성한다.

JVM Stack (스레드 전용) Stack Frame 지역 변수 배열 [0] m = 10 [1] x = 20 (if 블록 내) * if 블록 종료 시 슬롯 반환 오퍼랜드 스택 연산 중간값 임시 저장 생존 주기 메서드 호출 → 프레임 생성 지역 변수 슬롯 확보 블록 종료 → 변수 슬롯 반환 다음 변수가 슬롯 재사용 가능 메서드 반환 → 프레임 제거 모든 지역 변수 소멸 ※ 힙(Heap)에 남지 않음 ※ GC 대상 아님 — 즉시 제거

프레임은 지역 변수 배열, 오퍼랜드 스택, 런타임 상수 풀 참조로 구성된다. 지역 변수 배열의 슬롯 크기는 컴파일 시점에 결정된다. int, float 같은 4바이트 타입은 슬롯 1개, long, double 같은 8바이트 타입은 슬롯 2개를 차지한다. 코드 블록이 끝나면 그 블록에서 선언된 변수의 슬롯을 이후 변수가 재사용할 수 있다. 메서드 실행이 완전히 끝나면 프레임 전체가 스택에서 제거되고, 모든 지역 변수도 함께 사라진다. 힙(Heap)에 남지 않으므로 가비지 컬렉션 대상도 아니다.


2. 형변환(Type Casting)

타입 계층과 표현 범위

자바 기본 숫자 타입의 표현 범위는 아래와 같다. 단순히 바이트 수가 많을수록 범위가 큰 것은 아니다. 실수형은 정수형과 비트 해석 방식이 다르다.

타입크기표현 범위비고
byte1 byte-128 ~ 127정수
short2 byte-32,768 ~ 32,767정수
int4 byte약 -21억 ~ 21억정수 기본
long8 byte약 -920경 ~ 920경정수
float4 byte약 ±3.4E38, 유효 7자리IEEE 754
double8 byte약 ±1.7E308, 유효 15자리IEEE 754, 실수 기본
표현 범위 순서 (작음 → 큼) byte short int long float double 이 방향은 자동 형변환

자동 형변환 (묵시적 형변환)

작은 범위 타입을 큰 범위 타입에 대입하면 컴파일러가 자동으로 형변환을 수행한다. 개발자가 직접 코드를 작성할 필요가 없다.

package casting; public class Casting1 { public static void main(String[] args) { int intValue = 10; long longValue; double doubleValue; longValue = intValue; // int → long 자동 형변환 System.out.println("longValue = " + longValue); // 10 doubleValue = intValue; // int → double 자동 형변환 System.out.println("doubleValue1 = " + doubleValue); // 10.0 doubleValue = 20L; // long → double 자동 형변환 System.out.println("doubleValue2 = " + doubleValue); // 20.0 } }

컴파일러가 내부적으로 무엇을 하는지 단계별로 보면 이렇다.

// intValue = 10 doubleValue = intValue; doubleValue = (double) intValue; // 형 맞추기 (컴파일러가 삽입) doubleValue = (double) 10; // 변수 값 읽기 doubleValue = 10.0; // 형변환 완료

JVM 바이트코드 수준에서는 i2d(int to double), i2l(int to long), l2d(long to double) 같은 전용 변환 명령어가 사용된다. 이 명령어들은 런타임 예외를 발생시키지 않으며, 값의 크기 정보를 손실 없이 보존한다. 넓히는 방향의 변환이므로 항상 안전하다.

자동 형변환 — int → double 예시 int 10 4 byte i2d 자동 double 10.0 8 byte 정수 → 부동소수점 값 손실 없음 10 → 10.0

명시적 형변환 (캐스팅)

큰 범위 타입을 작은 범위 타입에 대입하면 컴파일 오류가 발생한다.

double doubleValue = 1.5; int intValue = 0; intValue = doubleValue; // 컴파일 오류 // java: incompatible types: possible lossy conversion from double to int

doubleint보다 표현 범위가 크고 소수점도 있다. 손실 없이 담기 불가능하다. 그러나 개발자가 손실을 의도적으로 감수하고 변환하고 싶다면 명시적 형변환(캐스팅)을 사용한다.

package casting; public class Casting2 { public static void main(String[] args) { double doubleValue = 1.5; int intValue = 0; intValue = (int) doubleValue; // 명시적 형변환 System.out.println(intValue); // 1 (소수점 버림) } }

캐스팅 연산자 (int)를 앞에 붙이면 컴파일러는 개발자가 이 위험을 인지했다고 판단하고 변환을 허용한다. 소수점 부분은 버려지며(truncation), 반올림이 아님에 주의한다.

명시적 형변환 과정을 단계별로 보면 이렇다.

// doubleValue = 1.5 intValue = (int) doubleValue; intValue = (int) 1.5; // doubleValue 값 읽기 intValue = 1; // (int)로 형변환, 소수점 버림 (반올림 아님)
명시적 형변환 — double → int 예시 double 1.5 8 byte d2i (int) 캐스팅 int 1 4 byte 소수점 버림 1.5 → 1 반올림 아님

캐스팅(casting)은 금속이나 물질을 녹여 특정 형태의 틀에 부어 만드는 과정에서 유래한 용어다. 데이터를 다른 타입의 틀로 강제로 바꾸는 행위와 의미가 맞닿아 있다. 형변환을 한다고 해서 원본 변수 doubleValue의 타입이나 값이 바뀌는 것은 아니다. 그 값을 읽어서 변환하는 것이며, doubleValue는 여전히 1.5를 유지한다.


형변환과 오버플로우

명시적 형변환에서 대상 타입의 표현 범위를 초과하면 오버플로우가 발생한다.

package casting; public class Casting3 { public static void main(String[] args) { long maxIntValue = 2147483647; // int 최댓값 long maxIntOver = 2147483648L; // int 최댓값 + 1 int intValue = 0; intValue = (int) maxIntValue; System.out.println("maxIntValue casting = " + intValue); // 2147483647 intValue = (int) maxIntOver; System.out.println("maxIntOver casting = " + intValue); // -2147483648 } }

maxIntOverint 범위를 1 초과하므로 오버플로우가 발생한다. 이진수 수준에서 어떤 일이 일어나는지 보면 이렇다.

long → int 캐스팅 시 상위 32비트 버림 2147483647L = 하위 32비트: 0 1111...1111 (31개) 부호 → int 범위 내, 그대로 2147483647 2147483648L = 하위 32비트: 1 0000...0000 (31개) 부호 → 부호 비트 1 = 음수, -2147483648

오버플로우가 발생하면 결과를 예측하기가 매우 어렵다. 오버플로우 자체가 발생하지 않도록 설계하는 것이 올바른 방향이다. 이 예제에서는 intValue의 타입을 int에서 long으로 바꾸면 문제가 해결된다. 오버플로우가 발생했을 때 결과가 어떻게 되는지 계산하는 데 시간을 쓰지 않아야 한다.


계산과 형변환

형변환은 대입뿐 아니라 계산 과정에서도 발생한다. 자바 산술 연산의 두 가지 원칙이다.

  1. 같은 타입끼리의 연산은 같은 타입의 결과를 낸다. int + intint, double + doubledouble.
  2. 서로 다른 타입의 연산은 큰 범위로 자동 형변환이 일어난다. int + longlong + long, int + doubledouble + double.
package casting; public class Casting4 { public static void main(String[] args) { int div1 = 3 / 2; System.out.println("div1 = " + div1); // 1 (int / int = int) double div2 = 3 / 2; System.out.println("div2 = " + div2); // 1.0 (int / int = int → double로 대입 시 자동 형변환) double div3 = 3.0 / 2; System.out.println("div3 = " + div3); // 1.5 (double / int → double / double) double div4 = (double) 3 / 2; System.out.println("div4 = " + div4); // 1.5 (명시적 형변환으로 double / double 유도) int a = 3; int b = 2; double result = (double) a / b; System.out.println("result = " + result); // 1.5 } }

div2의 경우가 흔한 실수 포인트다. double 변수에 대입하더라도 오른쪽 연산(3 / 2)이 먼저 int끼리 계산되어 1이 된 뒤, 그 결과가 double로 변환돼 1.0이 된다. 소수점 결과를 얻으려면 연산 전에 피연산자 중 하나를 double로 바꿔야 한다.

변수를 사용하는 경우 단계별 처리 과정은 이렇다.

double result = (double) a / b; double result = (double) 3 / 2; // 변수 값 읽기 double result = (double) 3 / (double) 2; // double + int → int가 double로 자동 형변환 double result = 3.0 / 2.0; // double / double → double double result = 1.5;

계산 결과 정리표를 보면 의도가 더 명확해진다.

표현식연산 과정결과비고
3 / 2int / int1정수 나눗셈, 소수점 버림
double d = 3 / 2int / int → double 대입1.0연산 후 자동 형변환
3.0 / 2double / int → double / double1.5피연산자 중 하나가 double
(double) 3 / 2double / int → double / double1.5명시적 형변환으로 동일 효과

형변환 방향 요약

자동(묵시적) 형변환 byte short int long float double 데이터 손실 없음 / 개발자 코드 불필요 / 항상 안전 명시적(강제) 형변환 double float long int short byte 소수점 버림 / 오버플로우 가능 / (타입) 캐스팅 연산자 필수
구분방향개발자 코드데이터 손실
자동 형변환작은 타입 → 큰 타입불필요없음
명시적 형변환큰 타입 → 작은 타입(타입) 필수소수점 버림, 오버플로우 가능