본문 바로가기
Course/Java

[java-mid1] 8. 중첩 클래스, 내부 클래스2

by Lpromotion 2025. 3. 28.
"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.

목차
 

1. 지역 클래스 - 시작

지역 클래스(Local class)는 내부 클래스의 특별한 종류 중 하나이다. 내부 클래스의 특징을 그대로 가진다.

따라서 지역 클래스도 내부 클래스와 마찬가지로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.

지역 클래스는 일반 클래스와 달리 지역 변수처럼 코드 블럭 안에서 정의된다.

 

지역 클래스의 특징

  • 지역 클래스는 지역 변수처럼 특정 코드 블럭(메서드나 생성자 내) 안에서 선언된다.
  • 지역 클래스는 자신이 선언된 코드 블럭의 지역 변수에 접근할 수 있다.
  • 바깥 클래스의 인스턴스 멤버(필드, 메서드)에 접근할 수 있다.
  • 지역 클래스는 지역 변수와 마찬가지로 접근 제어자(public, private 등)를 사용할 수 없다.

 

지역 클래스 기본 예시 코드

지역 클래스의 기본 형태는 다음과 같다.

class Outer {
    public void process() {
        int localVar = 0; // 지역 변수 선언

        class Local { 
            // 지역 클래스 정의
        }

        Local local = new Local(); // 지역 클래스 인스턴스 생성
    }
}

 

지역 클래스 사용 예제1

public class LocalOuterV1 {
    private int outInstanceVar = 3; // 바깥 클래스의 인스턴스 변수

    public void process(int paramVar) { // 매개변수(지역 변수의 한 종류)
        int localVar = 1; // 지역 변수 선언

        class LocalPrinter { // 지역 클래스 정의
            int value = 0; // 지역 클래스의 인스턴스 변수

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

        LocalPrinter printer = new LocalPrinter(); // 인스턴스 생성
        printer.printData(); // 메서드 호출
    }

    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2); // 메서드 호출 및 실행
    }
}

 

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3

 

지역 클래스의 접근 범위

  • 자신의 인스턴스 변수(value)에 접근할 수 있다.
  • 자신이 속한 메서드의 지역 변수(localVar)와 매개변수(paramVar)에 접근할 수 있다. 매개변수도 지역 변수의 한 종류이다.
  • 바깥 클래스의 인스턴스 변수(outInstanceVar)에 접근할 수 있다.
  • 지역 클래스는 지역 변수와 마찬가지로 접근 제어자를 사용할 수 없다.

 

지역 클래스 예제2 - 인터페이스 구현

지역 클래스는 일반 클래스와 마찬가지로 인터페이스를 구현하거나 부모 클래스를 상속할 수 있다.

아래는 지역 클래스가 인터페이스를 구현하는 예제이다.

 

인터페이스 정의

package nested.local;

public interface Printer {
    void print();
}

 

지역 클래스에서 인터페이스 구현

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 localOuter = new LocalOuterV2();
        localOuter.process(2);
    }
}

이 예제에서는 지역 클래스가 `Printer` 인터페이스를 구현했다.

인터페이스 구현 외에 이전 예제(`LocalOuterV1`)와 구조상 차이가 없다.

 

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3

위와 같이, 지역 클래스는 일반 클래스와 동일하게 인터페이스를 구현하거나 부모 클래스를 상속할 수 있다.

 

 

2. 지역 클래스 - 지역 변수와 생명 주기

지역 클래스에서 사용하는 지역 변수는 특별한 방식으로 처리된다. 이것을 지역 변수 캡처(Local Variable Capture) 라고 한다.

지역 변수 캡처를 이해하기 전에, 자바 변수들의 생명 주기를 먼저 살펴보자.

 

변수의 생명 주기

자바 변수들은 종류에 따라 생존 기간이 다르다.

  • 클래스 변수 (static 변수): 프로그램 종료까지 생존한다. (메서드 영역에 존재)
  • 인스턴스 변수: 인스턴스가 생성되어 존재하는 동안 생존한다. (힙 영역에 존재)
  • 지역 변수: 메서드 호출 시 생성되고 메서드 호출이 끝나면 제거된다. (스택 영역의 스택 프레임에 존재)
    • 매개변수도 지역 변수의 일종이다.

 

지역 클래스 예제3 - 생명 주기

다음 예제는 지역 클래스 인스턴스의 생존 기간이 지역 변수보다 긴 경우를 보여준다.

