자바 1.1 문법-내부 클래스

제작 : 윤경구
제작일 : 1997. 5. 22
최종 갱신일 : 1999. 12. 10
주요 검색어 : 내부 클래스, 이벤트 어댑터 클래스

필자의 동의 없이 복제 및 무단 전제할 수 없습니다.
Copyright (c) 1997 Yoon Kyung Koo(yoonforh@yahoo.com), All rights reserved.


1. 내부 클래스(Inner Class)란 무엇인가?

자바 1.1에서 달라진 문법 사항 중 가장 두드러진 것은 바로 내부 클래스라는 개념이다.
내부 클래스 외에는 문법적으로 크게 달라진 내용을 찾기는 어렵다.
내부 클래스의 정확한 개념을 갖추지 못하면 빈번하게 사용되는 이 개념에 당황하게 되고 또 소스 해독에 혼란을 느끼게 된다.

자바 1.0의 클래스 개념과 내부 클래스

자바 1.0에서는 모든 클래스는 어떤 패키지에 포함되는 클래스였다.
즉, 현재 소스 파일에 모든 클래스가 포함되어 패키지 이름이 생략되는 기본 패키지의 클래스라 하더라도 모든 클래스는 패키지에 속하도록 되어 있었다.
클래스 간의 접근 권한에서도 클래스 간의 상속 구조(하위 클래스이냐 아니냐)와 같은 패키지이냐 아니냐 하는 문제가 접근 권한을 나누는 두 가지 기준이 됨을 상기해보라.
이러한 클래스를 Top Level 클래스라고 부르자. 여기서 Top Level은 클래스 상속 구조의 수준과는 무관하다.

내부 클래스는 좀 특별한 존재이다.
이름은 클래스 내부에 존재하는(nested) 클래스라는 뜻을 가진다. 즉, 자바 1.0의 클래스 내부에 몇 가지 형태로 클래스를 또 정의할 수 있다는 것이다.

좀더 정확하게 표현하자면 이 내부 클래스도 두 가지 종류가 있다. 진정한 의미의 내부(inner) 클래스가 있고 nested top level 클래스가 있다.
nested top level 클래스는 비록 클래스 내부에 존재하긴 하지만 static 멤버로 선언되는 클래스로 이들은 비록 패키지의 멤버는 아니지만 외부에서 자신만의 인스턴스를 만들 수 있다는 점에서 top level 클래스로 분류된다.

필자는 자신만의 인스턴스를 만들 수 있는 netsted top level class를 포함한 모든 nested class를 안긴 클래스, 그렇지 못한 클래스(inner class)를 내부 클래스로 번역하니 오해 없길 바란다.

안긴 클래스 ---+--- 톱레벨 안긴 클래스 (static으로 선언된 멤버 클래스)
               |
               +--- 내부 클래스 (스스로는 인스턴스화되지 못한다)

내부 클래스를 포함한 이들 안긴 클래스는 컴파일러가 별도의 클래스로 분리하여 컴파일하기 때문에 (즉, 안긴 클래스는 자바 가상 기계에서 구현되는 것이 아니다) 내부 클래스가 포함된 코드를 사용한다고 해서 기존 버전과 호환성이 없어지지는 않는다.

2. 내부 클래스의 세 가지 형태

1) 클래스의 멤버로 존재하는 내부 클래스

멤버로 존재하므로 멤버 도구나 멤버 필드 앞에 오는 것처럼 public, private, static, final 등의 제한자를 사용할 수 있다.

2) 이름 있는 지역 클래스

도구나 블록 내부에서만 존재하는 클래스이다. 이 클래스는 해당 블록 내에서만 유효하다. 지역 클래스는 멤버가 아니므로 public, private, static, final 등의 제한자가 올 수 없다. 블록의 scope와 클래스의 scope가 동일하기 때문이다.

3) 이름 없는 클래스

