도도한 개발자

[Java] #16. Nested class, 내부 클래스 본문

Backend/Java

[Java] #16. Nested class, 내부 클래스

Kiara Kim 2022. 3. 24. 18:00

자바에서는 클래스 안에 클래스가 들어갈 수 있다. 이러한 클래스를 "Nested 클래스"라고 부른다. 코드를 간단하게 표현하기 위해 존재하는 이 Nested 클래스는 외부의 이벤트에 대한 처리를 하는 곳에서 가장 많이 사용된다. Nested 클래스는 선언한 방법에 따라 "Static nested 클래스"와 "내부 클래스"로 구분된다. 두 차이는 static으로 선언되었는지 여부다. 

내부 클래스는 다시 두 가지로 나뉘는데, 이름이 있는 내부 클래스는 "로컬 내부 클래스"라고 하고, 이름이 없는 클래스를 "익면 내부 클래스"라고 부른다. 그림으로 보자.

 

 

 

그럼 왜 Nested 클래스를 만들까?

 

· 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 때

· 캡슐화가 필요할 때. 즉 내부 구현을 감추고 싶을 때

· 소스의 가독성과 유지보수성을 높이고 싶을 때

 

 

* Static nested 클래스의 특징

 

내부 클래스는 감싸고 있는 외부 클래스의 어떤 변수도 접근할 수 있다. 그러나 Static nested 클래스를 그렇게 사용하는 것은 불가능하다. Static하기 때문이다. 우선 어떤 식으로 선언되는지 살펴보자.

 

public class OuterOfStatic {	// @1
	static class StaticNested {	// @2
		private int value = 0;
		public int getValue() {
			return value;
		}
		public void setValue(int value) {
			this.value = value;
		}
	}
}

 

@1 : OuterOfStatic 이라는 클래스를 선언했다.

@2 : OuterOfStatic 내부에 static으로 선언된 StaticNested라는 클래스가 선언되어 있다. 그 안에 간단히 인스턴스 변수의 값을 지정하고 조회하는 작업을 수행하는 메소드를 선언했다.

 

외부 클래스를 컴파일하면 내부의 클래스를 자동으로 컴파일해준다. 컴파일하면 다음과 같이 두 개의 클래스가 만들어진다.

 

OuterOfStatic.class
OuterOfStatic$StaticNested.class

 

그러면 이 StaticNested 클래스의 객체는 어떻게 생성할 수 있을까? NestedSample 클래스를 보자.

 

public class NestedSample {

	public static void main(String[] args) {
		NestedSample sample = new NestedSample();
		sample.makeStaticNestedObjects();
	}
	public void makeStaticNestedObjects() {
		OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested();	// @
		staticNested.setValue(3);
		System.out.println(staticNested.getValue());
	}
}

 

@부분을 보자. 이렇게 Static Nested 클래스를 만들었을 때 객체 생성은 클래스 파일 이름처럼 중간에 $를 쓰는 것이 아니라 외부 클래스 이름 뒤에 .(점)을 찍고 쓰면 된다. 객체 생성 후 사용 방법은 일반 클래스와 동일하다.

Static Nested 클래스를 만드는 이유는 클래스를 묶기 위함이다. 그래서 겉으로 보기에는 유사하지만 내부적으로 구현이 달라야 할 때 이와 같이 static nested 클래스를 사용한다.

 

 

* 내부 클래스와 익명 클래스

 

Static nested 클래스와 내부 클래스의 차이는 겉으로 보기엔  static이 있냐 없냐의 차이만 있을 뿐이다. 다음의 소스코드를 보자.

 

public class OuterOfInner {
	class Inner {
		private int value = 0;
		public int getValue() {
			return value;
		}
		public void setValue(int value) {
			this.value = value;
		}
	}
}

 

Inner 클래스의 선언부에는 StaticNested 클래스와 달리 static 선언이 없다. 이 Inner 클래스의 객체를 생성하는 방법도 다르다. InnerSample 클래스를 보자.

 

public class InnerSample {

	public static void main(String[] args) {
		InnerSample sample = new InnerSample();
		sample.makeInnerObject();
	}
	public void makeInnerObject() {
		OuterOfInner outer = new OuterOfInner();
		OuterOfInner.Inner inner = outer.new Inner();
		inner.setValue(3);
		System.out.println(inner.getValue());
	}
}

 

아래 두 줄을 보면 객체 생성 후 사용 방법에는 차이가 없다. 그러나 객체를 생성하는 방법에는 차이가 있다. 우선, Inner 클래스의 객체를 생성하기 전 Inner 클래스를 감싸고 있는 OuterOfInner 클래스의 객체를 만들어야 한다. 그리고 이 객체를 통해 Inner 클래스의 객체를 만들어 낼 수 있다. 

 

