[Java] 18. 익명 클래스(Anonymous class)와 클로저(closure)


Study/Java  2020. 5. 18. 18:02

안녕하세요. 명월입니다.


이 글은 Java에서의 익명 클래스(Anonymous class)와 클로저(closure)에 대한 글입니다.


예전에 제가 클래스를 생성하는 방법에 대한 글을 작성한 적이 있습니다.

링크 - [Java] 07. 클래스를 생성하는 방법 (생성자를 사용하는 방법)

링크 - [Java] 12. 인터페이스(Interface)


간략하게 정리하면 Java에서의 클래스는 가장 최소 단위로 동작하는 형태이고 객체를 생성(인스턴스 생성)하는 단위입니다. 그리고 그 생성된 인스턴스를 추상화하는 것을 인터페이스가 있습니다.

그리고 인터페이스를 작성할 때는 함수를 설정할 수 있습니다.

// 인터페이스
interface Testable {
  // 두개의 함수로 구성
  void run();
  void execute();
}
// 위 인터페이스를 상속
class Test1 implements Testable {
  // run 함수 재정의
  @Override
  public void run() {
    // 콘솔 출력
    System.out.println("Test - run!");
  }
  // execute 함수 재정의
  @Override
  public void execute() {
    // 콘솔 출력
    System.out.println("Test - execute!");
  }
}
public class Example {
  // 인터페이스 Testable로 된 클래스를 실행하는 함수
  public static void test(Testable param) {
    // run 실행
    param.run();
    // execute 실행
    param.execute();
  }
  // 실행 함수
  public static void main(String... args) {
    // Test1 인스턴스를 생성
    Testable case1 = new Test1();
    // test 함수 실행
    test(case1);
  }
}

위 예제는 제가 Testable의 인터페이스를 작성해서 Test1클래스로 상속받고, 인스턴스를 생성해서 test함수를 이용해 Testable의 인터페이스를 상속 받는 클래스의 run함수를 실행하고 execute함수를 실행하는 프로그램입니다.

여기까지 아무런 문제가 없습니다. 저는 예제를 만들기 위해서 하나의 클래스 파일에 하나의 인터페이스와 2개의 클래스를 작성했지만, 사실은 Java code 표준 규약상 각각의 파일에 따로 작성을 해야 합니다.>즉, 위 예제는 3개의 파일이 생성되겠네요.


여기서 우리는 클래스 중심이 아닌 test 함수의 중심으로 위 소스를 확인합시다.test함수의 파라미터는 Testable 인터페이스 타입을 받습니다. 즉, Testable인터페이스를 상속받은 클래스를 받는다는 뜻입니다.

우리가 Test1의 클래스를 작성하고 인스턴스를 생성하고 test함수에 넘겨 실행했습니다. 다른 Testable 인터페이스를 상속받은 케이스를 만들고 싶을 때는 Test2 클래스를 작성해서 Testable 인터페이스를 상속받고 인스턴스를 생성해서 함수로 넘기면 됩니다. 또 다른 Testable 인터페이스를 상속받고 싶은 때는 반복이 되겠습니다.

그럴 때마다 우리는 클래스를 계속 만들고 클래스가 계속 만든다면 클래스 파일도 계속 생성한다는 뜻입니다.

만약, 100가지의 케이스를 만든다고 할때, 100개의 클래스를 만든다는 뜻입니다.


그래서 이 클래스를 작성하지 않고 인터페이스만으로 인스턴스를 생성하여 사용하는 방법을 익명 클래스(Anonymous class)라고 합니다.

// 인터페이스
interface Testable {
  // 두개의 함수로 구성
  void run();
  void execute();
}
public class Example {
  // 인터페이스 Testable로 된 클래스를 실행하는 함수
  public static void test(Testable param) {
    // run 실행
    param.run();
    // execute 실행
    param.execute();
  }
  // 실행 함수
  public static void main(String... args) {
    // 인터페이스로 인스턴스를 생성
    Testable case1 = new Testable() {
      // run 함수를 재정의한다.
      @Override
      public void run() {
        // 콘솔 출력
        System.out.println("run call!");
      }
      // execute 함수를 재정의한다.
      @Override
      public void execute() {
        // 콘솔 출력
        System.out.println("execute call!");
      }
    };
    // test 함수 실행
    test(case1);
  }
}

위 예제를 보시면 인터페이스를 new로 생성하고 스택 영역으로 함수를 재정의했습니다. 이렇게 함으로 따로 클래스를 작성하지 않고 인터페이스만으로도 인스턴스를 생성할 수 있습니다.


이 익명 클래스가 단순히 클래스를 생성하는 것을 줄이는 기능만 있다면 이 익명 클래스를 권장하지는 않습니다.