public class LocalOuterV3 {
    private int outInstanceVar = 3;

    public Printer 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();
        return printer; // Printer 인스턴스를 반환
    }

    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);
        printer.print(); // process() 메서드 종료 후 print() 호출
    }
}
  • `LocalPrinter` 인스턴스를 반환하여, 메서드 호출이 끝난 후에도 지역 클래스의 인스턴스가 생존하도록 했다.
    • `LocalPrinter.print()` 메서드를 `process()` 안에서 실행하는 것이 아니라 `process()` 메서드가 종료된 이후에 `main()` 메서드에서 실행한다.

 

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3

언뜻 보면 이상하다. 지역 변수(`localVar`, `paramVar`)는 메서드가 종료될 때 함께 제거되는데, 어떻게 이 값들에 접근할 수 있는지 의문이 생긴다.

 

LocalPrinter 인스턴스 생성 직후 메모리 그림

 

지역 클래스 인스턴스의 생존 범위

  • 지역 클래스로 만든 객체도 인스턴스이기 때문에 힙 영역에 존재한다. 따라서 GC 전까지 생존한다.
    • `LocalPrinter` 인스턴스는 `process()` 메서드 안에서 생성된다. 그리고 `process()` 에서 `main()` 으로 생성한 `LocalPrinter` 인스턴스를 반환하고 `printer` 변수에 참조를 보관한다. 따라서 `LocalPrinter` 인스턴스는 `main()` 이 종료될 때 까지 생존한다.
  • `paramVar` , `localVar` 와 같은 지역 변수는 `process()` 메서드를 실행하는 동안에만 스택 영역에서 생존한다.
    `process()` 메서드가 종료되면 `process()` 스택 프레임이 스택 영역에서 제거 되면서 함께 제거된다.

 

즉, 메서드 종료 이후에도 지역 클래스의 인스턴스가 지역 변수의 값을 사용하려면, 어떻게든 지역 변수의 값을 메서드 종료 후에도 유지할 수 있는 방법이 필요하다.

이러한 생존 기간 차이 때문에, 단순히 생각하면 지역 클래스 인스턴스는 이미 제거된 지역 변수에 접근할 수 없어야 한다. 하지만 실행 결과를 보면 여전히 접근이 가능하다.

이러한 문제를 자바는 지역 변수 캡처라는 개념을 통해 해결한다.

 

참고: 스택 프레임 구조

 

 

 

3. 지역 클래스 - 지역 변수 캡처의 개념과 동작 과정

지역 클래스의 인스턴스는 힙(heap) 영역에 존재하며 가비지 컬렉션(GC)이 되기 전까지 살아있다. 그러나 지역 클래스가 접근하는 지역 변수는 메서드 호출이 종료되는 순간 스택(stack) 영역에서 제거된다.

따라서 지역 클래스의 인스턴스가 메서드가 종료된 후에도 지역 변수의 값에 접근할 수 있도록 하려면, 지역 변수의 값을 별도로 저장하는 방법이 필요하다. 자바는 이를 해결하기 위해 지역 클래스의 인스턴스를 생성할 때 접근이 필요한 지역 변수의 값을 복사하여 인스턴스 내부에 저장한다.

이러한 과정을 지역 변수 캡처(Local Variable Capture)라고 한다.

 

지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정

 

  1. 지역 클래스 인스턴스 생성 시 접근할 지역 변수 확인
    지역 클래스의 인스턴스를 생성할 때, 해당 지역 클래스가 사용하는 지역 변수를 확인한다.
    (LocalPrinter 클래스는 paramVar, localVar 지역 변수에 접근한다.)
  2. 지역 변수의 값 복사(캡처)
    지역 클래스가 사용하는 지역 변수의 값을 복사한다. (매개변수도 지역 변수의 일종이다.)
    여기서는 지역 변수인 paramVar, localVar의 값을 복사한다.
  3. 캡처한 지역 변수를 인스턴스 내부에 저장
    복사한 지역 변수의 값을 생성 중인 지역 클래스의 인스턴스 내부에 별도로 저장한다.
  4. 지역 클래스 인스턴스 생성 완료
    복사된 지역 변수 값을 포함한 지역 클래스의 인스턴스가 생성 완료된다.
    이제 지역 클래스의 인스턴스는 캡처된 지역 변수 값을 통해 지속적으로 지역 변수에 접근할 수 있다.

 

 