이렇게 내부 클래스를 만드는 이유는 캡슐화 때문이다. 하나의 클래스에서 어떤 공통적인 작업을 수행하는 클래스가 필요한데 다른 클래스에서는 그 클래스가 전혀 필요 없을 때 이러한 내부 클래스를 만들어 사용한다. 특히 GUI 관련 프로그램을 개발할 때 가장 많이 사용하는데, 그 중 리스너를 처리할 때 많이 사용된다. 사용자가 버튼을 클릭하거나 키보드를 입력할 때 모두 이벤트라는 것이 발생하게 된다. 

 

그런데 하나의 애플리케이션에서 어떤 버튼이 눌렸을 때 수행해야 하는 작업은 대부분 상이하다. 그러나 하나의 별도 클래스를 만들어 사용하는 것보단 내부 클래스를 만드는 것이 훨씬 편하다. 그리고, 내부 클래스를 만드는 것보다도 더 간단한 방법은 "익명 클래스"를 만드는 것이다.

 

익명리나는 것은 익명으로 Anonymous라고 하고, 이는 말 그대로 이름이 없는 클래스다. 간단한 버튼을 처리하는 예제를 통해 알아보자.

 

public class MagicButton {
	public MagicButton() {
		
	}
	private EventListener listener;
	public void setListener(EventListener listener) {
		this.listener = listener;
	}
	public void onClickProcess() {
		if(listener != null) {
			listener.onClick();
		}
	}
}

 

MagicButton 클래스는 이와 같이 간단하게 되어 있고, setListener() 메소드에서는 EventListener을 매개 변수로 받아 인스턴스 변수에 지정한다. 그리고 여기의 EventListener는 다음과 같이 선언되어 있다. 

 

public interface EventListener {
	public void onClick();
}

 

단순하게 선언된 인터페이스다. 그러면 이 클래스들을 적용해보기 위해 다음과 같이 AnonymousSample 클래스를 만들어 setButtonListener() 메소드를 만들어보자. 

 

public class AnonymousSample {

	public static void main(String[] args) {
		AnonymousSample sample = new AnonymousSample();
		sample.setButtonListener();
	}
	
	public void setButtonListener() {	
		MagicButton button = new MagicButton();
		button.setListener(----@----);
		button.onClickProcess();
	}

 

이 메소드의 중간에 @표시되어 있는 setListener() 메소드의 매개 변수에 넣을 것이 마땅치 않다. 우리가 아는 방법으로, AnonymousSample 클래스에 다음과 같이 EventListener 인터페이스를 구현한 MagicButtonListener 클래스를 만들자.

 

public class MagicButtonListener implements EventListener{
	public void onClick() {
		System.out.println("Magic Button Clicked!!");
	}
}

 

이렇게 만든 후 변경된 setButtonListener() 메소드는 다음과 같다.

 

	public void setButtonListener() {	
		MagicButton button = new MagicButton();
		MagicButtonListener listener = new MagicButtonListener();
		button.setListener(listener);
		button.onClickProcess();
	}

 

방금 사용한 것과 같이 내부 클래스로 MagicButtonListener 클래스를 별도로 만들 수도 있다. 하지만 다음과 같이 익명 클래스를 만들 수도 있다.

 

public void setButtonAnonymous() {	
	MagicButton button = new MagicButton();
	button.setListener(new EventListener() {
		public void onClick() {
			System.out.println("Magic Button Clicked!!");
		}
	});
}

 

setListener() 메소드를 보면 new EventListener()로 생성자를 호출한 후 바로 중괄호를 열었다. 그리고 그 안에 onClick() 메소드를 구현한 후 중괄호를 닫았다. 이렇게 구현한 것이 바로 "익명 클래스"다. 클래스 이름은 없지만 onClick()과 같은 메소드가 구현되어 있다. 이때 조심해야 하는 것은 괄호를 닫는 것이다. 겉보기엔 클래스를 선언한 것이지만 실제론 setListener() 메소드가 호출하는 과정 내에  익명 클래스가 있는 것이기 때문에 이와 같이 소괄호를 닫고 세미콜론을 해 줘야 한다.

 

setListener() 메소드가 호출되어 onClick() 메소드가 호출될 필요가 있을 때 그 안에 구현된 내용들이 실행된다. 그런데 이렇게 구현했을 때 클래스 이름도 없고 객체 이름도 없기 때문에 다른 클래스나 메소드에서는 참조할 수가 없다. 그래서 만약 객체를 해당 클래스 내에서 재사용하려면 다음과 같이 객체를 생성한 후 사용하면 된다.

 

public void setButtonListenerAnonymousObject() {
	MagicButton button = new MagicButton();
	EventListener listener = new EventListener() {
		public void onClick() {
			System.out.println("Magic Button Clicked!!");
		}
	};
	button.setListener(listener);
	button.onClickProcess();  
}

 

내부 클래스만으로도 편한데 왜 익명 클래스를 사용할까? 클래스를 만들고 그 클래스를 호출하면 그 정보는 메모리에 올라간다. 즉, 클래스를 많이 만들면 만들수록 메모리는 많이 필요해지고 애플리케이션을 시작할 때 더 많은 시간이 소요된다. 따라서, 자바에서는 이렇게 간단한 방법으로 객체를 생성할 수 있도록 했다. 

 

 

* 꼭 알아야 할 Nested 클래스 특징

 

Static Nested 클래스에서는 감싸고 있는 클래스의 static 변수만 참조할 수 있고 부모 클래스에 static하지 않은 변수를 참조할 수는 없다. 그러나 static nested 클래스와는 다르게, 내부 클래스와 익명 클래스는 감싸고 있는 클래스의 어떤 변수라도 참조할 수 있다. 그렇다면 반대로, 감싸고 있는 클래스에서 Static Nested 클래스의 인스턴스 변수나 내부 클래스의 인스턴스 변수로의 접근은 가능할까? 물론 가능하다. 

 

 

* 실습

 

public class InputBox {
	public InputBox() {
		
	}
	
