클로저(Closure)란 무엇인가요?

김상욱·2024년 11월 14일

클로저(Closure)란 무엇인가요?

렉시컬 스코프(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 클로저는

  • 자바에서는 람다나 익명 클래스 내부에서 참조하는 변수는 final 또는 effectively final(변경되지 않는 변수)이어야 한다.
  • 자바는 자바스크립트처럼 동적으로 변수를 캡처하지 않으므로, 명싲거으로 클로저를 구현해야 한다.

취업 준비 중인 신입 백엔드 개발자로서 클로저와 유사한 동작을 Java에서 실습하는 것은 상당히 유익합니다. 특히, 자바의 람다 표현식과 익명 클래스, 그리고 이들을 활용한 상태 관리와 데이터 은닉에 대해 실습하면, 객체지향 프로그래밍과 함수형 프로그래밍을 모두 이해하는 데 도움이 됩니다. 다음은 실습 아이디어와 구체적인 방법입니다.


1. 클로저를 활용한 상태 관리 실습

실습 목표:

  • 람다 표현식으로 외부 변수를 캡처하고, 이를 통해 상태를 관리하는 방법을 익힙니다.

실습 내용:

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를 만들어 서로 독립된 상태를 유지하는지 확인합니다.
  • Counter를 증감, 초기화 등 다양한 동작을 지원하는 클래스로 확장합니다.

2. 클로저 기반의 간단한 메모리 캐시 구현

실습 목표:

  • 클로저를 활용해 데이터 캐싱과 관련된 로직을 구현합니다.
  • 외부 데이터를 처리할 때 메모리 캐시를 유지하는 방법을 이해합니다.

실습 내용:

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)); // 캐시 히트!
    }
}

확장 아이디어:

  • 캐시의 최대 크기를 설정하여 오래된 항목을 삭제하는 LRU 캐시를 구현합니다.
  • 멀티스레드 환경에서 동작하도록 캐시를 동기화(Synchronized) 처리합니다.

3. 클로저를 활용한 API 호출 제한 실습

실습 목표:

  • 클로저와 상태 관리 기법을 활용하여 API 호출을 횟수 제한하는 기능을 구현합니다.

실습 내용:

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(); // 호출 제한 초과
    }
}

확장 아이디어:

  • 일정 시간 동안 호출 횟수를 제한하는 토큰 버킷 알고리즘을 구현합니다.
  • 비동기 작업과 결합하여 실시간으로 호출 제한을 관리합니다.

4. 클로저를 활용한 간단한 Observer 패턴 구현

실습 목표:

  • 클로저를 활용하여 Observer 패턴을 이해하고 상태 변화를 관리합니다.

실습 내용:

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 알림
    }
}

확장 아이디어:

  • 특정 조건에 따라 Observer를 추가, 삭제하는 기능을 구현합니다.
  • 여러 종류의 이벤트를 처리하는 Observer 패턴을 확장합니다.

5. 데이터 은닉과 연관된 클로저 실습

실습 목표:

  • 클로저를 통해 외부에서 변수에 직접 접근할 수 없도록 데이터 은닉을 구현합니다.

실습 내용:

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
    }
}

확장 아이디어:

  • 데이터를 외부에 노출하지 않으면서 다양한 기능을 제공하는 래퍼 클래스를 설계합니다.

실습을 통해 얻을 수 있는 역량

  1. 람다 표현식과 함수형 인터페이스 이해:

    • Java의 람다 표현식을 사용하여 클로저와 유사한 동작을 이해할 수 있습니다.
  2. 상태 관리:

    • 외부 변수와 내부 변수 간의 상호작용을 통해 상태를 효율적으로 유지하는 방법을 배웁니다.
  3. 데이터 은닉:

    • 외부에서 직접 접근할 수 없는 변수를 캡슐화하여 데이터 보안을 강화합니다.
  4. 현실 문제 적용:

    • 캐싱, 호출 제한, Observer 패턴 등 실제 개발에서 자주 활용되는 개념을 실습합니다.

이 실습들은 실무에서도 매우 유용하며, 이를 기반으로 자신만의 유틸리티 클래스나 라이브러리를 만들어보면 개발 역량과 문제 해결 능력을 더욱 키울 수 있습니다.

0개의 댓글