왜냐하면 Java 규약으로 클래스 단위로 파일을 만든 것은 관리하기 쉽게 하고 가독성 향상을 위한 정해진 규칙인데 적절하게 사용하면 좋을 지 모르지만, 역시 무분별하게 사용하면 코드 가독성이 치명적이기 때문에 사용하지 않는 편이 낫습니다.

그러나 이 익명클래스는 단순히 클래스를 작성하는 것에 대한 대체 문법이 아니고 익명 클래스를 사용하는 이유는 바로 클로저 기능 때문입니다.

// 인터페이스
interface Testable {
  // 함수로 추상화
  void run();
}
public class Example {
  // 인터페이스 Testable로 된 클래스를 실행하는 함수
  public static void test(Testable param) {
    // run 실행
    param.run();
  }
  // 실행 함수
  public static void main(String... args) {
    for (int i = 0; i < 100; i++) {
      // 상수화. 클로저를 사용하기 위해서는 값을 상수화되어야 한다.
      // 클로저 값을 사용하는 익명 클래스에서 이 값을 변경하지 못하게 하기 위해서 이다.
      final int index = i;
      // Testable 인스턴스를 생성
      Testable case1 = new Testable() {
        // run 함수 재정의
        @Override
        public void run() {
          // index는 이 인스턴스 밖에서 선언된 변수
          System.out.println("run call! index - " + index);
        }
      };
      // test 함수 실행
      test(case1);
    }
  }
}

위 예제에서 run 함수와 execute 함수에서 사용되는 index의 값은 Testable의 인스턴스 안이 아닌 main 함수에서 for문의 값으로 final된 상수값입니다.

즉, 익명 클래스에서는 내부에서 선언된 값이 아닌 상위 스택에서 선언된 값을 점프해서 사용할 수 있습니다.

이것을 Java에서는 클로저(closure)라고 합니다.


이 익명클래스와 클로저 기능이 없이 이걸 똑같이 구현한다면 클래스에서 int i의 값을 받는 함수가 필요할 것입니다.

// 인터페이스
interface Testable {
  // 함수로 추상화
  void run();
}
// 위 인터페이스를 상속
class Test1 implements Testable {
  // 멤버 변수
  private int index;
  // 클로저와 같은 기능으로 index를 받는다.
  public Test1(int index) {
    this.index = index;
  }
  // run 함수 재정의
  @Override
  public void run() {
    // 콘솔 출력
    System.out.println("run call! index - " + index);
  }
}
public class Example {
  // 인터페이스 Testable로 된 클래스를 실행하는 함수
  public static void test(Testable param) {
    // run 실행
    param.run();
  }
  // 실행 함수
  public static void main(String... args) {
    for (int i = 0; i < 100; i++) {
      Testable case1 = new Test1(i);
      // test 함수 실행
      test(case1);
    }
  }
}

만약에 이 넘겨받는 변수가 많아진다고 한다면, 혹은 실행되는 클래스의 사양이 조금씩 달라진다고 한다면 소스는 기하급수적으로 복잡해지고 어려워질 것입니다.

또 위에서는 단순히 원시 데이터형(Primitive type)을 사용했지만, 클래스를 사용해서 사용할 때마다 값이 조금씩 변경이 되어야 한다면 더욱 유용하게 사용됩니다.

// 인터페이스
interface Testable {
  // 함수로 추상화
  void run();
}
// 카운팅 클래스
class Node {
  // counting 함수를 호출한 횟수
  private int count = 0;
  // count 값을 리턴, 리턴하면 값을 1 증가한다.
  public int counting() {
    return this.count++;
  }
}
public class Example {
  // 인터페이스 Testable로 된 클래스를 실행하는 함수
  public static void test(Testable param) {
    // run 실행
    param.run();
  }
  // 실행 함수
  public static void main(String... args) {
    // 상수화. 클로저를 사용하기 위해서는 값을 상수화되어야 한다.
    // 클로저 값을 사용하는 익명 클래스에서 이 값을 변경하지 못하게 하기 위해서이다.
    final Node node = new Node();
    for (int i = 0; i < 100; i++) {
      // Testable 인스턴스를 생성
      Testable case1 = new Testable() {
        // run 함수 재정의
        @Override
        public void run() {
          // node는 인스턴스 밖에서 선언된 값
          // node는 상수화 되었지만, node안의 count 변수값은 변경된다.
          System.out.println("run call! index - " + node.counting());
        }
      };
      // test 함수 실행
      test(case1);
    }
  }
}

위 예제를 보면 Node 클래스를 선언하고 node.counting()할 때마다 Node클래스의 count 값이 변경되는 것을 확인할 수 있습니다.


여기까지 Java에서의 익명 클래스(Anonymous class)와 클로저(closure)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.