6) 객체지향 프로그래밍9 - 내부 클래스

dev-mage·2022년 10월 24일
0

Hello Java World!

목록 보기
19/32
post-thumbnail

Java의 내부 클래스

내부 클래스(inner class)

내부 클래스는 클래스 내에 선언된 클래스이다. 어떤 클래스와 밀접한 관련이 있는 클래스가 있다면 클래스 내에 해당 클래스를 선언하면 된다. 이렇게 한 클래스를 다른 클래스의 내부에 선언하면 두 클래스 멤버들 간 쉬운 접근이 가능하며 외부에는 불필요한 클래스를 감춤으로써(캡슐화) 코드의 복잡성을 줄일 수 있다.

다음과 같이 A, B 두 개의 독립적인 클래스를 합치면 B는 A의 내부 클래스(inner class)가 되고 A는 B를 감싸는 외부 클래스(outer class)가 된다.

class A { ... }
class B { ... }

/////////////////////////////////////////////////////////////

class A { // 외부 클래스
	...
	class B { // 내부 클래스 
		...
	}
	...
}

이때 내부 클래스인 B는 외부 클래스인 A를 제외하고는 다른 클래스에서 잘 사용되지 않는 것이어야 한다.

내부 클래스의 종류와 특징

내부 클래스의 종류는 변수의 선언 위치에 따른 종류와 같고 변수를 선언하는 것과 같은 위치에 선언할 수 있다.

내부 클래스특징
인스턴스 클래스(instance class)외부 클래스의 멤버 변수 위치에 선언되며, 외부 클래스의 인스턴스 멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다.
정적 클래스(static class)외부 클래스의 멤버 변수 위치에 선언되며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다.
지역 클래스(local class)외부 클래스의 메서드나 초기화 블럭 안에 선언되며, 선언된 영역 내부에서만 사용될 수 있다.
익명 클래스(anonymous class)클래스의 선언과 객체의 생성을 동시에 하는 이름 없는 일회용 클래스이다.

내부 클래스는 다음과 같은 특징을 가진다.

  • 변수가 선언된 위치에 따라 나뉘듯이 내부 클래스도 이와 마찬가지로 선언 위치의 변수와 동일한 유효범위(scope)와 접근성(accessibility)을 가진다. 멤버 변수와 같은 성질을 갖기 때문에 동일하게 제어자도 사용할 수 있다.
  • 내부 클래스 중 정적 클래스만 static 멤버를 가질 수 있다.
    • 다만 final static이 동시에 붙은 변수는 상수이므로 모든 내부 클래스에서 정의가 가능하다.
  • 인스턴스와 클래스 변수의 관계처럼 인스턴스 클래스는 외부 클래스의 인스턴스 멤버를 객체를 생성하지 않고 바로 사용할 수 있지만, 정적 클래스는 외부 클래스의 인스턴스 멤버를 객체 생성 없이 사용할 수 없다.
  • 인스턴스 클래스는 정적 클래스의 클래스 멤버들을 객체 생성 없이 사용할 수 있지만, 정적 클래스에서는 외부 클래스의 인스턴스 클래스 객체를 생성해야만 인스턴스 클래스의 멤버에 접근할 수 있다.
  • 정적 클래스의 인스턴스 변수의 경우 정적 클래스의 객체를 생성해야만 접근할 수 있다.
  • 지역 클래스는 외부 클래스의 인스턴스 멤버와 클래스 멤버 및 지역 클래스를 포함하고 있는 메서드의 매개변수나 지역 변수도 사용할 수 있다.
    • 지역 변수나 메서드 매개변수의 경우 메서드 내에서 변경되지 않는 것이 보장됐을 때에만 사용할 수 있다. 따라서 final이 붙은 변수만 가능한데 JDK 8부터 변수에 값이 한번 할당된 후 변경 사항이 없다면 불변 상태인 것으로 간주하고 final 키워드를 생략한 경우 컴파일러가 자동으로 제어자를 추가한다. 만약 지역 클래스에서 바깥 메서드의 지역 변수를 참조하고 있음에도 불구하고 지역 변수의 값을 변경하려고 하면 컴파일 에러가 발생한다.
      이처럼 통제하는 이유는 메서드가 종료되어 지역 변수가 소멸된 후에도 지역 클래스의 인스턴스가 소멸된 지역 변수를 참조하려는 일이 발생할 수 있기 때문이다.
  • 외부 클래스가 아닌 다른 클래스에서 내부 클래스를 생성하려면 외부 클래스의 인스턴스를 통해 접근해야 하지만 이런 경우가 발생했다는 것은 내부 클래스로 선언해서는 안되는 클래스를 내부 클래스로 선언했다는 의미이다.
public class Outer {
    private String outerInstanceVar = "1. 외부 클래스의 private 인스턴스 변수";
    static String outerStaticVar = "2. 외부 클래스의 클래스 변수";

    class InstanceInner { // 인스턴스 클래스
        String instanceInnerVar = "3-1. 인스턴스 클래스의 인스턴스 변수";
        // static String CONST = "* 인스턴스 클래스의 상수 *"; // 에러
        // -> Static declarations in inner classes are not supported at language level '8' / 16부터 가능
        static final String CONST = "3-2. 인스턴스 클래스의 상수"; // 상수는 허용
        void instanceInnerMethod() {
            System.out.println("-- 인스턴스 클래스의 메서드 시작--");
            System.out.println(outerInstanceVar);
            System.out.println(outerStaticVar);
            System.out.println(instanceInnerVar);
            System.out.println(StaticInner.staticInnerStaticVar);
            System.out.println("-- 인스턴스 클래스의 메서드 끝--\n");
        }

    }

