devseop08 님의 블로그

[OOP] 내부 클래스와 익명 객체 본문

Language/Java

[OOP] 내부 클래스와 익명 객체

devseop08 2025. 7. 9. 18:08

중첩 클래스와 중첩 인터페이스

  • 객체지향 프로그래밍에서 클래스들은 서로 긴밀한 관계를 맺고 상호작용
  • 어떤 클래스는 여러 클래스와 관계를 맺지만 어떤 클래스는 특정 클래스와 관계를 맺는다.
  • 클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나,
  • 클래스가 특정 클래스와 관계를 맺을 경우에는 해당 클래스 내부에 특정 클래스를 선언하는 것이 좋다

중첩 클래스

  • 중첩 클래스란 클래스 내부에 선언한 클래스를 말한다.
  • 중첩 클래스를 사용하면 두 클래스의 멤버들을 서로 쉽게 접근할 수 있고, 외부에는 불필요한 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있는 장점이 있다.
class ClassName {
    class NestedClassName {
    }
}

중첩 클래스의 분류

  • 중첩 클래스는 클래스 내부에 선언된 위치에 따라서 두 가지로 분류
  • 클래스의 멤버로서 선언되는 중첩 클래스를 멤버 클래스라 하고,
  • 생성자 또는 메서드 내부에서 선언되는 중첩 클래스를 로컬 클래스라고 한다.

  • 중첩 클래스도 하나의 클래스이기 때문에 컴파일하면 바이트 코드 파일(.class)이 별도로 생성된다
// 멤버 클래스일 경우의 바이트 코드 파일의 이름: 바깥 클래스와 $ 포함
A $ B.class

// 로컬 클래스일 경우의 바이트 코드 파일의 이름: 바깥 클래스와 $1 포함

A $1 B.class

중첩 클래스 멤버와 객체 생성

1. 인스턴스 멤버 클래스
  • 인스턴스 멤버 클래스는 static 키워드 없이 중첩 선언된 클래스를 말한다.
  • 인스턴스 멤버 클래스는 인스턴스 필드와 메서드만 선언이 가능하고 정적 필드와 메서드는 선언할 수 없다.
  • 인스턴스 멤버 클래스의 멤버
class A {
    /** 인스턴스 멤버 클래스 **/
    class B {
        B() { }   // 생성자
        int field1; // 인스턴스 필드
        //static int field2; // 정적 필드 불가
        void method1() { } // 인스턴스 메서드
        // static void method() { } // 정적 메서드 불가
    }
}
  • 인스턴스 멤버 클래스의 객체 생성과 사용
    • 바깥 클래스 외부: 바깥 클래스 객체를 먼저 생성하고 인스턴스 멤버 클래스 객체 생성, 사용
    • A a = new A(); A.B b = a.new B(); b.field1 = 3; b.method1();
    • 바깥 클래스 내부: 일반 클래스처럼 객체 생성, 사용
    • class A{ class B { ... } void methodA() { B b = new B(); b.field1 = 3; b.method1(); } }
2. 정적 멤버 클래스
  • 정적 멤버 클래스는 static 키워드로 선언된 클래스를 말한다.
  • 정적 멤버 클래스는 모든 종류의 필드와 메서드를 선언할 수 있다.
class A {
    /** 정적 멤버 클래스 **/
    static class C {
        C() { } // 생성자
        int field1; // 인스턴스 필드
        static int field2; // 정적 필드
        void method1() { } // 인스턴스 메서드
        static void method2() { } // 정적 메서드    
    }
}
  • 정적 멤버 클래스의 객체 생성과 사용
    • 바깥 클래스의 외부에서 정적 멤버 클래스의 객체를 생성하고 사용하기 위해서는 바깥 클래스의 객체를 생성할 필요가 없다
    • A.C c = new A.C(); c.field1 = 3; // 인스턴스 필드 사용 c.method1(); // 인스턴스 메서드 호출 A.C.field2 = 3; // 정적 필드 사용 A.C.method2(); // 정적 메서드 호출
