렉시컬 스코프(Lexical Scope)는 함수가 정의된 위치를 기준으로 스코프(변수의 유효 범위)가 결정되는 것을 의미.
자바에서는 변수의 유효 범위는 해당 변수가 선언된 블록 또는 메서드에서만 접근 가능
내부 클래스나 람다 표현식에서 외부 변수를 참조할 때, 변수의 선언 위치를 기준으로 유효 범위가 결정
자바는 동적 스코프(Dynamic Scope)를 지원하지 않는다.
: Inner 클래스가 호출되는 위치는 영향을 미치지 않고, 정의된 스코프를 기준으로 동작
public class LexicalScopeExample {
static int x = 10; // 전역 변수
public static void outer() {
int x = 20; // outer 메서드 내부 변수
class Inner { // 중첩 클래스
void printX() {
System.out.println(x); // outer 메서드의 x를 참조
}
}
Inner inner = new Inner();
inner.printX();
}
public static void main(String[] args) {
outer(); // 20 출력
}
}
:람다 표현식과 렉시컬 스코프 -> 람다 표현식 내부에서 outerVar를 참조. outerVar는 람다 표현식이 정의된 위치 기준으로 유효 범위를 가진다. 실행 위치는 상관없이 정의 시점에 존재하던 외부 변수 값을 기억.
public class LexicalScopeLambda {
public static void main(String[] args) {
int outerVar = 100; // 외부 변수
Runnable lambda = () -> {
// 외부 변수를 참조 (final 또는 effectively final이어야 함)
System.out.println("Outer variable: " + outerVar);
};
lambda.run(); // Outer variable: 100
}
}
: 렉시컬 스코프와 중첩 클래스 -> NestedClass는 main 메서드 내부에서 선언되었으며, outerVar를 참조합니다. NestedClass 내부 메서드가 실행되는 시점과 무관하게 정의된 스코프 기준으로 동작.
public class LexicalScopeNestedClass {
public static void main(String[] args) {
int outerVar = 50;
class NestedClass {
void printVar() {
System.out.println("Outer variable: " + outerVar);
}
}
NestedClass nested = new NestedClass();
nested.printVar(); // Outer variable: 50
}
}
렉시컬 스코프를 이용하면 데이터를 은닉시킬 수 있다. -> createCounter 메서드는 Runnable 객체를 반환하며, 람다 내부에서 count를 참조. count는 배열로 선언되어 있어 람다에서 변경 가능하며, 렉시컬 스코프를 통해 외부 변수를 캡처. count는 createCounter 메서드가 종료된 이후에도 메모리에 유지
public class LexicalScopeCounter {
public static Runnable createCounter() {
int[] count = {0}; // final 또는 effectively final 변수
return () -> {
count[0]++; // 외부 변수 참조
System.out.println("Count: " + count[0]);
};
}
public static void main(String[] args) {
Runnable counter = createCounter();
counter.run(); // Count: 1
counter.run(); // Count: 2
}
}
클로저(Closure)는 함수와 함수가 선언된 렉시컬(Lexical Environment)의 조합을 말합니다. 클로저는 함수가 생성될 당시의 외부 변수 상태를 기억하고, 생성된 이후에도 이를 참조하거나 수정할 수 있는 기능 제공.
클로저는 렉시컬 스코프를 기반으로 하여 함수가 선언된 위치의 스코프를 기억. 내부 함수는 자신이 선언된 환경에 있는 변수에 접근할 수 있습니다. 이 때, 외부 변수는 함수 내부에서 캡처(capture) 됩니다.
클로저를 사용하면 특정 변수나 데이터를 외부에서 직접 접근할 수 없게 하고, 함수 내부에서만 접근할 수 있도록 제한할 수 있다.
즉, 클로저는 함수가 반환된 이후에도 외부 스코프의 변수를 "참조"할 수 있다. javascript에서는 GC(Garbage Collector)가 함수 내부에서 참조되는 변수를 메모리에서 제거하지 않기 때문.
Java에서는 자바스크립트와 동일한 방식으로 클로저를 지원하지는 않지만 람다나 익명 클래스를 통해 클로저와 유사한 동작을 구현할 수 있다. 특히, 함수 내부에서 외부 변수나 환경을 참조하여 메모리에 유지시키는 동작은 자바에서도 가능하다.
import java.util.function.Supplier;
public class ClosureExample {
public static Supplier<Integer> createCounter() {
int[] count = {0}; // effectively final 변수 (배열로 처리)
return () -> {
count[0]++; // 외부 변수를 캡처
return count[0];
};
}
public static void main(String[] args) {
Supplier<Integer> counter = createCounter();
System.out.println(counter.get()); // 1
System.out.println(counter.get()); // 2
System.out.println(counter.get()); // 3
}
}
import java.util.function.Function;
public class ClosureExample {
public static Function<Integer, Integer> createMultiplier(int multiplier) {
return value -> value * multiplier; // multiplier를 클로저로 캡처
}
public static void main(String[] args) {
Function<Integer, Integer> doubleMultiplier = createMultiplier(2);
Function<Integer, Integer> tripleMultiplier = createMultiplier(3);
System.out.println(doubleMultiplier.apply(5)); // 10
System.out.println(tripleMultiplier.apply(5)); // 15
}
}
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ClosureTimer {
public static void timer() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
for (int i = 1; i <= 3; i++) {
final int capturedI = i; // effectively final로 변수 캡처
scheduler.schedule(() -> {
System.out.println("타이머: " + capturedI);
}, i * 1, TimeUnit.SECONDS);
}
scheduler.shutdown();
}
public static void main(String[] args) {
timer();
}
}
클로저를 사용하는 이유
하지만 Java 클로저는
취업 준비 중인 신입 백엔드 개발자로서 클로저와 유사한 동작을 Java에서 실습하는 것은 상당히 유익합니다. 특히, 자바의 람다 표현식과 익명 클래스, 그리고 이들을 활용한 상태 관리와 데이터 은닉에 대해 실습하면, 객체지향 프로그래밍과 함수형 프로그래밍을 모두 이해하는 데 도움이 됩니다. 다음은 실습 아이디어와 구체적인 방법입니다.
import java.util.function.Supplier;
public class CounterClosure {
public static Supplier<Integer> createCounter() {
int[] count = {0}; // effectively final 변수 (상태 저장)
return () -> {
count[0]++; // 상태 업데이트
return count[0];
};
}
public static void main(String[] args) {
// Counter 생성
Supplier<Integer> counter = createCounter();
// 상태 관리 확인
System.out.println(counter.get()); // 1
System.out.println(counter.get()); // 2
System.out.println(counter.get()); // 3
}
}
Supplier를 만들어 서로 독립된 상태를 유지하는지 확인합니다.import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class ClosureCache {
public static Function<Integer, Integer> createCache() {
Map<Integer, Integer> cache = new HashMap<>();
return (key) -> {
if (cache.containsKey(key)) {
System.out.println("캐시 히트! 키: " + key);
return cache.get(key);
}
System.out.println("캐시 미스! 키: " + key);
int value = key * key; // 계산 작업
cache.put(key, value);
return value;
};
}
public static void main(String[] args) {
Function<Integer, Integer> cachedFunction = createCache();
// 캐시 동작 확인
System.out.println(cachedFunction.apply(2)); // 캐시 미스!
System.out.println(cachedFunction.apply(2)); // 캐시 히트!
System.out.println(cachedFunction.apply(3)); // 캐시 미스!
System.out.println(cachedFunction.apply(3)); // 캐시 히트!
}
}
import java.util.function.Supplier;
public class ApiRateLimiter {
public static Supplier<Boolean> createRateLimiter(int maxCalls) {
int[] callCount = {0}; // 호출 횟수 저장
return () -> {
if (callCount[0] < maxCalls) {
callCount[0]++;
System.out.println("API 호출 가능. 현재 호출 횟수: " + callCount[0]);
return true;
} else {
System.out.println("API 호출 제한 초과!");
return false;
}
};
}
public static void main(String[] args) {
Supplier<Boolean> rateLimiter = createRateLimiter(3);
// API 호출 시도
rateLimiter.get(); // 호출 가능
rateLimiter.get(); // 호출 가능
rateLimiter.get(); // 호출 가능
rateLimiter.get(); // 호출 제한 초과
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class ObserverClosure {
public static Consumer<String> createNotifier() {
List<Consumer<String>> observers = new ArrayList<>();
// Observer 등록 및 알림
return (event) -> {
if (event.equals("register")) {
observers.add(message -> System.out.println("Observer 알림: " + message));
System.out.println("Observer 등록 완료!");
} else {
for (Consumer<String> observer : observers) {
observer.accept(event);
}
}
};
}
public static void main(String[] args) {
Consumer<String> notifier = createNotifier();
notifier.accept("register"); // Observer 등록
notifier.accept("register"); // Observer 등록
notifier.accept("이벤트 발생!"); // Observer 알림
}
}
public class ClosureDataHiding {
public static Runnable createHiddenData() {
int[] secretData = {42}; // 은닉된 데이터
return () -> {
System.out.println("데이터 접근: " + secretData[0]);
secretData[0] += 1;
};
}
public static void main(String[] args) {
Runnable hiddenData = createHiddenData();
hiddenData.run(); // 데이터 접근: 42
hiddenData.run(); // 데이터 접근: 43
}
}
람다 표현식과 함수형 인터페이스 이해:
상태 관리:
데이터 은닉:
현실 문제 적용:
이 실습들은 실무에서도 매우 유용하며, 이를 기반으로 자신만의 유틸리티 클래스나 라이브러리를 만들어보면 개발 역량과 문제 해결 능력을 더욱 키울 수 있습니다.