캡처 변수 접근 방식

  • `LocalPrinter` 인스턴스가 `print()` 메서드를 통해 `paramVar`, `localVar`에 접근할 때, 실제로는 스택 영역의 원본 지역 변수에 접근하는 것이 아니다.
    대신, 인스턴스 내부에 별도로 저장된 캡처된 지역 변수 값에 접근한다.
  • 이 캡처된 지역 변수의 생명 주기는 지역 클래스의 인스턴스(`LocalPrinter`)와 동일하다.
    따라서 지역 클래스의 인스턴스는 지역 변수의 원본 생명 주기와 무관하게, 언제든지 캡처된 지역 변수 값을 통해 안정적으로 접근할 수 있다.
  • 자바는 이런 방식을 통해 지역 클래스 인스턴스와 지역 변수 간 생명 주기가 서로 다른 문제를 해결한다.

 

코드로 캡처 변수 확인

LocalOuterV3 - 추가

public static void main(String[] args) {
    LocalOuterV4 localOuter = new LocalOuterV4();
    Printer printer = localOuter.process(2);
    // printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
    printer.print();

    // 추가
    System.out.println("필드 확인");
    Field[] fields = printer.getClass().getDeclaredFields();
    for (Field field : fields) {
        System.out.println("field = " + field);
    }
}

 

실행 결과

필드 확인

//인스턴스 변수
field = int nested.local.LocalOuterV4$1LocalPrinter.value

//캡처 변수
field = final int nested.local.LocalOuterV4$1LocalPrinter.val$localVar
field = final int nested.local.LocalOuterV4$1LocalPrinter.val$paramVar

//바깥 클래스 참조
field = final nested.local.LocalOuterV4 nested.local.LocalOuterV4$1LocalPrinter.this$0

실행 결과를 통해 `LocalPrinter` 클래스의 캡처 변수를 확인할 수 있다.

추가로 바깥 클래스를 참조하기 위한 필드도 확인할 수 있다. 참고로 이런 필드들은 자바가 내부에서 만들어 사용하는 필드들이다.

 

 

4. 지역 클래스 - 캡처 변수의 제약과 final

캡처 변수의 값 변경 불가

지역 클래스는 지역 변수의 값을 캡처하여 사용하는데, 캡처된 지역 변수는 중간에 값을 변경할 수 없다.
즉, 지역 클래스가 접근하는 지역 변수는 반드시 final이거나 사실상 final(effectively final)이어야 한다.

자바에서 이러한 제약 조건을 만든 이유는, 메서드가 종료된 이후 스택 영역에 존재하는 원본 지역 변수가 제거되었을 때, 지역 클래스 내부에 저장된 캡처된 값과 원본 지역 변수의 값이 달라지는 동기화 문제를 방지하기 위함이다.

 

final 또는 effectively final 예제

public class LocalOuterV4 {

    private int outInstanceVar = 3;

    public Printer 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);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        // 만약 localVar의 값을 변경한다면? 다시 캡쳐해야 하나?
        // localVar = 10; // 컴파일 오류
        // paramVar = 20; // 컴파일 오류
        return printer;
    }

    public static void main(String[] args) {
        LocalOuterV4 localOuter = new LocalOuterV4();
        Printer printer = localOuter.process(2);
        // printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
        printer.print();
    }
}
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3

 

  • ` Printer printer = new LocalPrinter(); `: `LocalPrinter` 를 생성하는 시점에 지역 변수인 `localVar` , `paramVar` 를 캡처한다.

 

final 제약의 이유 - 동기화 문제 방지

지역 클래스는 지역 변수의 값을 복사(캡처)하여 인스턴스 내부에 저장한다. 이후 메서드 호출이 끝나 원본 지역 변수가 스택에서 제거되어도 지역 클래스 인스턴스가 계속해서 지역 변수의 값을 사용할 수 있도록 하기 위해서이다.

하지만 여기서 중요한 문제가 있다. 만약 지역 변수의 값을 중간에 변경할 수 있다면, 원본 지역 변수의 값과 캡처된 변수의 값 사이에 불일치가 발생한다. 이렇게 값이 달라지는 현상을 동기화 문제라고 한다.

  • 스택 영역의 지역 변수 값을 변경하면, 인스턴스 내부의 캡처된 값과 일치하지 않게 된다.
  • 반대로 인스턴스 내부의 캡처 변수 값을 변경하면, 원본 지역 변수 값과 차이가 생긴다.

