관리 메뉴

나구리의 개발공부기록

중첩 클래스와 내부클래스, 지역 클래스(시작, 지역 변수 캡처), 익명 클래스(시작, 활용) 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편

중첩 클래스와 내부클래스, 지역 클래스(시작, 지역 변수 캡처), 익명 클래스(시작, 활용)

소소한나구리 2025. 1. 23. 13:46

출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 지역 클래스

1) 시작

(1) 지역 클래스

  • 지역 클래스는 내부 클래스의 특별한 종류의 하나이므로 내부 클래스의 특징을 그대로 가지기 때문에 바깥 클래스의 인스턴스 멤버에 접근할 수 있음
  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에 클래스를 선언하며 지역 변수(바깥 클래스의 메서드에 선언된 변수)에 접근할 수 있음

(2) LocalOuterV1

  • 지역 클래스는 자신의 인스턴스 변수, 자신이 속한 코드블럭의 지역 변수(매개변수 포함), 바깥 클래스의 인스턴스 멤버에 모두 접근할 수 있음
  • 매개변수도 지역 변수의 한 종류이고 지역 클래스도 내부 클래스의 한 종류이기 때문에 모두 접근이 가능하여 출력이 되는 것을 확인할 수 있음
  • 지역 클래스는 지역 변수처럼 접근 제어자는 사용할 수 없음
package nested.local;

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
*/

 

(3) Printer, LocalOuterV2 - 중첩, 내부 클래스들의 상속

  • 내부 클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나 부모 클래스를 상속할 수 있음
  • LocalOuterV2의 지역 클래스인 LocalPrinter에서 생성한 Printer 인터페이스를 구현해보면 정상적으로 구현이 되는 것을 확인할 수 있음
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);
    }
}

2) 지역 변수 캡처

** 참고

  • 지금부터 설명할 지역 변수 캡처에 관한 내용은 너무 깊이있게 이해하지 않아도 됨
  • 어렵다면 단순하게 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다 정도로 이해해도 충분함
  • 여기에서 자세히 다루는 이유는 이 내용에 대해 다른 강의나 책에서 깊게 다루지 않고 지역 클래스를 사용하다보면 드는 의문점을 해결하기 위함임

(1) 변수의 생명 주기 - 복습

  • 클래스 변수: 메서드 영역에 존재하고 자바가 클래스 정보를 읽어 들이는 순간부터 프로그램 종료까지 존재하며 생명주기가 가장 긺
  • 인스턴스 변수: 힙 영역에 존재하고 본인이 소속된 인스턴스가 GC되기 전까지 존재하며 생존주기가 긴 편임
  • 지역 변수
    - 스택 영역의 스택 프레임안에 존재하며 메서드가 호출되면 생성되고 메서드 호출이 종료되면 스택 프레임이 제거되면서 그안에 있는 지역 변수도 모두 제거됨
    - 생존 주기가 매우 짧으며 매개변수도 지역 변수의 한 종류임

(2-1) LocalOuterV3

  • 기존의 LocalOuterV2를 살짝 수정하여 LocalOuterV3의 메서드인 process()의 반환값을 Printer 인터페이스로 변경하여 LocalPrinter의 print()메서드를 출력하지 않고 반환함
  • 실행을 위한 main() 메서드에서 LocalOuterV3를 생성하고 process()메서드를 호출하여 결과를 반환한 뒤 그 결과로 print()메서드를 호출하도록 변경
  • 실행해보면 기존과 동일하게 정상적으로 출력이 되지만, 각 변수와 메서드의 생명주기를 생각해보면 이상한 점을 느낄 수 있음
package nested.local;

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();
//        printer.print(); // 여기서 실행하지 않고 Printer 인스턴스만 반환
        return printer;
    }

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

 

(2-2) LocalPrinter 인스턴스 생성 직후 메모리, 지역 클래스 인스턴스의 생존 범위

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

(2-3) LocalPrinter.print() 접근 메모리 그림

  • LocalPrinter 인스턴스는 print() 메서드를 통해 힙 영역에 존재하는 바깥 인스턴스의 변수인 outInstanceVar에 접근하는데, 이부분은 인스턴스의 필드를 참조하는 것이기 때문에 문제가 없음
  • print()메서드에서는 스택 영역에 존재하는 지역 변수도 접근하는 것 처럼 보이는데, 스택 영역에 존재하는 지역 변수를 힙 영역에 있는 인스턴스가 접근하는 것은 생각처럼 단순하지 않음