3. 로컬 클래스
  • 로컬 클래스는 접근 제한자(public, private) 및 static을 붙일 수 없다.
  • 로컬 클래스는 메소드 내부에서만 사용되므로 접근을 제한할 필요가 없기 때문이다.
  • 로컬 클래스 내부에는 인스턴스 필드와 메서드만 선언할 수 있고 정적 필드와 메서드는 선언할 수 없다.
class A {
    void method() {
        class D {
            D() { } // 생성자
            int field1; // 인스턴스 필드
            // static int field2; // 정적 필드 불가
            void method1() { } // 인스턴스 메서드
            // static void method2() { } // 정적 메서드 불가
        }

        D d = new D();
        d.field1 = 3;
        d.method1();
    }
}
  • 로컬 클래스는 메서드가 실행될 때 메서드 내에서 객체를 생성하고 사용해야 한다.
  • 로컬 클래스는 주로 자신이 선언된 바깥 메서드가 속한 스레드와는 분리되는 컨텍스트의 스레드객체를 만들어 비동기 처리를 하기 위한 목적으로 사용된다.
class A {
    void method() {
        class DownloadThread extends Thread { ... }
        DownloadThread thread = new DownloadThread();
        thread.start() 
        // 바깥 메서드(method) 호출 스택이 사라져도 thread가 참조하는 
        // DownloadThread 객체는 여전히 실행 중이다. 
        // 바깥 메서드(method)가 속한 스레드와는 비동기이다.
    }
}
4. 중첩 클래스 객체 생성과 사용
/** 바깥 클래스 **/
class A {
    A() { System.out.println("A 객체가 생성됨"); }

    /**인스턴스 멤버 클래스**/
    class B {
        B() { System.out.println("B 객체가 생성됨") }
        int field1;
        // static int field2;
        void method1() { }
        // static void method2() { } 
    }

    /**정적 멤버 클래스**/
    static class C {
        C() { System.out.println("C 객체가 생성됨") }
        int field;
        static int field2;
        void method1() { }
        static void method2() { } 
    }

    void method() {
        /**지역 클래스**/
        class D{
            D() { System.out.println("D 객체가 생성됨"); }
            int field1;
            // static int field2;
            void method1() { }
            // static void method2() { }
        }
        D d = new D();
        d.field1 = 3;
        d.method1();

    }
}
public class Main {
    public static void main(String[] args) {
        A a = new A();

        // 인스턴스 멤버 클래스 객체 생성
        A.B b = a.new B();
        b.field1 = 3;
        b.method1();

        //  정적 멤버 클래스 객체 생성
        A.C c = new A.C();
        c.field1 = 3;
        c.method1();
        A.C.field2 = 3;
        A.C.method2();

        // 로컬 클래스 객체 생성을 위한 메서드 호출
        a.method();
    }
}

중첩 클래스의 접근 제한

1. 바깥 필드와 메서드에서 사용 제한
  • 인스턴스 멤버 클래스는 바깥 클래스의 인스턴스 필드의 초기값이나 인스턴스 메서드에서 객체를 생성할 수 있다.
  • 인스턴스 멤버 클래스는 정적 필드의 초기값이나 정적 메서드에서는 객체를 생성할 수 없다.
  • 정적 멤버 클래스는 모든 필드의 초기값이나 모든 메서드에서 객체를 생성할 수 있다.
public class A {

    // 인스턴스 필드
    B field1 = new B();
    C field2 = new C();

    // 인스턴스 메서드
    void method1() {
        B var1 = new B();
        C var2 = new C();
    }

    // 정적 필드 초기화
    // static B field3 = new B();
    static C field4 = new C();

    // 정적 메서드
    static void method2() {
        // B var1 = new B();
        C var2 = new C();
    }

    // 인스턴스 멤버 클래스
    class B { ... }

