본문 바로가기

Language/JAVA

[JAVA]익명 클래스(Anonymous Class)

해당 게시글의 내용은'김영한의 실전 자바 - 중급편'을 기반으로 작성되었으며, 제가 학습한 내용을 토대로 다시 작성하게 되었습니다.

익명 클래스(Anonymous Class)

익명 클래스(Anonymous class)는 지역 클래스의 특별한 종류의 하나입니다. 익명 클래스는 지역 클래스인데 클래스의 이름이 없다는 특징이 있습니다.

 

다음 코드를 살펴보겠습니다.

package chapter08.local;

public class LocalOuterV2 {
    private int outInstanceVar = 3;

    public void process(int paramVar) {
        int localVar = 1;

        class LocalPrinter implements Printer {
            int value = 0;

            @Override
            public void print(){
                System.out.println("value = "+value);
                System.out.println("localVar = "+localVar);
                System.out.println("paramVar = "+paramVar);
                System.out.println("outInstanceVar = "+outInstanceVar);
            }
        }

        Printer printer = new LocalPrinter();
        printer.print();
    }

    public static void main(String[] args) {
        LocalOuterV2 localOuterV2 = new LocalOuterV2();
        localOuterV2.process(2);
    }
}

여기서는 지역 클래스를 사용하기 위해 선언생성의 2가지 단계를 거칩니다.

  1. 선언: 지역 클래스를 LocalPrinter라는 이름으로 선언합니다. 이때 Printer 인터페이스도 함께 구현합니다.
  2. 생성: new LocalPrinter()를 사용해서 앞서 선언한 지역 클래스의 인스턴스를 생성합니다.

 

익명 클래스(Anonymous class)를 사용하면 선언과 생성을 한번에 할 수 있습니다.

Printer printer = new Printer(){
 //body
}

다음 코드를 통해서 좀 더 자세히 알아보겠습니다.

package chapter08.anonymous;

import chapter08.local.Printer;
public class AnonymousOuter {
    private int outInstanceVar = 3;
    public void process(int paramVar) {
        int localVar = 1;

        //인터페이스에 대한 객체를 생성하면서 동시에 구현체를 만든다.
        Printer printer = new Printer() {
         int value = 0;

         @Override
         public void print(){
             System.out.println("value = "+value);
             System.out.println("localVar = "+localVar);
             System.out.println("paramVar = "+paramVar);
             System.out.println("outInstanceVar = "+outInstanceVar);
         }
        };
        printer.print();
        System.out.println("printer.class = "+printer.getClass());
    }

    public static void main(String[] args) {
        AnonymousOuter main = new AnonymousOuter();
        main.process(2);
    }
}

/*
[실행 결과]

value=0
localVar=1
paramVar=2
outInstanceVar=3
printer.class=class nested.anonymous.AnonymousOuter$1
*/

new Printer() { body }

익명 클래스는 클래스의 본문(body)를 정의함과 동시에 생성합니다. new 다음에 바로 상속 받으면서 구현 할 부모 타입을 입력하면 됩니다. 이 코드는 마치 인터페이스 Printer를 생성하는 것 처럼 보입니다. 하지만 자바에서 인터페이스를 생성하는 것은 불가능합니다. 이 코드는 인터페이스를 생성하는 것이 아니고 Printer 라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것입니다.

쉽게 말해, Printer를 상속(구현)하면서 바로 생성하는 것입니다.

 

익명 클래스의 특징

  • 이름이 없는 지역 클래스를 선언하면서 동시에 생성합니다.
  • 익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 합니다. 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요합니다.
  • 익명 클래스는 말 그대로 이름이 없습니다. 따라서 생성자를 가질 수 없습니다.(기본 생성자만 사용됩니다)
  • 익명 클래스는 AnonymousOuter$1과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의됩니다. 익명 클래스가 여러 개면 $1, $2, $3으로 숫자가 증가하면서 구분됩니다.

 

익명 클래스의 장점

인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 간결해진다는 장점이 있습니다. 하지만, 복잡하거나 재사용이 필요한 경우에는 별도의 클래스를 정의하는 것이 좋습니다.

 

익명 클래스를 사용할 수 없는 경우