(2-4) process() 메서드의 종료

  • 지역 변수의 생명주기는 스택 프레임이 제거되면 모두 제거되는 반면 인스턴스의 생명주기는 GC 전까지 생존할 수 있음
  • 지역 변수인 paramVar, localVar는 process() 메서드가 실행되는 동안에만 생존할 수 있으며 process() 메서드가 종료되면 process()의 스택 프레임이 제거되면서 두 지역 변수도 함게 제거됨
  • 여기서 문제는 process() 메서드가 종료되어도 LocalPrinter 인스턴스는 계속 생존할 수 있다는 점임

(2-5) process() 메서드가 종료된 이후에 지역 변수 접근

  • 예제에서의 흐름을 보면 process() 메서드가 종료된 이후에 main() 메서드 안에서 LocalPrinter 인스턴스에 있는 print() 메서드를 호출하는데, print() 메서드는 지역 변수인 paramVar, localVar에 접근해야 함
  • 그러나 process() 메서드가 이미 종료되었으므로 해당 지역 변수들도 이미 제거된 상태임
  • 그런데 실행 결과를 보면 localVar, paramVar와 같은 지역 변수의 값들이 모두 정상적으로 출력되고 있음

** 참고

  • 여기서는 이해를 돕기 위해 설명을 단순화 했지만 조금 더 정확하게 하면 LocalPrinter.print() 메서드를 실행하면 이 메서드도 스택 프레임에 올라가서 실행됨
  • main()메서드에서 print()메서드를 실행했으므로 main() 스택 프레임 위에 print() 스택 프레임이 올라가서 실행됨
  • 물론 process() 스택 프레임은 이미 제거된 상태이기 때문에 지역 변수인 localVar, paramVar도 함께 제거되어서 접근할 수 없는 것은 동일함

(3-1) 지역 변수 캡처

  • 자바는 위의 예제에서 발생했던 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 넣어두는데 이런 과정을 변수 캡처(Capture)라고 함
  • 스크린 캡처를 떠올려보면 바로 이해가 될 것이며 인스턴스를 생성할 때 필요한 지역 변수를 복사하여 보관해 두는 것임
  • 모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역 변수만 캡처함
  • 즉, 위 예제에서 문제없이 이미 삭제된 지역변수인 localVar, paramVar의 값을 출력할 수 있던 것은 스택 영역에 있는 지역 변수에 접근한 것이 아니라 캡처한 변수에 접근한 것임

(3-2) 지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정

  • 1. LocalPrinter 인스턴스 생성 시도: 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인
  • 2. 사용하는 지역 변수 복사: 지역 클래스가 사용하는 지역 변수를 복사(매개변수도 포함)
  • 3. 지역 변수 복사 완료: 복사한 지역 변수를 인스턴스에 포함함
  • 4. 인스턴스 생성 완료: 복사한 지역 변수를 포함해서 인스턴스 생성이 완료되며 복사한 지역 변수를 인스턴스를 통해 접근할 수 있음
  • LocalPrinter 인스턴스에서 print() 메서드를 통해 paramVar, localVar에 접근하면 스택영역에 있는 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 변수에 접근함
  • 캡처한 paramVar, localVar의 생명주기는 LocalPrinter인스턴스의 생명주기와 같으므로 스택 영역의 지역 변수의 생명주기와 무관하게 얼마든지 캡처 변수에 접근할 수 있으며 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명 주기가 다른 문제를 해결함

(3-3) LocalOuterV3 - 추가

  • printer.getClass().getDeclaredFields()를 통해 printer 인스턴스가 가지고 있는 필드들을 모두 출력해보면 인스턴스의 필드인 value와 더불어서 val$localVar, val$paramVar 처럼 캡처 변수가 있는것을 확인할 수 있음
  • 추가적으로 LocalOuterV3$LocalPrinter.this$0도 있는데 이것은 바깥 클래스를 참조하기 위한 필드도 확인할 수 있음
public class LocalOuterV3 {

    // 기존 코드 동일 생략

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