지역 클래스의 이름이 별 의미가 없을 때, 아예 생략을 허용한다. 지역 클래스이므로 역시 블록 내에서만 유효하며 보통 블록 내에서 new 예약어와 함께 사용된다.

3. 내부 클래스의 전형적인 사용 예:이벤트 어댑터클래스

자바 1.1 버전에서 달라진 이벤트 모델(AWT 코드 업그레이드 참조)은 각 이벤트에 Source와 Listener 객체를 지정하도록 하고 있다. Source는 이벤트를 발생시키는 객체이고 Listener는 Source로부터 이벤트를 건네받는 객체이다. Listener 객체는 addXXXListener 형태의 도구를 통하여 소스 객체에 등록하게 되는데 해당 이벤트를 처리할 수 있기 위해서는 해당 이벤트의 Listener 인터페이스를 구현해야 한다. 하나의 이벤트를 처리하기 위해서도 Listener 인터페이스가 가지는 모든 멤버 도구들을 구현해야 하므로 불필요하게 코딩이 늘어난다. 예를 들어 윈도우가 닫기는 이벤트를 처리하고자 한다면 WindowEvent를 처리해야 한다. 이것은 WindowListener 인터페이스를 구현하여 windowClosing 도구에서 처리를 해야 한다. 하지만 이 인터페이스는 모두 7개의 도구를 가진다. 자바에서 인터페이스를 구현한다는 것은 인터페이스의 모든 도구를 구현한다는 의미이다. (비록 그것의 몸체에는 아무 내용이 없다 하더라도 한번 더 써주어야 한다.) 이것을 한번 적어보자.

import java.awt.Frame;
import java.awt.event.*;

public class A extends Frame implements WindowListener {
    A () {
        addWindowListener(this); // Listener 클래스가 자기 자신(this)
    }
    public static void main(String[] args) {
        A a=new A();
        a.show();
    }
    public void windowClosing(WindowEvent we) {  // 유일하게 구현하고 싶은 도구
        System.out.println("before window is closed");
        System.exit(0);
    }
    /* 아래의 6개 도구는 여기서는 아무런 관심이 없지만
       WindowListener 인터페이스를 구현하는 원죄로
       몸체도 없이 코딩 품만 축내야 함. */
    public void windowClosed(WindowEvent we) {
    }
    public void windowDeiconified(WindowEvent we) {
    }
    public void windowIconified(WindowEvent we) {
    }
    public void windowActivated(WindowEvent we) {
    }
    public void windowDeactivated(WindowEvent we) {
    }
    public void windowOpened(WindowEvent we) {
    }
}

불필요한 코딩 품을 줄이기 위해 내부 클래스를 활용할 수 있도록 자바 1.1에서는 어댑터 클래스를 제공한다.
java.awt.events 패키지에 있는 이벤트 클래스들은 ComponentAdapter, ContainerAdapter, FocusAdapter, KeyAdapter, MouseAdapter, MouseMotionAdapter, WindowAdapter 모두 7개로 이들은 각각 자신의 이름에서 Adapter를 빼고 Listener를 붙인 인터페이스를 아무 몸체 없이 미리 구현해놓은 클래스들이다. 즉, 인터페이스를 구현하면 각 도구를 모두 구현해야 하지만 이미 아무 몸체 없이 구현해놓은 Adapter 클래스를 상속하면 필요한 도구만 덮어쓰면 된다.
자바 언어의 특징을 최대한 살려 코딩 품을 줄여주는 편의 클래스들인 셈이다.
자, WindowAdapter 클래스를 사용하여 위의 코드를 단순하게 해보자.

먼저 클래스 선언문의 다음 인터페이스 선언문은 삭제한다.

/* implements WindowListener */

WindowListener 인터페이스의 7개 도구는 사라지고 addActionListener의 인자(WindowEvent 처리 객체)인 WindowListener는 this가 아니라 WindowListener 인터페이스를 미리 구현해놓은 WindowAdapter 클래스를 상속한 객체가 된다.
생성자 도구에서 addActionListener 구문을 바꾸자.