이러한 동기화 문제는 코드가 예측하지 못한 방식으로 동작하게 만들고, 디버깅을 어렵게 하며, 특히 멀티쓰레드 환경에서는 안정성과 성능에 심각한 문제를 유발할 수 있다.

 

캡처 변수의 값을 변경하지 못하는 이유

  • 지역 변수와 인스턴스 내부에 캡처된 값의 동기화를 유지하려면, 값이 변경될 때마다 두 값을 서로 일치시키는 작업을 해야 한다.
  • 원본 변수와 캡처 변수는 위치가 서로 다르다. 스택 영역과 힙 영역에 각각 존재하므로, 이 둘 사이의 동기화는 매우 어렵고 복잡한 작업이 된다.
  • 특히 멀티쓰레드 상황에서는 동기화 처리가 매우 복잡하며, 성능 저하의 원인이 될 수 있다. (멀티쓰레드 학습 시 보다 명확히 이해 가능)
  • 또한, 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경되는 현상이 발생해 코드의 유지보수가 어렵고, 예측 가능성을 떨어뜨린다.

이러한 이유로 자바는 처음부터 캡처되는 지역 변수를 반드시 final이거나 사실상 final(effectively final)로 제한하여, 동기화 문제를 원천적으로 방지하고 있다.

 

명시적 final vs 사실상 final(effectively final)

자바 8부터는 지역 변수가 명시적으로 final 키워드를 붙이지 않더라도, 값이 단 한 번도 바뀌지 않았다면 자동으로 사실상 final(effectively final)로 취급한다.

아래 두 선언은 동일한 의미이다.

final int localVar = 1; // 명시적 final 선언
int localVar = 1;       // 사실상 final (값 변경이 없으면)

 

컴파일러의 지역 변수 검사

자바 컴파일러는 지역 클래스가 접근하는 지역 변수를 자동으로 검사하여, 값이 중간에 변경된 지역 변수는 캡처할 수 없도록 제한한다.

값을 변경하면 다음과 같은 컴파일 오류가 발생한다.

localVar = 10; // 컴파일 오류 발생
paramVar = 20; // 컴파일 오류 발생

오류 메시지는 다음과 같다.

Variable 'localVar' is accessed from within inner class, needs to be final or effectively final
Variable 'paramVar' is accessed from within inner class, needs to be final or effectively final

 

정리

  • 지역 클래스가 접근하는 지역 변수는 반드시 final 또는 사실상 final이어야 한다.
  • 지역 클래스의 캡처 변수 값을 변경하지 못하는 이유는, 지역 변수와 캡처된 값 간에 발생할 수 있는 동기화 문제를 원천적으로 차단하기 위해서이다.
  • 자바는 이를 통해 지역 클래스가 안전하게 지역 변수를 사용할 수 있도록 보장한다.

 

 

5. 익명 클래스 - 시작

익명 클래스란?

익명 클래스(anonymous class)는 이름이 없는 지역 클래스로, 지역 클래스의 특별한 형태이다.

  • 내부 클래스의 일종이며, 클래스 이름 없이 선언과 동시에 인스턴스를 생성한다.
  • 한 번만 사용할 간단한 구현체를 만들 때 사용하면 유용하다.

 

지역 클래스 사용 예 (기존 방식)

// 선언
class LocalPrinter implements Printer {
    // body
}

// 생성
Printer printer = new LocalPrinter();

 

익명 클래스 사용 예 (선언 + 생성 동시에)

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

 

익명 클래스 예제

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}

  • `new` 다음에 바로 상속 받으면서 구현할 부모 타입을 입력한다.
  • `Printer`라는 이름의 인터페이스를 구현한 익명클래스를 생성하는 것이다.
    • 인터페이스를 생성하는 것이 아니다. 자바에서 인터페이스를 생성하는 것은 불가능하다.
  • `{body}` 부분에 `Printer` 인터페이스를 구현할 코드(본문)를 작성하면 된다.

 