        System.out.println("필드 확인");
        Field[] fields = printer.getClass().getDeclaredFields();
        for (Field field : fields) {
            System.out.println("field = " + field);
        }
    }
}
/* 실행 결과 - 기존 실행결과는 생략
필드 확인
field = int nested.local.LocalOuterV3$1LocalPrinter.value
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$localVar
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$paramVar
field = final nested.local.LocalOuterV3 nested.local.LocalOuterV3$1LocalPrinter.this$0
*/

 

(3-4) 정리

  • 지역 클래스는 인스턴스를 생성할 때 필요한 지역 변수를 먼저 캡처해서 인스턴스에 보관
  • 지역 클래스의 인스턴스를 통해 지역 변수에 접근하면 실제로는 스택 영역의 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 캡처 변수에 접근함

(4-1) 지역 변수는 절대로 중간에 값이 변하면 안됨

  • 지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안됨
  • 즉, final로 선언하거나 사실상 final이여함 이것은 자바 문법이고 규칙임

** 용어 - 사실상 final

  • 영어로 effectively final 이라고하면 사실상 final 지역 변수는 지역 변수에 final 키워드를 사용하지는 않았지만 값을 변경하지 않는 지역 변수를 뜻함
  • final 키워드를 넣지 않았을 뿐이지 final 키워드를 넣은 것 처럼 중간에 값을 변경하지 않은 지역 변수이므로 사실상 final 지역 변수는 final 키워드를 넣어도 동일하게 작동해야 함

(4-2) LocalOuterV4

  • new LocalPrinter()로 LocalPrinter를 생성하는 시점에 지역 변수인 localVar, paramVar를 캡처를 함
  • 그런데 이후에 localVar와 paramVar의 값을 변경하려고하면 컴파일 오류가 발생하는 것을 확인할 수 있음
  • 만약 이것이 허용된다면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생할 텐데 이것을 동기화 문제라 함
  • 물론 자바 언어를 설계할 때 지역 변수의 값이 변경되면 인스턴스에 캡처한 변수의 값도 함게 변경되도록 설계할 수도 있겠지만 이로 인해서 수많은 문제들이 파생될 수 있음
package nested.local;

import java.lang.reflect.Field;

public class LocalOuterV4 {

    public Printer process(int paramVar) {

        int localVar = 1;

        class LocalPrinter implements Printer {

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

        Printer printer = new LocalPrinter();
        // localVar의 값을 변경
//        localVar = 20;    // 컴파일 오류 발생
//        paramVar = 20;    // 컴파일 오류 발생

        return printer;
    }

    // ... 코드 동일 생략 
}

 

(4-3) 캡처 변수의 값을 변경하지 못하는 이유

  • 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야하고 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야함
  • 개발자 입장에서는 예상하지 못한 곳에서 값이 변경될 수 있기 때문에 디버깅을 어렵게 함
  • 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데 멀티쓰레드 상황에서 이런 동기화는 매우 어려우며 성능에 나쁜 영향을 줄 수 있음
  • 이 모든 문제는 캡처한 지역 변수의 값이 변하기 때문에 발생하기 때문에 자바는 캡처한 지역 변수의 값을 변하지 못하도록 막아서 이런 문제들을 근본적으로 차단함

** 참고

  • 변수 캡처에 대한 내용이 이해가 어렵다면 단순하게 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다 정도로 이해해도 됨

2. 익명 클래스

1) 시작

(1) 익명 클래스(anonymous class)

  • 지역 클래스의 특별한 종류의 하나로 클래스의 이름이 없는 것이 특징임
  • 앞서 예제에서 활용했던 LocalOuterV2코드를 보면 LocalPrinter 지역 클래스를 선언하고, Printer printer = new LocalPrinter();로 생성하는 과정을 거쳤음
  • 그러나 익명 클래스를 사용하면 클래스 이름을 생략하고 클래스의 선언과 생성을 한번에 처리할 수 있음
// 지역 클래스
class LocalPrinter implements Printer{   // 선언
    //body
}
Printer printer = new LocalPrinter();    // 생성

// 익명 클래스
Printer printer = new Printer(){  // 선언과 생성을 한번에
    //body
}

 