익명 클래스는 인스턴스를 단 한번만 생성할 수 있습니다. 다음과 같이 여러 번의 인스턴스 생성이 필요하다면 익명 클래스를 사용할 수 없습니다. 대신, 지역 클래스를 선언하고 사용하면 됩니다.

Printer printer1 = new LocalPrinter();
printer1.print();

Printer printer2 = new LocalPrinter();
printer2.print();

익명 클래스의 활용

익명 클래스 활용 - 1

익명 클래스가 어떻게 활용되는지 알아보기 전에 간단한 문제를 하나 풀어보겠습니다.

[리팩토링 전]

public class Ex0Main {
     public static void helloJava() {
         System.out.println("프로그램 시작");
         System.out.println("Hello Java");
         System.out.println("프로그램 종료");
     }
     
     public static void helloSpring() {
         System.out.println("프로그램 시작");
         System.out.println("Hello Spring");
         System.out.println("프로그램 종료");
     }
     
     public static void main(String[] args) {
         helloJava();
         helloSpring();
     }
}

/*
[실행 결과]

프로그램 시작
Hello Java
프로그램 종료
프로그램 시작
Hello Spring
프로그램 종료
*/

위 코드를 보시면 중복된 부분들이 보입니다. 이를 리팩토링 하여 수정해보겠습니다.

[리팩토링 후]

public class Ex0RefMain {
     public static void hello(String str) {
         System.out.println("프로그램 시작");
         System.out.println(str);
         System.out.println("프로그램 종료");
     }
     public static void main(String[] args) {
         hello("hello Java");
         hello("hello Spring");
     }
}

/*
[실행 결과]

프로그램 시작
hello Java
프로그램 종료
프로그램 시작
hello Spring
프로그램 종료
*/

 리팩토링의 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것입니다. 변하지 않는 부분은 그대로 유지하고 변하는 부분은 어떻게 해결할 것인가에 집중하면 됩니다.

 

리팩토링 전 코드를 보면 helloJava(), helloSpring()에 "Hello Java", "Hello Spring" 문자열 부분만 변하고 있습니다.

여기서 상황에 따라 변하는 문자열 데이터를 다음과 같이 외부에서 전달 받아 출력하면 됩니다.

public static void hello(String str) {
     System.out.println("프로그램 시작"); //변하지 않는 부분
     System.out.println(str); //str: 변하는 부분
     System.out.println("프로그램 종료"); //변하지 않는 부분
}

단순한 문제이지만 프로그래밍에서 중복을 제거하고, 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것입니다. 여기서는 변하지 않는 "프로그램 시작", "프로그램 종료"를 출력하는 부분을 그대로 유지하고 상황에 따라 변화가 필요한 문자열은 외부에서 전달 받아서 처리하였습니다. 이렇게 변하는 부분과 변하지 않는 부분을 분리하고 변하는 부분을 외부에서 전달 받으면, 메서드의 재사용성을 높일 수 있습니다. 

 

익명 클래스 활용 - 2

[리팩토링 전]

import java.util.Random;
public class Ex1Main {
     public static void helloDice() {
         System.out.println("프로그램 시작");
         //코드 조각 시작
         int randomValue = new Random().nextInt(6) + 1;
         System.out.println("주사위 = " + randomValue);
         //코드 조각 종료
         System.out.println("프로그램 종료");
     }
     
     public static void helloSum() {
         System.out.println("프로그램 시작");
         //코드 조각 시작
         for (int i = 1; i <= 3; i++) {
         	System.out.println("i = " + i);
         }
         //코드 조각 종료
         
         System.out.println("프로그램 종료");
     }
     
     public static void main(String[] args) {
         helloDice();
         helloSum();
     }
}

/*
[실행 결과]

프로그램 시작
주사위 = 5 //랜덤
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료
*/

위 코드를 하나의 메서드에서 실행할 수 있도록 리팩토링 해보겠습니다.

[리팩토링 후]

public interface Process {
   void run();
}
package nested.anonymous.ex;

import java.util.Random;

//정적 중첩 클래스 사용
public class Ex1RefMainV1 {
 public static void hello(Process process) {
     System.out.println("프로그램 시작");
     //코드 조각 시작
     process.run();
     //코드 조각 종료
     System.out.println("프로그램 종료");
 }
 