    // 정적 멤버 클래스
    static class C { ... }
} 
2. 멤버 클래스에서 사용 제한
  • 인스턴스 멤버 클래스 안에서는 바깥 클래스의 모든 필드와 메서드에 접근 가능
  • 정적 멤버 클래스 안에서는 바깥 클래스의 정적 필드와 정적 메서드에만 접근 가능하고 인스턴스 필드와 인스턴스 메서드에는 접근 불가
public class A {
    int field1;
    void method1(){ }

    static int field2;
    static void method2{ }

    class B {
        void method() {
            field1 = 10;
            method1();

            field2 = 10;
            method2();
        }
    }

    static class C {
        void method() {
            //field1 = 10;
            //method1();

            field2 = 10;
            method2();
        }
    }
}
3. 로컬 클래스에서 사용 제한: 바깥 메서드의 지역 변수 final 특성 부여
  • 메서드의 매개 변수나 지역 변수를 로컬 클래스에 사용할 때 제한되는 부분이 있다.
  • 메서드 내의 매개 변수나 지역 변수는 메소드 실행이 종료되면서 스택 프레임에서 제거된다.
  • 마찬가지로 메서드 내에서 로컬 클래스 객체가 생성된 후 해당 메서드 실행이 종료되면
  • 해당 로컬 클래스 객체의 참조 변수도 스택 프레임에서 제거되면서 로컬 클래스 객체가 힙 영역에서 JVM 가비지 컬렉터에 의해 수거되고 소멸되는 것이 일반적이다.
  • 하지만 메서드가 종료되어도 메서드 내에서 생성된 로컬 클래스 객체가 계속 실행 상태로 존재하는 경우가 있다.
  • 예를 들면 로컬 클래스가 Thread 클래스를 상속한 스레드 클래스일 때 해당 로컬 클래스 객체를 메서드 내에서 생성하면 메서드가 종료되어도 로컬 클래스 객체는 메서드가 속한 스레드와는 다른 스레드에 속하므로 JVM 가비지 컬렉터가 수거하지 않게 되어 계속 실행 상태로 존재하게 된다.
  • 그런데 이미 종료된 바깥 메서드에서 생성된 로컬 클래스 객체에서 이미 종료된 바깥 메서드 내에서 정의됐던 매개 변수나 지역 변수를 사용한다면 문제가 될 것이다.
  • 이미 종료된 바깥 메서드 내에서 정의됐던 매개 변수나 지역 변수는 사라졌기 때문이다.
  • 자바는 이 문제를 해결하기 위해 로컬 클래스를 컴파일 시 로컬 클래스에서 사용하는 바깥 메서드의 매개변수나 지역 변수의 값을 로컬 클래스 내부에 복사해두고 사용하도록 한다.
  • 또한 매개 변수나 로컬 변수가 수정되어 값이 변경되면 로컬 클래스에 복사해둔 값과 달라지므로 문제를 해결하기 위해 로컬 클래스에 사용하는 바깥 메서드의 매개변수나 지역 변수 final로 선언할 것을 요구한다.
  • 다만 자바 7 이전까지는 로컬 클래스에 사용하는 바깥 메서드의 매개변수나 지역 변수를 명시적으로 final 선언을 해줘야 했지만 자바 8부터는 명시적인 final 선언을 하지 않아도 값이 수정될 수 없도록 로컬 클래스에 사용하는 바깥 메서드의 매개변수나 지역 변수에 자동으로 final 특성을 부여해준다.
public class Outer{
    // 자바 7 이전
    publilc void method1(final int arg) {
        final int localVariable = 1;
        //arg = 100;
        //locaVariable = 100;

        class Inner{
            public void method() {
                int result = arg + localVariable;
            }
        }
    }

    // 자바 8 이후
    public void method2(int arg) {
        int localVariable = 1;
        //arg = 100;
        //locaVariable = 100;

        class Inner{
            public void method() {
                int result = arg + localVariable;
            }
        }
    }
}
4. 중첩 클래스에서 바깥 클래스 참조 얻기
  • 클래스 내부에서 this는 객체 자신의 참조이다.
  • 중첩 클래스 내부에서 this 키워드를 사용하면 바깥 클래스의 객체 참조가 아니라, 중첩 클래스의 객체 참조가 된다.
  • 중첩 클래스 내부에서 바깥 클래스의 객체 참조를 얻으려면 바깥 클래스의 이름을 this 앞에 붙여주면 된다.