(2) AnonymousOuter

  • 앞서 설명한 LocalOuterv2와 완전히 같은 코드지만 지역클래스를 익명 클래스로 변경하였음
  • 출력해보면 결과도 똑같이 나오는 것을 확인할 수 있으며 출력된 클래스 정보를 보면 끝에 숫자가 있는데 익명 클래스는 이름이 없으므로 하나만있으면1, 하나 더있으면2 이런식으로 출력됨
package nested.anonymous;

import nested.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.getClass() = " + printer.getClass());
    }

    public static void main(String[] args) {
        AnonymousOuter main = new AnonymousOuter();
        main.process(2);
    }
}
/* 실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
printer.getClass() = class nested.anonymous.AnonymousOuter$1
*/

 

(3) new Printer() {body}

  • 익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성하며 new 다음에 바로 상속 받으면서 구현할 부모 타입을 입력하면 됨
  • 인터페이스인 Printer를 생성하는 것 처럼보이지만 인터페이스를 생성하는 것이 아니고 Printer라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것임
  • { body } 부분에 Printer 인터페이스를 구현한 코드를 작성하면 되는데 이부분이 바로 익명 클래스의 본문이 됨
  • 즉, Printer를 상속(구현)하면서 바로 생성하는 것임

(4) 익명 클래스의 특징

  • 이름 없는 지역 클래스를 선언하면서 동시에 생성함
  • 부모 클래스를 상속 받거나 인터페이스를 구현해야하므로 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요함
  • 익명 클래스는 말 그대로 이름이 없으므로 생성자를 가질 수 없음(기본 생성자만 사용됨
  • 익명 클래스는 AnonymousOuter$1과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의되고 익명 클래스가 정의되면 숫자가 증가하면서 구분됨

(5) 익명 클래스의 장점

  • 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 간결해짐
  • 하지만 복작하거나 재사용이 필요한 경우에는 별도의 클래스를 정의하는 것이 좋음

(6) 익명 클래스를 사용할 수 없을 때

  • 익명 클래스는 단 한번만 인스턴스를 생성할 수 있으므로 여러 번 인스턴스를 생성해야 한다면 익명 클래스를 사용할 수 없으므로 지역 클래스를 선언하고 사용하면 됨

(7) 정리

  • 익명 클래스는 이름이 없는 지역 클래스
  • 특정 부모 클래스(인터페이스)를 상속 받고 바로 생성하는 경우에 사용
  • 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용

2) 활용

(1-1) Ex0Main - 리펙토링 전

  • 단순히 출력을 하는 코드로 helloJava()의 메서드와 helloSpring()의 메서드를 보면 출력문에서 코드의 중복이 보임
package nested.anonymous.ex;

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-2) Ex0RefMain - 리펙토링 후

  • 메소드에 매개변수를 추가하여 메서드 호출 시 넘어오는 인수를 변하는 부분에 출력하도록 변경하면 코드의 중복이 제거되며 기존과 동일한 결과가 출력되는 것을 확인할 수 있음
package nested.anonymous.ex;

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");
    }
}
/* 실행 결과 동일 */

 

(1-3) 코드 분석

  • 여기 리펙토링에서의 핵심은 변하는 부분과 변하지 않는 부분을 분리하고 변하지 않는 부분은 유지하고 변하는 부분만 어떻게 해결할 것인가에 집중하면 됨
  • 여기서 변하는 부분은 "Hello Java", "Hello Spring" 이므로 이 문자열 데이터를 외부에서 전달받아서 출력하면 해결됨
  • 단순한 문제였지만 프로그래밍에서 중복을 제거하고 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것임
  • 이렇게 변하는 부분과 변하지 않는 부분을 분리하고 변하는 부분을 외부에서 전달 받으면 메서드의 재사용성을 높일 수 있음
  • 리펙토링 전과 후를 비교해보면 hello(String str) 함수의 재사용성은 매우 높아졌음
  • 핵심은 변하는 부분을 메서드(함수)내부에서 가지고 있는 것이 아니라 외부에서 전달 받는다는 점임

(2-1) Ex1Main - 리팩토링 전

  • 랜덤 주사위의 값을 출력하는 메서드와 0부터 2까지 숫자를 출력하는 메서드 2가지가 있음
  • 여기도 프로그램 시작, 프로그램 종료라는 공통적으로 변하지 않는 부분이 있고 중간에 반하는 코드 조각이 있는데 이를 리팩토링할 수 있음