 static class Dice implements Process {
     @Override
     public void run() {
         int randomValue = new Random().nextInt(6) + 1;
         System.out.println("주사위 = " + randomValue);
     }
 }
 
 static class Sum implements Process {
     @Override
     public void run() {
         for (int i = 1; i <= 3; i++) {
             System.out.println("i = " + i);
         }
     }
 }
 
 public static void main(String[] args) {
     Process dice = new Dice();
     Process sum = new Sum();
     System.out.println("Hello 실행");
     hello(dice);
     hello(sum);
 }
}

여기서는 단순히 데이터를 전달하는 수준을 넘어서 코드 조각을 전달해야 합니다.

리팩토링 전 코드를 살펴보면 helloDice(), helloSum() 메서드들은 아래와 같은 부분만 다르고 나머지 코드는 전부 일치합니다.

//코드 조각 시작
 int randomValue = new Random().nextInt(6) + 1;
 System.out.println("주사위 = " + randomValue);
 //코드 조각 종료
 
 //코드 조각 시작
 for (int i = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 //코드 조각 종료

따라서, 코드 조각을 시작하고 종료하는 부분을 외부에서 전달받아야 합니다. 이것은 단순히 문자열 같은 데이터를 전달받는 것과는 차원이 다른 문제입니다.

 

어떻게 외부에서 코드 조각을 전달할 수 있을까요?

코드 조각은 보통 메서드에 정의합니다. 따라서 코드 조각을 전달하기 위해선 메서드가 필요합니다. 하지만 지금까지 학습한 내용으로는 메서드를 전달할 수 있는 방법이 없습니다. 대신 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 됩니다.

 

이 문제를 해결하기 위해 인터페이스를 정의하고 구현 클래스를 만들었습니다.

public interface Process {
   void run();
}
static class Dice implements Process {
     @Override
     public void run() {
         int randomValue = new Random().nextInt(6) + 1;
         System.out.println("주사위 = " + randomValue);
     }
}

static class Sum implements Process {
     @Override
     public void run() {
         for (int i = 1; i <= 3; i++) {
         	System.out.println("i = " + i);
         }
     }
}
  • Dice, Sum 각각의 클래스는 Process 인터페이스를 구현하고 run() 메서드에 필요한 코드 조각을 구현하였습니다.
  • 여기서는 정적 중첩 클래스를 사용하였습니다. 물론 정적 중첩 클래스가 아니라 외부에 클래스를 직접 만들어도 됩니다.
public static void hello(Process process) {
     System.out.println("프로그램 시작"); //변하지 않는 부분
     //코드 조각 시작
     process.run();
     //코드 조각 종료
     System.out.println("프로그램 종료"); //변하지 않는 부분
}
  • Process process 매개변수를 통해 인스턴스를 전달할 수 있습니다. 이 인스턴스의 run() 메서드를 실행하면 필요한 코드 조각을 실행할 수 있습니다.
  • 이 때 다형성을 활용하여 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행됩니다.
public static void main(String[] args) {
     Process dice = new Dice();
     Process sum = new Sum();
     
     System.out.println("Hello 실행");
     hello(dice);
     hello(sum);
}

/*
[실행 결과]

Hello 실행
프로그램 시작
주사위 = 5 //랜덤
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료
*/
  • hello()를 호출할 때 어떤 인스턴스를 전달하는 가에 따라서 다른 결과가 실행됩니다.
  • hello(dice)를 호출하면 주사위 로직이, hello(sum)을 호출하면 계산 로직이 수행됩니다.

 

익명 클래스 활용 - 3

이번엔 지역 클래스를 사용해서 같은 기능을 구현해보겠습니다.

[지역 클래스 활용]

import java.util.Random;

//지역 클래스 사용
public class Ex1RefMainV2 {

     public static void hello(Process process) {
         System.out.println("프로그램 시작");
         //코드 조각 시작
         process.run();
         //코드 조각 종료
         System.out.println("프로그램 종료");
     }
     
     public static void main(String[] args) {
         class Dice implements Process {
             @Override
             public void run() {
                 int randomValue = new Random().nextInt(6) + 1;
                 System.out.println("주사위 = " + randomValue);
             }
         }

         class Sum implements Process {
             @Override
             public void run() {
                 for (int i = 1; i <= 3; i++) {
                    System.out.println("i = " + i);
                 }
             }
         }

         Process dice = new Dice();
         Process sum = new Sum();
         System.out.println("Hello 실행");
         hello(dice);
         hello(sum);
 }
}

익명 클래스 활용 - 2는 정적 중첩 클래스를 사용한 코드이고, 위 코드는 지역 클래스를 활용한 예시입니다. 코드의 결과 값은 동일합니다.

 

[익명 클래스 사용1]

앞의 지역 클래스는 인스턴스를 간단히 한번만 생성해서 사용합니다. 이런 경우 익명 클래스로 변경할 수 있습니다.

import java.util.Random;

//익명 클래스 사용
public class Ex1RefMainV3 {

 public static void hello(Process process) {
     System.out.println("프로그램 시작");
     //코드 조각 시작
     process.run();
     //코드 조각 종료
     System.out.println("프로그램 종료");
 }
 
 public static void main(String[] args) {
     Process dice = new Process() {
         @Override
         public void run() {
             int randomValue = new Random().nextInt(6) + 1;
             System.out.println("주사위 = " + randomValue);
         }
     };
     
     Process sum = new Process() {
         @Override
         public void run() {
             for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
             }
         }
     };
     
     System.out.println("Hello 실행");
     hello(dice);
     hello(sum);
   }
}

 