A() {
    addActionListener(this);
}

대신에 다음과 같이 지역 클래스를 포함하게 된다.

1) 지역 클래스 버전

A() {
    class AWindowAdapter extends WindowAdapter {
        public void windowClosing(WindowEvent we) {
            System.out.println("before window is closed");
            System.exit(0);
        }
    } // end of class AWindowAdapter's declaration
    addWindowListener(new AWindowAdapter()); // Listener 객체가 AWindowAdapter 클래스 인스턴스
}

사실 이벤트 어댑터 클래스를 사용하는 대부분의 경우에 별도의 이름이 필요없다. 위에서도 아예 인스턴스 변수 이름조차 만들지 않았다. 이름없는 지역 클래스를 사용하면 더욱 간단해진다. 앞에서 설명했듯이 이름없는 지역 클래스는 new 예약어를 사용하여 클래스 상속을 함축한다. 다음과 같이 바뀐다.

2) 이름을 생략한 지역 클래스 버전

A() {
    addWindowListener(
        new WindowAdapter() {
            public void windowClosing(WindowEvent we) {
                System.out.println("before window is closed");
                System.exit(0);
            }
        } // end of anonymous class's declaration
    ); // Listener 객체가 이름 없는 WindowAdapter 클래스 인스턴스
}

자바 1.1의 새로운 이벤트 모델에 따라 프로그램을 작성한다면 이벤트 어댑터 클래스 사용은 거의 생활에 가까울 것이다. 또 대부분의 이벤트 어댑터는 이름없는 지역 클래스로 사용될 것이다.
내부 클래스의 문법 유형에 익숙해지기 위해서도 위와 같은 코드 형식을 반복해서 사용할 것을 권한다.

내부 클래스 학습을 돕기 위해서 위의 코드를 멤버 클래스 버전으로 옮겨 보자. 사실 이름 있는 지역 클래스 버전에서 클래스 선언문을 도구 내에서 도구 밖으로 빼내면 된다.

3) 멤버 클래스 버전

public class A extends Frame {
    class AWindowAdapter extends WindowAdapter {
        public void windowClosing(WindowEvent we) {
            System.out.println("before window is closed");
            System.exit(0);
        }
    } // end of class AWindowAdapter's declaration

    A () {
        addWindowListener(new AWindowAdapter()); // Listener 객체가 AWindowAdapter 클래스 인스턴스
    }
    public static void main(String[] args) {
        A a=new A();
        a.show();
    }
}

4) 각 버전의 클래스 파일 이름

자바 1.1은 자바 1.0과의 바이너리 호환성 등을 고려하여 내부 클래스라는 새로운 개념을 문법에 추가하면서 몇 가지 편법(?)을 사용하였다. 즉 내부 클래스는 전혀 클래스 바이너리 파일을 가지고 실행하는 자바 가상 기계에는 영향을 미치지 않도록 고려되었다. 즉, 자바 1.1의 새로운 기능을 전혀 사용하지 않았다면 내부 클래스 문법을 사용한 자바 소스 코드를 자바 1.1의 컴파일러로 컴파일한 다음 자바 1.0의 가상 기계에서 (즉, 자바 1.0의 인터프리터로) 실행하여도 실행할 수 있다는 것이다.
실제로 내부 클래스는 $ 부호를 포함한 별도의 클래스 파일로 컴파일된다.
위의 예제에서 각 버전을 컴파일하면 다음 클래스 파일들이 생성된다.

(1) 원래의 A 클래스(내부 클래스 사용하지 않은 버전)
당연하지만 하나의 클래스밖에 없으므로 하나의 클래스 파일만 생성된다.

(2) 멤버 클래스 버전
A 클래스 멤버인 AWindowAdapter 클래스는 $부호를 사용하여 A$AWindowAdapter.class 파일을 생성한다.