package nested.anonymous.ex;

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();
    }
}
/* 실행 결과
프로그램 시작
주사위 = 5
프로그램 종료
프로그램 시작
i = 0
i = 1
i = 2
프로그램 종료
*/

 

(2-2) Ex1RefMain - 리팩토링 후

  • 여기에서의 변하는 부분은 단순한 데이터가아니고 코드 조각인데 이것을 외부에서 전달 받아야하는 것은 단순한 데이터를 전달 받는 것과는 차원이 다른 문제임
  • 코드 조각은 보통 메서드(함수)에 정의하므로 코드 조각을 전달받기 위해서는 메서드가 필요한데 지금까지 학습한 내용으로는 메서드를 전달할 수 있는 방법이 없으므로 인스턴스를 전달하고 인스턴스에 있는 메서드를 호출하는 방법으로 해결할 수 있음
  • 인스턴스를 전달하기 위해 별도의 Process 라는 인터페이스를 생성 후 Dice, Sum이라는 클래스를 만들어서 인터페이스를 구현하여 run()메서드에 필요한 코드조각들을 구현
  • 참고로 여기서는 정적 중첩 클래스를 했지만 별도의 외부 클래스로 해도 상관없음
  • hello(Process process) 메서드를 통해 외부에서 Process를 구현한 인스턴스를 전달 받을 수 있도록하고 그 인스턴스를 통해 run()메서드를 호출하면 다형성 덕분에 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행되는 것을 확인할 수 있음
package nested.anonymous.ex;

public interface Process {
    void run();
}

package nested.anonymous.ex;

import java.util.Random;

public class Ex1RefMain {

    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 = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        }
    }

    public static void main(String[] args) {
        hello(new Dice());
        hello(new Sum());
    }
}
/* 실행 결과는 동일(랜덤값 바뀜) */

 

(2-3) 정리

  • 문자열 같은 데이터를 메서드에 전달할 때는 String, int와 같은 데이터에 맞는 타입을 전달하면 됨
  • 코드 조각을 메서드에 전달할 때는 인스턴스를 전달하고 해당 인스턴스에 있는 메서드를 호출하면 됨

(3-1) Ex1RefMainV2 - 지역 클래스 사용

  • Ex1RefMain에서 만들었던 코드의 정적 중첩 클래스들을 main()메서드의 안으로 이동하여 static을 제거하여 지역 클래스로 선언할 수 있음
  • main()메서드의 지역 클래스로 변경하여도 문제없이 코드가 동일하게 실행 되는 것을 확인할 수 있음
package nested.anonymous.ex;

public class Ex1RefMainV2 {

    public static void hello(Process process) {
        // ... 코드 동일 생략
    }

    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 = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        }

        hello(new Dice());
        hello(new Sum());
    }
}

 

(3-2) Ex1RefMainV3 - 익명 클래스 사용1

  • 앞의 지역 클래스는 간단히 각 인스턴스들을 한번식만 생성해서 사용하므로 익명 클래스로 변경할 수 있음
  • 선언한 static Dice, Sum 클래스들을 를 new Process()로 Process 인터페이스를 구현한 익명 클래스를 생성하도록 변경하고 Process 타입의 dice, sum 변수에 각각 인스턴스의 참조값을 저장
  • 참조변수가 main() 내부에서 선언되어있으므로 객체 생성없이 hello(dice), hello(sum)로 참조값을 입력해주면 됨
package nested.anonymous.ex;

public class Ex1RefMainV3 {

    public static void hello(Process process) {
        // ... 코드 동일 생략
    }

    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 = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        };

        hello(dice);
        hello(sum);
    }
}

 

(3-3) Ex1RefMainV4 - 익명 클래스 사용2

  • Ex1RefMainV3와 같은 경우에는 익명 클래스의 참조값을 변수에 담아둘 필요 없이 인수로 바로 전달할 수 있음
  • hello()의 메서드에 바로 익명 클래스를 인수로 전달해도 값이 정상적으로 출력됨
package nested.anonymous.ex;

public class Ex1RefMainV4 {

    public static void hello(Process process) {
        // 동일 코드 생략
    }

    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 = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        });
    }
}

3) 람다(lambda)