바깥 클래스.this.필드
바깥 클래스.this.메서드();
public class Outer {
    String field = "Outer-field";
    void method() {
        System.out.println("Outer-method");
    }

    class Nested {
        String field = "Nested-field";
        void  method() {
            System.out.println("Nested-method");
        }
        void print() {
            System.out.println(this.field); // Nested-field
            this.method(); // Nested-method
            System.out.println(Outer.this.field) // Outer-field
            Outer.this.method(); // Outer-method
        }

    }
}

중첩 인터페이스

  • 중첩 인터페이스란 클래스 내부에 선언한 인터페이스
  • 인터페이스를 클래스 내부에 선언한 이유는 해당 클래스와 긴밀한 관계를 맺는 인터페이스 구현 클래스를 만들기 위함이다.(class implements interface)
public class A {
    [static] interface I {
        void method();
    }
}
  • 중첩 인터페이스는 인스턴스 멤버 인터페이스와 정적 멤버 인터페이스 모두 가능하다.
  • 인스턴스 멤버 인터페이스는 바깥 클래스의 객체가 있어야 사용 가능하며, 정적 멤버 인터페이스는 바깥 클래스의 객체 없이 바깥 클래스만으로 바로 접근 가능
  • 주로 정적 멤버 인터페이스를 많이 사용한다.
  • UI 프로그래밍에서 이벤트를 처리할 목적으로 많이 활둉된다.
public class Button {
    static interface OnClickListener { // 중첩 인터페이스, 정적 멤버 인터페이스
        void onClick();
    }

    OnClickListener listener; // 인터페이스 타입 필드

    void setOnClickListener(OnClickListener listener) {
        this.listener = listener; 
    }

    void touch() {
        listener.onClick();
    }
}
  • 중첩 인터페이스 구현 클래스
public class CallListener implements Button.OnClickListener {
    @Override
    public void onClick() {
        System.out.println("전화를 겁니다.");
    }
}
public class MessageListener implements Button.OnClickListener {
    @Override
    public void onClick(){
        System.out.println("메시지를 보냅니다.");
    }
}
public class ButtonExample {
    public static void maint(String[] args) {
        Button btn = new Button();

        btn.setOnClickListener(new CallListener());
        btn.touch();

        btn.setOnClickListener(new MessageListener());
        btn.touch();
    }
}

익명 객체

  • 익명 객체는 이름이 없는 객체
  • 익명 객체는 반드시 어떤 클래스를 상속하거나 인터페이스를 구현해야만 하는데
  • 익명 객체를 생성할 때는 익명 객체에 해당하는 클래스 이름이 없다.
// 부모 클래스 상속
Parent p = new Parent(){ ... }; // Parent 클래스를 상속한 익명 클래스의 객체 생성

// 인터페이스 구현
Interface i = new Interface(){ ... } // 인터페이스를 구현한 익명 클래스의 객체 생성

1. 익명 자식 객체 생성

  • 자식 클래스가 재사용되지 않고, 오로지 특정 위치에서만 사용할 경우라면 자식 클래스를 명시적으로 선언하기 보다는 익명 자식 객체를 생성해서 사용하는 것이 좋다.
부모 클래스 [필드 | 변수] = enw 부모 클래스(파라미터값, ...){
    // 필드 
    // 메서드
}
public class Person {
    void wake() {
        System.out.println("7시에 일어납니다.");
    }
}

public class Anonymous {
    // 필드 초기값으로 익명 자식 객체 대입
    Person field = new Person() {
        void work() {
            System.out.println("출근합니다.");
        }

        @Override
        void wake() {
            System.out.println("6시에 일어납니다.");
            work();
        }
    };