	KeyEventListener listener;
	public void setKeyListener(KeyEventListener listener) {
		this.listener = listener;
	}
	
	public static final int KEY_DOWN = 2;
	public static final int KEY_UP = 4;
	
	public void listenerCalled(int eventType) {
		if(eventType == KEY_DOWN) {
			listener.onKeyDown();
		}else if(eventType == KEY_UP) {
			listener.onKeyUp();
		}
	}
}

 

public interface KeyEventListener {
	public void onKeyDown();
	public void onKeyUp();
}

 

public class MyPage {
	InputBox input; 

	public static void main(String[] args) {
		MyPage page = new MyPage();
		page.setUI();
		page.pressKey();
	}
	
	public void setUI() {
		input = new InputBox();
		KeyEventListener listener = new KeyEventListener() {
			@Override
			public void onKeyUp() {
				System.out.println("Key Up");
			}
			@Override
			public void onKeyDown() {
				System.out.println("Key Down");
			}
		};
		input.setKeyListener(listener);
	}
	
	public void pressKey() {
		input.listenerCalled(InputBox.KEY_DOWN);
		input.listenerCalled(InputBox.KEY_UP);
	}
}

 

 

* 정리

 

Nested 클래스에 속하는 3가지 클래스에는 어떤 것들이 있나요?
Nested 클래스는 Static nested class, Local inner class, Anonymous inner class로 나뉜다.

Nested 클래스를 컴파일하면 Nested클래스 파일의 이름은 어떻게 되나요?
Nested 클래스를 컴파일하면 "감싼클래스$Nested클래스.class"가 생성된다.

Static Nested 클래스는 다른 Nested 클래스와 어떤 차이가 있나요?
Static nested 클래스와 다른 nested 클래스의 차이점은 객체를 생성하는 방법이 다르다는 것이다.

StaticNested 클래스의 객체 생성은 어떻게 하나요?
OuterClass클래스 내에 StaticNestedClass 이라는 static nested 클래스가 있다면,
OuterClass.StaticNestedClass staticNested=new OuterClass.StaticNestedClass();
와 같이 선언한다.

일반적인 내부 클래스의 객체 생성은 어떻게 하나요?
OuterClass클래스 내에 NestedClass 라는 inner 클래스가 있다면,
OuterClass outer=new OuterClass();
OuterClass.NestedClass nested=outer.new NestedClass();
로 선언한다. 

Nested 클래스를 만드는 이유는 무엇인가요?
Nested 클래스를 생성하는 이유는 다음과 같다.
- 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 때
- 캡슐화가 필요할 때(예를 들어 A 라는 클래스에 private 변수가 있다. 이 변수에 접근하고 싶은 B라는 클래스를 선언하고, B 클래스를 외부에 노출시키고 싶지 않을 경우가 여기에 속한다.) 
- 소스의 가독성과 유지보수성을 높이고 싶을 때 

Nested 클래스에서 감싸고 있는 클래스의 private 로 선언된 변수에 접근할 수 있나요?
내부 클래스와 익명 클래스는 감싸고 있는 클래스의 어떤 변수라도 참조할 수 있다. 

감싸고 있는 클래스에서 Nested 클래스에 선언된 private 로 선언된 변수에 접근할 수 있나요? 

감싸고 있는 클래스에서 Static Nested 클래스의 인스턴스 변수나 내부 클래스의 인스턴스 변수로의 접근하는 것도 가능하다.

 

 

그럼 이만.