(1) 람다

  • 자바8 이전까지 메서드에 인수로 전달할 수 있는 것은 int, double과 같은 기본형 타입이나 Process Member와 같은 참조형 타입 2가지임
  • 결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나 인스턴스의 참조임
  • 지금 예제에서는 메서드만 전달하고 전달하고 싶었지만 이를 위해서 클래스를 정의하고 메서드를 만들고 인스턴스를 생성해서 전달했어야 했음
  • 자바8에 들어서면서 큰 변화가 있었는데 바로 메서드(함수)를 인수로 전달할 수 있게 되었는데 이것을 람다(Lambda)라고 함

(2) Ex1RefMainV5 - 람다로 리팩토링

  • Ex1RefMainV4 코드를 보면 인텔리제이에서 new Process()익명 클래스 부분이 검정색으로 변해져 있는데 그 이유가 이것은 람다로 구현하는 것이 더 좋다고 IDE가 알려준 것임
  • new Process()부분에 커서를 올려놓고 옵션 + 엔터(mac의 경우)를 치면 바로 람다로 변경되는데, 코드를 보면 클래스나 인스턴스를 정의하지 않고 메서드(함수)의 코드 블럭을 직접 전달하는 것을 확인할 수 있음
  • 여러 리팩토링을 거치면서 최종적으로 람다로 구현하면서 코드가 매우 간결해짐과 동시에 프로그래밍도 정상적으로 동작하는 것을 확인할 수 있음
package nested.anonymous.ex;

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 = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}

 

** 참고

  • 람다에 대한 자세한 내용은 이후에 다른 강의에서 별도로 다루므로 지금은 이렇게 람다가 어떤 흐름으로 나오게 되었고 람다라는 것이 있다는 정도만 알아두면 됨

3. 문제와 풀이

1) 정적 중첩 클래스를 완성

(1) 문제 설명

  • OuterClass1에 NestedClass를 구현하고 hello() 메서드를 작성
  • OuterClass1Main에서 NestedClass의 hello() 메서드를 호출
더보기
package nested.test;

public class OuterClass1 {
    // 여기에 NestedClass를 구현하고 hello() 메서드를 만들기
}
package nested.test;

public class OuterClass1Main {
    public static void main(String[] args) {
        // 여기에서 NestedClass의 hello() 메서드를 호출
    }
}

 

실행결과

NestedClass.hello

 

(2) 정답

더보기
package nested.test;

public class OuterClass1 {
    static class NestedClass {
        public void hello() {
            System.out.println("NestedClass.hello");
        }
    }
}
package nested.test;

public class OuterClass1Main {
    public static void main(String[] args) {
        OuterClass1.NestedClass nestedClass = new OuterClass1.NestedClass();
        nestedClass.hello();
    }
}

2) 내부 클래스를 완성

(1) 문제 설명

  • OuterClass2에 InnterClass와 hello()메서드를 정의하고 OuterClass2Main을 완성시킨 후 hello() 메서드 출력
더보기
package nested.test;

public class OuterClass2 {
    // 여기에 InnerClass를 구현하고 hello() 메서드를 작성
}
package nested.test;

public class OuterClass2Main {
    public static void main(String[] args) {
        // 여기에서 InnerClass의 hello() 메서드를 호출해라.
    }
}

 

실행 결과

InnerClass.hello

 

(2) 정답

더보기
package nested.test;

public class OuterClass2 {

    class InnerClass {
        public void hello() {
            System.out.println("InnerClass.hello");
        }
    }
}
package nested.test;

public class OuterClass2Main {
    public static void main(String[] args) {
        OuterClass2 outerClass = new OuterClass2();
        OuterClass2.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.hello();

        new OuterClass2().new InnerClass().hello(); // 위와 동일
    }
}

 

3) 지역 클래스를 완성

(1) 문제 설명

  • OuterClass3의 myMethod()에 지역 클래스 LocalClass를 구현하고 hello()메서드를 호출
더보기
package nested.test;

public class OuterClass3 {
    public void myMethod() {
        // 여기에 지역 클래스 LocalClass를 구현하고 hello() 메서드를 호출
    }
}
package nested.test;

class OuterClass3Main {
    public static void main(String[] args) {
        OuterClass3 outerClass3 = new OuterClass3();
        outerClass3.myMethod();
    }
}

 

실행 결과

LocalClass.hello

 

(2) 정답

더보기
package nested.test;