    void method1() {
        // 메서드의 로컬 변수값으로 익명 자식 객체 대입
        Person localVar = new Person() {
            void walk() {
                System.out.println("산책합니다.");
            }

            @Override
            void wake() {
                System.out.println("7시에 일어납니다.");
                walk();
            }
        };

        localVar.wake();
    }

    void method2(Person person) {
        person.wake();
    }
}
public class AnonymousExample {
    public static void main(String[] args){
        Anonymous anony = new Anonymous();
        // 익명 객체 필드 사용
        anony.field.wake();
        // 익명 객체 로컬 변수 사용
        anony.method1();
        // 익명 객체 파라미터값 사용
        anony.method2(new Person() {
            void study(){
                System.out.println("공부합니다.");
            }
            @Override
            void wake() {
                System.out.println("8시에 일어납니다.");
                study();
            }
        })
    }
}

2. 익명 구현 객체 생성

  • 인터페이스의 구현 클래스가 재사용되지 않고, 오로지 특정 위치에서 사용할 경우라면 구현 클래스를 명시적으로 선언하는 것보단 익명 구현 객체를 생성해서 사용하는 것이 좋다.
인터페이스 [필드 | 변수] = new 인터페이스() {
    // 필드
    // 메서드
    // 인터페이스에 선언된 추상 메서드의 실제 메서드 선언
}
public class Button {
    static interface OnClickListener { // 중첩 인터페이스, 정적 멤버 인터페이스
        void onClick();
    }

    OnClickListener listener; // 인터페이스 타입 필드

    void setOnClickListener(OnClickListener listener) {
        this.listener = listener; 
    }

    void touch() {
        listener.onClick();
    }
}
public class Window {
    Button button1 = new Button();
    Button button2 = new Button();

    // 필드 초기값으로 익명 구현 객체 대입
    Button.OnClickListener listener = new Button.OnClickListener() {
        @Override
        public void onClick(){
            System.out.println("전화를 겁니다.");
        }
    };

    Window() {
        button1.setOnClickListener(listener); // 파라미터값으로 필드 대입

        // 파라미터값으로 익명 구현 객체 대입
        button2.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(){
                System.out.println("메시를 보냅니다.");
            }
        }); 
    }
}
public class Main {
    public static void main(String[] args){
        Window w = new Window();
        w.button1.touch(); // 전화를 겁니다.
        w.button2.touch(); // 메시지를 보냅니다.
    }
}

3. 익명 객체의 로컬 변수 사용: 바깥 메서드의 지역 변수 final 특성 부여

  • 로컬 클래스와 같은 원리로, 익명 객체가 생성된 메서드의 내부에서 해당 익명 객체가 해당 메서드의 매개 변수나 지역 변수를 사용할 때 제한되는 부분이 존재한다.
  • 결론적으로 로컬 클래스 객체에서 사용하는 메서드의 매개 변수나 지역 변수는 final 특성이 자동으로 부여되는 것과 같이 익명 객체에서 사용하는 메서드의 매개 변수나 지역 변수도 final 특성이 자동으로 부여된다.
public interface Calculatable {
    public int sum();
}
public class Anonymous {
    private int field;

    public void method(final int arg1, int arg2){
        final int var1 = 0;
        int var2 = 0;

        field = 10; 
        /* 
            field는 메서드 호출 스택이 제거되도 소멸되지 않으므로 
            익명 객체에서 사용하더라도 final 특성이 부여되지 않아야 한다.
        */

        // arg1 = 20;
        // arg2 = 20;

        // var1 = 30;
        // var2 = 30;

        Calculatable calc = new Calculatable() {
            @Override 
            public int sum() {
                int result = field + arg1 + arg2 + var1 + var2;
                return result;        
            }
        };

        System.out.println(calc.sum());
    }
}
public class AnonymousExample{
    public static void main(String[] args){
        Anonymous anony = new Anonymous();
        anony.method(0, 0);
    }
}