[익명 클래스 사용2 - 참조값 직접 전달]

이 경우 익명 클래스의 참조값을 변수에 담아둘 필요 없이, 인수로 바로 전달할 수 있습니다.

import java.util.Random;

//익명 클래스 참조 바로 전달
public class Ex1RefMainV4 {

 public static void hello(Process process) {
     System.out.println("프로그램 시작");
     //코드 조각 시작
     process.run();
     //코드 조각 종료
     System.out.println("프로그램 종료");
 }
 
 public static void main(String[] args) {
     hello(new Process() {
         @Override
         public void run() {
             int randomValue = new Random().nextInt(6) + 1;
             System.out.println("주사위 = " + randomValue);
         }
     });
     
     hello(new Process() {
         @Override
         public void run() {
             for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
             }
         }
     });
  }
}

람다(lambda)

자바8 이전까지 메서드에 인수로 전달할 수 있는 것은 크게 2가지 였습니다.

  • int, double과 같은 기본형 타입
  • Process Member와 같은 참조형 타입(인스턴스)

결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나, 인스턴스의 참조입니다.

 

지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 꼭 생성해서 전달해야 할까요?

생각해보면 클래스나 인스턴스와 관계 없이 다음과 같이 메서드만 전달할 수 있다면 더 간단할 것 같습니다.

public void runDice() {
 int randomValue = new Random().nextInt(6) + 1;
 System.out.println("주사위 = " + randomValue);
}

public void runSum() {
 for (int i = 1; i <= 3; i++) {
 	System.out.println("i = " + i);
 }
}


hello(메서드 전달: runDice())
hello(메서드 전달: runRun())

자바 8에 들어서면서 큰 변화가 있었는데 바로 메서드(더 정확히는 함수)를 인수로 전달할 수 있게 되었습니다. 이것을 간단히 람다(Lambda)라 합니다.

 

리팩토링 - 람다

import java.util.Random;

//람다 사용
public class Ex1RefMainV5 {
     public static void hello(Process process) {
         System.out.println("프로그램 시작");
         //코드 조각 시작
         process.run();
         //코드 조각 종료
         System.out.println("프로그램 종료");
     }
     
     public static void main(String[] args) {
         hello(() -> {
             int randomValue = new Random().nextInt(6) + 1;
             System.out.println("주사위 = " + randomValue);
         });
         
         hello(() -> {
             for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
             }
         });
     }
}
  • 코드를 보면 클래스나 인스턴스를 정의하지 않고, 메서드(더 정확히는 함수)의 코드 블럭을 직접 전달하는 것을 확인할 수 있습니다.

관련 포스팅

1. [JAVA]중첩/내부 클래스 (1/4)

2. [JAVA]내부 클래스 (2/4)

3. [JAVA]지역 클래스 (3/4)

4. [JAVA]익명 클래스 (4/4)