    static class StaticInner { // 정적 클래스
        // 정적 클래스만 static 멤버를 정의할 수 있음.
        static String staticInnerStaticVar = "4-1. 정적 클래스의 클래스 변수";
        String staticInnerVar = "4-2. 정적 클래스의 인스턴스 변수";
        static void staticInnerStaticMethod() {
            System.out.println("-- 정적 클래스의 클래스 메서드 시작--");
            // System.out.println(outerInstanceVar); // 에러
            // -> 클래스 멤버이므로 인스턴스 멤버에 바로 접근할 수 없음.
            System.out.println(outerStaticVar);
            System.out.println(staticInnerStaticVar);
            System.out.println("-- 정적 클래스의 클래스 메서드 끝--\n");
        }
    }
    static void staticMethod() {
        System.out.println("-- 클래스 메서드 시작--");
        StaticInner.staticInnerStaticMethod();
        // new InstanceInner().instanceInnerMethod(); // 에러
        // -> 인스턴스 클래스에 접근하려면 외부 클래스를 먼저 생성해야만 함.
        System.out.println(new Outer().new InstanceInner().instanceInnerVar);
        System.out.println(StaticInner.staticInnerStaticVar);
        // System.out.println(StaticInner.staticInnerVar); // 에러
        // -> 정적 클래스의 인스턴스 변수이므로 정적 클래스를 생성해야 한다.
        System.out.println(new StaticInner().staticInnerVar);
        System.out.println("-- 클래스 메서드 끝--\n");
    }

    void method() {
        // JDK 8부터 final 생략 가능
        String methodLocalVar = "5. 메서드의 지역 변수"; // == final String ...
        class LocalInner { // 지역 클래스
            String localInnerVar = "6-1. 지역 클래스의 인스턴스 변수";
            // static String CONST = "지역 클래스의 상수"; -> 에러
            static final String CONST = "6-2. 지역 클래스의 상수"; // 상수 허용
            void localInnerMethod() {
                System.out.println("-- 지역 클래스의 메서드 시작--");
                System.out.println(outerInstanceVar);
                System.out.println(outerStaticVar);
                System.out.println(new InstanceInner().instanceInnerVar);
                System.out.println(StaticInner.staticInnerStaticVar);
                // System.out.println(StaticInner.staticInnerVar); // 에러
                // -> staticInnerVar은 정적 클래스의 인스턴스 변수이므로 정적 클래스의 인스턴스를 통해 접근
                System.out.println(new StaticInner().staticInnerVar);
                System.out.println(methodLocalVar); // final이 붙은 상수만 접근 가능
                System.out.println(localInnerVar);
                System.out.println("-- 지역 클래스의 메서드 끝--\n");
            }
        }

        LocalInner localInner = new LocalInner();
        localInner.localInnerMethod();
        System.out.println(LocalInner.CONST);
    }

    public static void main(String[] args) {
				// 인스턴스 클래스의 인스턴스를 생성하려면
        // 외부 클래스의 인스턴스를 먼저 생성해야 함.
        Outer outer = new Outer();
        outer.new InstanceInner().instanceInnerMethod();
        // outer.new StaticInner().staticInnerMethod();
        // -> 에러 Qualified new of static class
        // : 클래스 멤버이기 때문에 외부 클래스의 인스턴스로 접근하지 않음.

        StaticInner.staticInnerStaticMethod();

        staticMethod();
        outer.method();

        System.out.println(InstanceInner.CONST);
    }
}

컴파일 시 생성되는 클래스 파일은 다음과 같다.

내부 클래스와 외부 클래스에 선언된 변수명이 같을 경우 변수 앞에 this 또는 외부클래스명.this처럼 구별할 수 있다.

익명 클래스

익명 클래스는 말 그대로 이름이 없는 클래스로 클래스의 선언과 객체의 생성을 동시에 하기 때문에 오직 하나의 객체만을 생성하여 단 한번만 사용되는 일회용 클래스이다. 이름이 없기 때문에 생성자도 가질 수 없고 부모 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스를 상속 받는 동시에 인터페이스를 구현한다거나 다중 인터페이스 구현은 할 수 없다. 오로지 단 하나의 클래스를 상속 받거나 인터페이스를 구현할 수 있다.

new 부모클래스명() {
		// 멤버 선언
}
/////////////////////////////////////////////
new 구현인터페이스명() {
		// 멤버 선언
}

예를 들어 캐리커처를 그려주는 이벤트가 열렸다고 하자. 캐리커처는 고객의 요청에 따라 항상 다르게 그려질 것이다. 그렇기에 익명 클래스로 구현하여 일회성으로 사용할 수 있다.

public class Caricature { ... }

//////////////////////////////////////////////////////////

public interface CaricatureService {
    Caricature draw();
}

//////////////////////////////////////////////////////////

public class Customer {
    void receive(Caricature caricature) {
        System.out.println("감사합니다!");
    };
}

//////////////////////////////////////////////////////////

import java.util.Scanner;
public class Event {
    public static void main(String[] args) {
        Customer customer = new Customer();
        System.out.println("어떻게 그려드릴까요?");
        Scanner scanner = new Scanner(System.in);
        String request = scanner.nextLine();
        scanner.close();
        CaricatureService caricatureService = new CaricatureService() {
            @Override
            public Caricature draw() {
                System.out.println("고객 요청사항: " + request);
                System.out.println("요청사항에 따라 그리는 중...");
                System.out.println("캐리커처 완성");
                return new Caricature();
            }
        };
        customer.receive(caricatureService.draw());
    }
}


References

0개의 댓글