익명 클래스의 특징

  • 지역 클래스처럼 내부에서 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
  • 부모 클래스를 상속받거나 또는 인터페이스를 구현해야 한다.
    • 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요하다.
  • 클래스 이름이 없으므로 생성자를 선언할 수 없다. (기본 생성자만 가능)
  • 자바 내부에서 `바깥 클래스 이름 + $` + 숫자`로 정의된다.
    • 익명 클래스가 여러 개면 숫자가 증가하면서 구분된다.
  • 단 한 번만 인스턴스를 생성할 수 있다.
    • 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용한다.

 

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

 

 

6. 익명 클래스 활용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
프로그램 종료

 

두 메서드는 중간 출력 문자열만 다르고, 나머지는 동일하다.
이처럼 중복된 코드가 보인다면, 변하는 부분과 변하지 않는 부분을 분리해서 리팩토링해야 한다.

 

리팩토링 과정

1단계 - 변하는 부분과 변하지 않는 부분 구분

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("프로그램 종료"); // 변하지 않는 부분
}

여기서 공통되는 "프로그램 시작"과 "프로그램 종료" 부분은 고정이고, "Hello Java", "Hello Spring"은 상황에 따라 달라지는 부분이다.

 

2단계 - 통합 메서드 설계

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

변하지 않는 부분은 함수 내부에 두고, 변하는 출력 문자열은 매개변수로 전달받도록 설계한다.

실행 결과는 동일하다.

 

  • 리팩토링의 핵심은 변하지 않는 부분은 함수 내부에 고정하고, 변하는 부분은 외부에서 전달받는 것이다.
  • 이 구조를 통해 메서드의 재사용성을 높일 수 있다.

 

 

7. 익명 클래스 활용2 - 코드 조각 전달

앞서 "Hello Java", "Hello Spring"과 같은 단순 문자열 데이터를 전달받아 출력하는 형태로 리팩토링하였다.
하지만 이번에는 출력할 데이터가 아니라 실행할 코드 자체가 바뀌는 상황을 다룬다.

 

리팩토링 전

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 = 0; i < 3; i++) {
            System.out.println("i = " + i);
        }
        // 코드 조각 종료

        System.out.println("프로그램 종료"); // 변하지 않는 부분
    }

    public static void main(String[] args) {
        helloDice();
        helloSum();
    }
}

 

실행 결과

프로그램 시작
주사위 = 1
프로그램 종료
프로그램 시작
i = 0
i = 1
i = 2
프로그램 종료

 

문제점

  • `helloJava()`와 `helloSpring()`은 문자열만 다르고 구조는 동일하므로, 앞선 예제처럼 문자열을 전달하면 해결 가능하다.
  • 하지만 `helloDice()`는 문자열이 아닌 코드 자체(주사위 계산)가 다르기 때문에 단순 문자열 전달 방식으로 해결할 수 없다.

 

코드 조각을 전달하는 구조로 리팩토링

코드 조각은 보통 메서드(함수)에 정의되며, 코드 조각을 전달하기 위해서는 메서드가 필요하다.

메서드를 전달하는 대신에 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 된다.

따라서 코드를 담을 인터페이스와 그 코드 조각을 실행해줄 공통 메서드 구조가 필요하다.

 

1. 코드 조각 인터페이스 정의

public interface Process {
    void run();
}

 

2. 공통 메서드 & 정적 중첩 클래스 사용

public class Ex1RefMainV1 {
    
    // 공통 메서드 구조
    public static void hello(Process process) {
        System.out.println("프로그램 시작"); // 변하지 않는 부분

        // 코드 조각 시작
        process.run();
        // 코드 조각 종료

        System.out.println("프로그램 종료"); // 변하지 않는 부분
    }

    // Process 구현 클래스 정의 (정적 중첩 클래스)
    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 = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        }
    }

    public static void main(String[] args) {
        Dice dice = new Dice();
        Sum sum = new Sum();

        System.out.println("Hello 실행");
        hello(dice); // new Dice() 바로 넣어도 됨
        hello(sum);
    }
}

 

hello 메서드 (공통 메서드 구조)

  • `Process process` 매개변수를 통해 인스턴스를 전달할 수 있다.
  • 이 인스턴스의 `run()` 메서드를 실행하면 필요한 코드 조각을 실행할 수 있다.
  • 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행된다.

 

Process 구현 클래스 (지역 클래스)

  • `Dice` , `Sum` 각각의 클래스는 `Process` 인터페이스를 구현하고 `run()` 메서드에 필요한 코드 조각을 구현한다.
  • 정적 중첩 클래스를 사용했지만, 외부에 클래스를 직접 만들어도 된다.

 

main 메서드

  • `hello()` 를 호출할 때 어떤 인스턴스를 전달하는 가에 따라서 다른 결과가 실행된다.
  • `hello(dice)` 를 호출하면 주사위 로직이, `hello(sum)` 을 호출하면 계산 로직이 수행된다.

 

실행 결과

Hello 실행
프로그램 시작
주사위 = 5
프로그램 종료
프로그램 시작
i = 0
i = 1
i = 2
프로그램 종료

주사위 값은 매 실행마다 랜덤

 

정리

  • 단순 문자열 전달만으로는 코드 로직이 달라지는 경우 대응할 수 없다.
  • 이럴 땐 인터페이스를 활용해 코드 조각 자체를 전달하는 구조가 필요하다.
  • `hello()` 는 코드 실행 전후 공통 로직을 유지하면서, 다양한 동작을 주입받아 실행할 수 있다.
  • 이는 이후 익명 클래스, 람다 표현식, 전략 패턴, 콜백 구조 등으로 확장되는 매우 중요한 개념이다.

 

 

8. 익명 클래스 활용3 - 다양한 방식의 구현 비교

앞에서 코드 조각을 Process 인터페이스의 run() 메서드에 담아 전달하는 구조를 설계하였다.
이제 동일한 목적을 다양한 방식으로 구현할 수 있는 방법들을 비교해본다.

 

1. 클래스 직접 구현

별도의 클래스 정의 후 사용

public class Dice implements Process {
    public void run() {
        int value = new Random().nextInt(6) + 1;
        System.out.println("주사위 = " + value);
    }
}
public static void main(String[] args) {
    hello(new Dice());
}
  • 장점: 재사용 가능, 여러 곳에서 반복 사용 가능
  • 단점: 단 한 번만 사용할 목적이라면 클래스 이름을 만들고 파일을 분리해야 하므로 번거로움

 

2. 지역 클래스 사용

메서드 내부에서만 사용할 목적이라면 지역 클래스가 더 적합하다

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

    hello(dice);
}
  • 장점: 해당 메서드 내에서만 클래스가 존재하므로 외부로 노출되지 않음
  • 단점: 여전히 클래스 이름이 필요함

 

3. 익명 클래스 사용 (변수에 담기)

클래스 이름 없이 바로 구현

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);
        }
    };
    
    hello(dice);
}
  • 장점: 이름 없는 클래스 정의 → 코드 간결
  • 단점: 인스턴스를 재사용해야 할 경우 변수로 분리 필요

 

4. 익명 클래스 사용 (참조값 직접 전달)

익명 클래스 인스턴스를 직접 hello() 메서드의 인수로 전달

public static void main(String[] args) {
    hello(new Process() {
        @Override
        public void run() {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        }
    });
}
  • 장점: 인스턴스를 재사용하지 않는다면 가장 간결하고 효율적
  • 단점: 코드 블록이 길어질 경우 가독성 저하 가능

 

5. 람다(lambda) 사용

자바 8 이전 & 이후

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

  • int , double 과 같은 기본형 타입 (간단한 데이터)
  • Process Member 와 같은 참조형 타입 (인스턴스의 참조)

즉, 메서드 자체를 전달하는 것은 불가능했기 때문에, 메서드처럼 동작하는 코드를 전달하려면
인터페이스를 구현한 클래스 또는 익명 클래스를 생성하여 인스턴스를 전달해야 했다.

 

자바 8부터는 메서드(함수)를 인수로 직접 전달하는 기능이 도입되었고, 이를 간결하게 표현하는 방법이 람다(lambda)이다.

람다를 사용하면 위와 같은 코드를 훨씬 짧고 간단하게 작성할 수 있다.

 

람다 적용

public static void main(String[] args) {
    hello(() -> {
        int randomValue = new Random().nextInt(6) + 1;
        System.out.println("주사위 = " + randomValue);
    });
}

`run()` 메서드의 구현을 `(매개변수) -> { 실행문 }` 형태의 람다 표현식으로 간결하게 작성할 수 있으며,
이를 통해 코드 블록 자체를 값처럼 전달할 수 있다.

  • 장점: 불필요한 클래스 선언 없이 기능만 빠르게 전달할 수 있고,
    스트림, 정렬, 이벤트 처리 등 다양한 상황에서 유용하게 활용된다.
  • 단점: 디버깅이 어렵고, `this` 키워드의 의미가 기존과 다르게 동작한다.

 

예전에는 람다가 없어서 항상 익명 클래스로 만들어 전달함
익명 클래스 또한 사용함 -> 멤버 변수를 사용할 수 있기 때문
=> 람다와 익명 클래스는 용도가 다름

 

 

반응형

댓글