public class OuterClass3 {
    public void myMethod() {

        class LocalClass {
            public void hello() {
                System.out.println("LocalClass.myMethod");
            }
        }
        new LocalClass().hello();
    }
}

4) 익명 클래스를 완성

(1) 문제 설명

  • AnonymousMain의 main()메서드에서 Hello의 익명 클래스를 생성하고 hello를 호출
더보기
package nested.test;

public interface Hello {
    void hello();
}
package nested.test;

public class AnonymousMain {
    public static void main(String[] args) {
        // 여기에서 Hello의 익명 클래스를 생성하고 hello()를 호출
    }
}

 

실행 결과

Hello.hello

 

(2) 정답

더보기
package nested.test;

public class AnonymousMain {
    public static void main(String[] args) {

        Hello hello = new Hello() {
            @Override
            public void hello() {
                System.out.println("Hello.hello");
            }
        };
        hello.hello();
    }
}

5) 도서 관리 시스템

(1) 문제 설명

  • 도서관에서 사용할 수 있는 간단한 도서 관리 시스템을 만들기
  • 여러 권의 도서 정보를 관리할 수 있어야 하며 각 도서는 도서 제목(title)과 저자명(author)을 가지고 있음
  • 시스템은 도서를 추가하고 모든 도서의 정보를 출력하는 기능을 제공해야 함
  • LibraryMain과 실행 결과를 참고하여 Library 클래스를 완성
  • Book 클래스는 Library 내부에서만 사용되며 Library 클래스 외부로 노출하면 안됨
  • Library 클래스는 Book 객체 배열을 사용하여 도서 목록을 관리
더보기
package nested.test.ex1;

public class Library {
    // 코드 작성
}
package nested.test.ex1;

public class LibraryMain {
    public static void main(String[] args) {
        Library library = new Library(4); // 최대 4권의 도서를 저장할 수 있는 도서관 생성
        library.addBook("책1", "저자1");
        library.addBook("책2", "저자2");
        library.addBook("책3", "저자3");
        library.addBook("자바 ORM 표준 JPA 프로그래밍", "김영한");
        library.addBook("OneMoreThing", "잡스");
        library.showBooks(); // 도서관의 모든 도서 정보 출력
    }
}

 

실행 결과

도서관 저장 공간이 부족합니다.

== 목록 출력 ==

도서 제목: 1, 저자: 저자1

도서 제목: 2, 저자: 저자2

도서 제목: 3, 저자: 저자3

도서 제목: 자바 ORM 표준 JPA 프로그래밍, 저자: 김영한

 

(2) 정답

더보기

강의 버전

package nested.test.ex1;

public class Library {

    private Book[] books;
    private int bookCount;

    public Library(int size) {
        this.books = new Book[size];
        bookCount = 0;
    }

    public void addBook(String title, String author) {
        if (bookCount >= books.length) {
            System.out.println("도서관 저장 공간이 부족합니다.");
            return;
        }
        books[bookCount++] = new Book(title, author);
    }
    
    public void showBooks() {
        System.out.println("== 책 목록 출력 ==");
        for (int i = 0; i < bookCount; i++) {
            System.out.println("도서 제목: " + books[i].title + ", 저자: " + books[i].author);
        }
    }

    private static class Book {
        private String title;
        private String author;

        public Book(String title, String author) {
            this.title = title;
            this.author = author;
        }
    }
}

 

6) 정리

(1) 정적 중첩 클래스

  • 바깥 클래스와 밀접한 관련이 있지만 인스턴스 간에 데이터 공유가 필요 없을 때 사용

(2) 내부 클래스

  • 바깥 클래스의 인스턴스와 연결되어 있고 바깥 클래스의 인스턴스 상태에 의존하거나 강하게 연관된 작업을 수행할 때 사용

(3) 지역 클래스

  • 내부 클래스의 특징을 가지며 지역 변수에 접근할 수 있음
  • 접근하는 지역 변수는 final이거나 사실상 final이여야 함
  • 주로 메서드 내에서만 간단히 사용할 목적으로 사용함

(4) 익명 클래스

  • 지역 클래스인데 이름이 없으므로 상위 타입을 상속하거나 구현하면서 바로 생성함
  • 주로 특정 상위 타입을 간단히 구현하여 일회성으로 사용할 때 유용함