좋아! 바로 디버깅 형식으로 한 줄도 빠짐없이 해설해줄게.
우선은 너가 헷갈릴 수 있는 포인트인 스코프, 값 변화, z 변수의 중복 선언을 중심으로 뜯어보자.
public class TrapExample {
public static void main(String[] args) {
int x = 10;
int y = 5;
{
int z = x + y;
x = z * 2;
}
int z = x - y;
System.out.println("x: " + x);
System.out.println("y: " + y);
System.out.println("z: " + z);
}
}
public class TrapExample {TrapExample이라는 이름의 클래스를 선언함.public static void main(String[] args) {main 메서드부터 실행을 시작함.int x = 10;변수 선언 및 초기화
x에 10을 할당.현재 상태:
x = 10
int y = 5;또 다른 정수형 변수 선언 및 초기화
y = 5현재 상태:
x = 10
y = 5
{새로운 블록 시작
int z = x + y;z는 블록 내부 지역 변수임.x + y = 10 + 5 = 15z = 15⛔ 이 z는 블록 안에서만 존재하고, 블록 밖에서 선언된 z와는 아무런 관련이 없음.
x = z * 2;z = 15 → z * 2 = 30 → x = 30x는 바깥에서 선언된 것이므로 영향 받음.현재 상태:
x = 30
y = 5
(이 블록 내부의 z = 15 → 이제 곧 사라짐)
}int z = x + y;로 선언했던 z는 스코프가 끝나서 소멸됨int z = x - y;30 - 5 = 25z = 25현재 상태:
x = 30
y = 5
z = 25
System.out.println("x: " + x);x: 30System.out.println("y: " + y);y: 5System.out.println("z: " + z);z: 25x: 30
y: 5
z: 25
| 포인트 | 설명 |
|---|---|
z 스코프 | 블록 {} 안에서 선언된 z는 블록 밖에서 안 보임. |
변수 x | 블록 안에서 값을 변경해도 바깥 스코프의 x에 직접 영향을 줌. |
변수 z 중복 선언 | 블록 안의 z와 블록 밖의 z는 서로 다른 변수임. 이름만 같음. |
| 최종 출력 | x=30, y=5, z=25 |
좋아! 바로 이 코드:
public class ScopeTrap {
static int num = 100;
public static void main(String[] args) {
int num = 50;
if (true) {
int result = num + ScopeTrap.num;
System.out.println("result: " + result);
}
int result = num * 2;
System.out.println("num: " + num);
System.out.println("result: " + result);
}
}
이걸 한 줄도 빠짐없이, 예제 값이 어떻게 흘러가는지 시각적으로 따라가면서 완전 해부해서 설명할게.
public class ScopeTrap {ScopeTrap.static 변수도 선언되어 있음.static int num = 100;ScopeTrap.num으로 언제든 접근 가능.현재 상태:
ScopeTrap.num = 100
public static void main(String[] args) {main() 시작 → 자바 프로그램의 시작점int num = 50;num이라고만 쓰면 → 50이 참조됨현재 상태:
ScopeTrap.num = 100 ← static 변수 (클래스 전역)
num = 50 ← main 메서드 안의 지역 변수
if (true) {int result = num + ScopeTrap.num;num의 정체를 파악하는 것!| 변수 | 값 | 설명 |
|---|---|---|
num | 50 | 지역 변수 |
ScopeTrap.num | 100 | 클래스(static) 변수 |
→ 계산: result = 50 + 100 = 150
이 result는 if 블록 안에서만 살아 있는 지역 변수임
System.out.println("result: " + result);result: 150
🔸 이건 if 블록 안의 result
}result (블록 안에 선언된) 사라짐int result = num * 2;result를 선언함 (바깥에서)num = 50 (지역 변수 그대로) → 50 * 2 = 100현재 상태:
num = 50
result = 100
(ScopeTrap.num = 100은 여전히 static 변수로 존재)
System.out.println("num: " + num);num: 50
System.out.println("result: " + result);result: 100
result: 150
num: 50
result: 100
| 포인트 | 설명 |
|---|---|
static int num = 100 | 클래스 변수. ScopeTrap.num으로 접근 |
int num = 50 | 지역 변수. static 변수와 이름 같지만 가려짐 (shadowing) |
if 블록 안의 result | 블록 전용 변수 |
블록 밖의 result | 별개의 지역 변수 |
좋아! 이 문제는 정보처리기사 실기에서 충분히 나올 수 있는 수준이고, 자바 문법 중에서도 초보자들이 정말 많이 틀리는 대표적인 주제인:
❗ 로컬 클래스에서 외부 지역변수를 사용할 때 발생하는 제한사항 (
final또는 effectively final)
이 부분을 노린 함정 문제야. 지금부터 한 줄도 빠짐없이 완전 해부해서 설명해줄게.
public class FinalTrap {
public static void main(String[] args) {
int x = 10;
final int y = 20;
class Calculator {
void printSum() {
System.out.println("x + y = " + (x + y));
}
}
x = 30;
Calculator calc = new Calculator();
calc.printSum();
}
}
정답은: C
이제 그 이유를 한 줄씩 디버깅 해설 방식으로 설명해볼게.
public class FinalTrap {FinalTrap이라는 클래스 선언.public static void main(String[] args) {main() 메서드 안에서 모든 실행이 이루어짐.int x = 10;지역 변수 x 선언.
현재 상태:
x = 10
final int y = 20;y는 final 지역 변수로 선언.
즉, 값이 변경 불가하고, Local Class에서 참조 가능
현재 상태:
y = 20 (final)
class Calculator { ... }🔥 조건: 지역 클래스(Local Class) 안에서 접근하는 지역 변수는
final또는 effectively final(값이 변경되지 않은 변수)이어야 함
void printSum() {System.out.println("x + y = " + (x + y));x와 y에 접근하려고 함.🔸 y: final int y = 20; → OK ✅
🔸 x: int x = 10; → 초기에는 effectively final로 간주될 수 있음
→ 하지만 아래에서 x = 30;으로 값이 바뀐다!
❗ 그러면
x는 이제 effectively final이 아님 → 지역 클래스에서 사용 불가 ❌
→ 컴파일 에러 발생
x = 30;x 값을 변경.x를 검사해서 "이거는 final도 아니고 변경도 되었으니까 에러!"라고 판단함.Calculator calc = new Calculator();calc.printSum();Local variable x defined in an enclosing scope must be final or effectively final
| 항목 | 설명 |
|---|---|
final int y = 20 | Local Class에서 접근 OK |
int x = 10 → x = 30 | 값이 변경되었기 때문에 Local Class 안에서 참조 불가 |
| Local Class 규칙 | 외부 지역 변수 참조 시 반드시 final 혹은 변경되지 않아야 함 |
| 결과 | 컴파일 에러 발생 |
effectively final이란?자바 8부터는 변수에 명시적으로 final 키워드를 안 써도,
"값을 바꾸지 않으면" → 컴파일러가 `final처럼 간주해줌. 이걸 effectively final이라고 해.
하지만 한 번이라도 값을 바꾸면 → 더 이상 effectively final 아님 → 지역 클래스/람다에서 사용 금지됨.
좋아! 이 문제는 지금까지 우리가 배운 내용을 총정리한 매우 훌륭한 함정 문제야.
이제 바로 디버깅 모드로 한 줄씩 흐름을 따라가며 완전히 해부해서 설명해줄게.
정답은 A.
staticValue: 100
instanceValue: 200
localValue: 10
✔️ 이 코드는 컴파일 에러 없이 정상 실행되고,
✔️ 람다 내부에서 모든 변수에 정상적으로 접근 가능한 상태야.
public class LambdaTrap {
static int staticValue = 100;
int instanceValue = 200;
public void calculate() {
int localValue = 10;
Runnable r = () -> {
System.out.println("staticValue: " + staticValue);
System.out.println("instanceValue: " + instanceValue);
System.out.println("localValue: " + localValue);
};
// localValue = 20; // (주의: 이 줄은 주석 상태)
r.run();
}
public static void main(String[] args) {
new LambdaTrap().calculate();
}
}
public class LambdaTrap {LambdaTrap.static int staticValue = 100;staticValue로 접근 가능.Runnable 안의 람다식도 이 static 변수에 제약 없이 접근 가능함.현재:
staticValue = 100
int instanceValue = 200;public void calculate() {calculate() 메서드 실행 시작.int localValue = 10;calculate() 메서드 안의 지역 변수🔥 람다 내부에서 참조되는 지역 변수는 final 또는 effectively final이어야 함.
✔️ 여기서는 localValue가 한 번만 할당되고, 이후 절대 변경되지 않으므로 → effectively final
→ 람다에서 참조 가능 ✅
Runnable r = () -> { ... };Runnable은 함수형 인터페이스 → 람다식 정의 가능System.out.println("staticValue: " + staticValue);staticValue는 클래스 변수 → 자유롭게 접근 가능 → 문제 없음 ✅staticValue: 100
System.out.println("instanceValue: " + instanceValue);instanceValue는 이 클래스의 인스턴스 변수 → 람다가 그 인스턴스 안에서 실행되므로 접근 가능 ✅instanceValue: 200
System.out.println("localValue: " + localValue);localValue = 10; 이후 절대 값이 변경되지 않음 → effectively final으로 인정됨출력:
localValue: 10
// localValue = 20; ← 이 줄이 핵심 포인트!localValue는 값을 바꾸지 않음.💥 하지만 이 주석을 풀면?
localValue = 20; // ❌ 값이 바뀌니까 → effectively final 아님 → 컴파일 에러 발생
✔️ 주석이기 때문에 지금은 정상 작동함
r.run();public static void main(String[] args) {main() 실행 시 new LambdaTrap().calculate(); 호출 → 위 모든 흐름이 실행됨staticValue: 100
instanceValue: 200
localValue: 10
| 변수 종류 | 변수명 | 접근 가능 여부 | 이유 |
|---|---|---|---|
| 클래스 변수 | staticValue | ✅ 가능 | static 변수는 어디서든 참조 가능 |
| 인스턴스 변수 | instanceValue | ✅ 가능 | 람다는 this에 접근 가능 |
| 지역 변수 | localValue | ✅ 가능 | 값이 안 바뀌어서 effectively final |
| 지역 변수 (값 바꾸면) | localValue = 20; | ❌ 컴파일 에러 | 값이 바뀌면 final 아님 |
좋아! 이 문제는 자바에서 아주 중요한 개념인 람다식(lambda) vs **익명 내부 클래스(anonymous class)**의 스코프 처리 차이를 다루는 실기 함정 문제야.
이건 진짜 실무에서도 종종 실수하는 부분이라, 이번 기회에 확실히 디버깅 해설 모드로 완전히 이해시켜줄게.
public class ShadowTrap {
interface Printer {
void print();
}
public static void main(String[] args) {
String message = "Outer";
Printer lambdaPrinter = () -> {
// String message = "Inner"; // ❌ (1)
System.out.println("Lambda: " + message);
};
Printer anonPrinter = new Printer() {
// String message = "Inner"; // ✅ (2)
public void print() {
String message = "Inner";
System.out.println("Anonymous: " + message);
}
};
lambdaPrinter.print();
anonPrinter.print();
}
}
| 비교 항목 | 람다식 | 익명 클래스 |
|---|---|---|
| 외부 지역 변수 사용 | 가능 (단, final 또는 effectively final일 때만) | 가능 |
| 같은 이름으로 변수 다시 선언 | ❌ 불가능 (shadowing 안 됨) | ✅ 가능 (shadowing 허용됨) |
| 내부에서 스코프 분리 여부 | ❌ 외부 메서드와 같은 스코프 | ✅ 완전히 새로운 스코프 |
String message = "Outer";message 선언"Outer"Printer lambdaPrinter = () -> {
// String message = "Inner"; // ❌ (1)
System.out.println("Lambda: " + message);
};
() -> { ... } 는 람다식message에 접근함"Outer" 변수를 참조하는 것임 → OK ✅📛 그런데 주석 처리된 줄을 보면:
String message = "Inner"; // ❌ 변수 재선언 시도
이 줄이 주석이 아니라면 → 컴파일 에러 발생!
이유:
🧠 요약:
람다 안에서는 바깥의 지역 변수 이름과 같은 이름의 변수를 선언할 수 없다
✔️ 하지만 주석 처리돼 있으니 현재 상태에서는 문제 없음
Lambda: Outer
Printer anonPrinter = new Printer() {
public void print() {
String message = "Inner"; // ✅ 가능
System.out.println("Anonymous: " + message);
}
};
message라는 지역 변수를 print() 안에서 새로 선언함✔️ 이건 전혀 문제 없음
✔️ 왜냐면 익명 클래스는 자체 스코프를 가지기 때문
→ 바깥의 message = "Outer"와는 완전히 별개로 동작함
Anonymous: Inner
Lambda: Outer
Anonymous: Inner
| 항목 | 설명 |
|---|---|
람다식 내부에서 message 선언 ❌ | 람다는 외부 메서드와 같은 스코프이므로 중복 선언 불가 |
람다에서 바깥의 message 사용 ✅ | effectively final이므로 접근 가능 |
익명 클래스에서 message 다시 선언 ✅ | 별도 스코프를 가지므로 가능 |
| 최종 출력 | Lambda: Outer, Anonymous: Inner |
만약 람다식 안에서 이렇게 선언했다면 어떻게 될까?
String message = "Inner"; // 이 줄이 주석 해제된다면?
정답: ❌ 컴파일 에러 발생!
이유:
message는 이미 같은 스코프(main 메서드)에서 존재하므로, 같은 이름으로 재선언 불가!
좋아! 이 문제는 자바에서 람다, 익명 클래스, 변수 캡처의 작동 방식이 실제 스레드(Thread)와 함께 어떻게 움직이는지를 테스트하는 실기 스타일 고급 문제야.
📌 핵심 테마는:
public class ThreadScopeTrap {
public static void main(String[] args) {
int count = 5;
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Anonymous: " + count);
}
};
Runnable r2 = () -> {
System.out.println("Lambda: " + count);
};
// count++; // (주석 처리된 상태)
new Thread(r1).start();
new Thread(r2).start();
}
}
정답은 A 또는 B
(둘 다 맞음. 출력 순서는 스레드 실행 타이밍에 따라 달라지므로 순서는 정해져 있지 않음)
Anonymous: 5
Lambda: 5
또는
Lambda: 5
Anonymous: 5
int count = 5;지역 변수 count 선언 및 초기화.
현재 상태:
count = 5
이 count는 바로 아래 두 Runnable 안에서 참조됨
Runnable r1 = new Runnable() { ... };run() 안에서 외부 지역 변수 count 사용✅ 익명 클래스는 외부 지역 변수에 접근 가능, 단 조건:
count는 변경되지 않았으므로 → effectively final 상태 → OKRunnable r2 = () -> { ... };count를 참조✅ 람다도 외부 지역 변수에 접근할 수 있음, 단:
final 또는 effectively final일 것count는 값을 바꾸지 않았으므로 → 역시 OK// count++; // 주석 처리된 상태count는 변경되지 않음 → effectively final 유지됨❗ 이 줄을 주석 해제하면 → 아래 참고
new Thread(r1).start();new Thread(r2).start();Anonymous: 5
Lambda: 5
Lambda: 5
Anonymous: 5
✔️ 둘 다 정답 → A, B 모두 맞음
count++ 주석을 해제한다면?count++; // ❌ 지역 변수 값을 변경
이 경우 → count는 더 이상 effectively final이 아님
→ 익명 클래스와 람다 모두에서 사용 불가 → 컴파일 에러 발생!
🔴 컴파일러가 다음과 같은 에러 메시지를 출력:
Local variable count defined in an enclosing scope must be final or effectively final
❌ 둘 다 못 씀 → 정답은: C
| 항목 | 설명 |
|---|---|
count = 5 | 지역 변수, 변경되지 않음 → effectively final |
익명 클래스에서 count 사용 | ✅ 가능 |
람다식에서 count 사용 | ✅ 가능 |
| 출력 순서 | 병렬 스레드 → 순서 보장 X |
count++ 주석 해제 시 | ❌ 컴파일 에러 발생 (람다 & 익명 클래스 둘 다) |
| 상황 | 정답 |
|---|---|
주석 상태일 때 (count++ 없음) | A 또는 B (둘 다 맞음) |
주석 해제할 경우 (count++ 있음) | C (컴파일 에러) |
final 또는 effectively final일 것좋아! 이 문제는 자바에서 정말 많이 헷갈리는 개념이야.
바로 람다식(lambda)과 익명 클래스(anonymous class)에서의 this가 가리키는 객체가 다르다는 걸 묻는 문제지.
정답은: C
Lambda this: ThisTrap
Anonymous this: ThisTrap$1
이제 그 이유를 한 줄씩 디버깅 해설 방식으로 완전히 해부해서 설명할게.
public class ThisTrap {
interface Printer {
void print();
}
public void runTest() {
Printer lambdaPrinter = () -> {
System.out.println("Lambda this: " + this.getClass().getSimpleName());
};
Printer anonPrinter = new Printer() {
public void print() {
System.out.println("Anonymous this: " + this.getClass().getSimpleName());
}
};
lambdaPrinter.print();
anonPrinter.print();
}
public static void main(String[] args) {
new ThisTrap().runTest();
}
}
public class ThisTrap {ThisTrapinterface Printer { void print(); }public void runTest() {main() → runTest()로 이어짐Printer lambdaPrinter = () -> {
System.out.println("Lambda this: " + this.getClass().getSimpleName());
};
this는 과연 누구인가?💡 람다에서의
this는 enclosing 클래스의 인스턴스 (즉, 바깥 클래스인ThisTrap)를 가리킨다
this는 바깥 클래스인 ThisTrap의 인스턴스를 그대로 가리킴✔️ 결과:
Lambda this: ThisTrap
Printer anonPrinter = new Printer() {
public void print() {
System.out.println("Anonymous this: " + this.getClass().getSimpleName());
}
};
this는 누구인가?💡 익명 클래스는 새로운 클래스 객체를 생성한다
→ 즉,ThisTrap$1이라는 이름의 새로운 내부 클래스를 만들고 그 인스턴스를 가짐
this는 익명 클래스 자기 자신을 가리킴ThisTrap$1
✔️ 결과:
Anonymous this: ThisTrap$1
Lambda this: ThisTrap
Anonymous this: ThisTrap$1
| 항목 | 람다식 | 익명 클래스 |
|---|---|---|
this가 가리키는 대상 | 바깥 클래스 (ThisTrap) | 익명 클래스 자신 (ThisTrap$1) |
getClass().getSimpleName() 결과 | "ThisTrap" | "ThisTrap$1" |
| 클래스 생성 방식 | 바깥 클래스 내부의 코드 | 별도 내부 클래스 객체 생성 |
✅ 람다 안의
this는 바깥 클래스
❌ 익명 클래스 안의this는 익명 클래스 자기 자신
람다에서 this는 ThisTrap을 가리키니까,
this.runTest()도 가능하고, 바깥의 모든 멤버 변수와 메서드 접근이 자유롭지만,
익명 클래스에서는 this가 자기 자신을 가리켜서
ThisTrap 클래스의 멤버에 접근하려면 ThisTrap.this.someMethod() 이런 식으로 써야 해.