(3) 이름 있는 지역 클래스 버전
지역 클래스는 블록이나 도구 안에서만 정의되므로 블록이나 도구들을 구별하기 위하여 번호를 사용한다. 즉, 이 경우에는 지역 클래스가 하나이므로 A$1$AWindowAdapter.class 파일이 생성된다.

(4) 이름 없는 지역 클래스 버전
블록이나 도구에 지역적인 클래스이면서 자신의 이름이 없으므로 해당 클래스를 번호로 구별한다. 즉 지역 클래스가 하나이므로 A$1.class 파일이 생성된다.

4. static 내부 클래스

일반적으로 클래스는 static으로 선언할 수 없지만 내부 클래스는 static이 될 수 있다.
앞에서 언급했듯이 static 내부 클래스는 top level class이다.
보통의 내부 클래스는 자신을 포함하고 있는 객체에 대한 참조를 가지고 있어 포함한 객체를 클래스 이름을 사용하여 참조할 수 있다.
즉, 다음 예에서
class Enclosing {
		int var;

		class Inner { // static이 아닌 내부 클래스
				...
		}

		static class StaticInner { // static인 내부 클래스
				...
		}
}
Inner 클래스는 Enclosing 객체 인스턴스를 참조할 수 있고 멤버 필드인 var도 참조할 수 있다.
Inner 클래스에서 Enclosing 객체 인스턴스나 var 필드를 참조하려면 Enclosing.this와 Enclosing.this.var를 각각 쓰면 된다.
이렇게 일반적인 내부 클래스는 포함하고 있는 클래스의 객체 인스턴스에 연결되기 때문에
포함하고 있는 클래스의 static 메쏘드에서 생성할 수 없다.
반면, static 내부 클래스는 포함하고 있는 클래스에 연결되기 때문에 포함하는 클래스의 this에 대한 참조를 가지고 있지 않다.
static 내부 클래스는 다른 클래스에서도 다음과 같이 사용할 수 있다.
		Enclosing.StaticInner obj = new Enclosing.StaticInner();
static이 아닌 일반적인 내부 클래스는 포함하는 클래스 안에서 주로 사용될 것이다.
		new Enclosing().new Inner();

5. Object initializer block

이름을 생략한 지역 클래스의 경우 생성자를 별도로 선언할 수 없으므로 초기화를 할 필요가 있을 때 곤란해진다.
이런 경우에는 객체의 인스턴스 초기화 블록(object initializer block)을 사용할 수 있다.
이 인스턴스 초기화 블록은 static 변수를 초기화시키는 데 주로 사용하는 static initializer block과 형태가 아주 흡사하다. (다만 static이 없다.)
다음 예에서
public class InstanceInitializationBlock {
	public static void main(String[] args) {
		Thread t = new Thread() { // Thread 클래스를 상속하는 이름없는 지역 클래스 선언
				 // 인스턴스 초기화 블록
				{
					System.out.println("instance init block");
					setName("SimpleThread-" + System.identityHashCode(this));
				}
				
				public void run() {
					System.out.println("I'm running");
				}
			};

		System.out.println("thread created..");
		t.start();
	}
}
위의 예에서 이름없는 지역클래스로 선언된 쓰레드는 생성될 때 인스턴스 초기화 블록을 수행한다. 즉, 생성자 역할을 한다. 이 예에서는 쓰레드의 이름을 생성된 객체에 고유한 해시코드값으로 지정하는 일을 한다.
위의 예제를 수행한 예는 다음과 같다.
instance init block
thread created..
I'm SimpleThread-6483656...


참고 사이트 : 자바 언어 명세 중 JDK 1.1에서 변경된 사항들(Changes for Java 1.1)
;-) 내용 보강 중입니다...
자바 - 웹 프로그래밍의 진한 커피향 차례
Last modified: Thu Jan 31 21:09:30 2002