1. 내부 클래스(inner class)란?
Java에서는 클래스가 최상위(파일) 수준뿐 아니라 다른 클래스 내부에도 선언될 수 있습니다. 이러한 클래스를 중첩 클래스(nested class)라고 합니다. 이 중에서 static 한정자 없이 선언된 클래스는 내부 클래스(non-static inner class, 또는 간단히 inner class)라고 합니다.
내부 클래스는 다른 클래스 내부에 선언되며, 그 외부 클래스의 특정 인스턴스와 연결됩니다. 외부 클래스의 모든 필드와 메서드에 접근할 수 있으며, 심지어 private 멤버에도 접근할 수 있습니다. 마치 객체마다 모든 것이 허용된 “비밀 조력자”가 있는 것과 같습니다!
집과 방에 비유할 수 있습니다. 집이 외부 클래스라면 방은 내부 클래스입니다. 방은 집 없이 존재할 수 없지만, 집의 자원(전기, 난방, 가구 등)에는 접근할 수 있습니다. 집이 사라지면 방도 사라지죠. Java의 내부 클래스도 이와 같은 방식으로 동작합니다!
내부 클래스가 필요한 이유
- 논리적 결합: 한 클래스가 다른 클래스와 함께 사용할 때만 의미가 있고, 그 밖에서는 필요하지 않을 때.
- 캡슐화: 패키지 네임스페이스를 어지럽히지 않고 구현 세부를 숨길 수 있음.
- private 멤버 접근: 내부 클래스는 외부 클래스의 private 필드와 메서드에도 접근할 수 있음.
- 간결성: 패키지 수준의 “잡다한” 클래스 수를 줄일 수 있음.
내부 클래스 선언 문법
내부 클래스 선언은 간단합니다. 다른 클래스의 본문 내부에 static 한정자 없이 선언하면 됩니다.
class Outer {
// 외부 클래스의 필드와 메서드
class Inner {
// 내부 클래스의 필드와 메서드
void printHello() {
System.out.println("Hello from Inner!");
}
}
}
여기서 Inner는 Outer의 내부 클래스입니다.
그림: 시각화
Outer
│
├─ 필드/메서드
│
└─ Inner (내부 클래스)
└─ 자체 필드/메서드
2. 내부 클래스 인스턴스 생성 방법
여기서 초보자가 자주 겪는 함정이 있습니다! 내부 클래스의 인스턴스는 항상 특정 외부 클래스 객체와 연결됩니다.
예시:
Outer outer = new Outer(); // 외부 클래스 객체 생성
Outer.Inner inner = outer.new Inner(); // 반드시 외부 객체를 통해 내부 클래스 생성!
inner.printHello(); // Hello from Inner!
그냥 new Inner()만 호출하면 컴파일 오류가 발생합니다. Java는 그 내부 클래스를 어떤 외부 클래스 객체와 연결해야 하는지 알 수 없기 때문입니다.
왜 그럴까요?
내부 클래스는 외부 클래스의 필드와 메서드에 접근할 수 있습니다. 따라서 자신이 어떤 외부 클래스 객체와 함께 동작하는지 “알고” 있어야 합니다.
3. 내부 클래스 사용 예제
내부 클래스가 실제로 유용한 예시를 살펴봅시다.
예제 1: “백팩과 아이템” 모델
예를 들어 Backpack 클래스가 있고 여기에 아이템(Item)을 담을 수 있다고 해봅시다. 그런데 아이템 클래스 Item은 Backpack 내부에서만 접근 가능하도록 만들고 싶습니다. 백팩 밖에서는 아이템이 의미가 없기 때문이죠.
public class Backpack {
private String owner;
public Backpack(String owner) {
this.owner = owner;
}
// 내부 클래스 — 아이템은 백팩 안에서만 존재!
class Item {
private String name;
public Item(String name) {
this.name = name;
}
public void printInfo() {
// 마법처럼! 외부 클래스의 private 필드에 접근
System.out.println(owner + "의 아이템: " + name);
}
}
}
사용:
Backpack bp = new Backpack("Vasya");
Backpack.Item item = bp.new Item("Java 교재");
item.printInfo(); // Vasya의 아이템: Java 교재
주의: Item은 private인 필드 owner에도 접근할 수 있습니다!
예제 2: 자체 컬렉션을 위한 이터레이터
Java의 컬렉션은 종종 내부 클래스 기반 이터레이터를 제공합니다. 간단한 컬렉션을 만들고 내부 클래스 이터레이터를 구현해 봅시다.
public class IntList {
private int[] data = new int[10];
private int size = 0;
public void add(int value) {
data[size++] = value;
}
// 내부 클래스 — 이터레이터!
class Iterator {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public int next() {
return data[index++];
}
}
}
사용:
IntList list = new IntList();
list.add(10);
list.add(20);
IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. 내부 클래스의 특징
외부 클래스 멤버에 대한 접근
내부 클래스는 외부 클래스의 모든 멤버(심지어 private 포함)에 접근할 수 있습니다. 이는 편리하지만 주의가 필요합니다. 외부 클래스가 변경되면 내부 클래스가 갑자기 “깨질” 수 있습니다.
class Outer {
private int secret = 42;
class Inner {
void showSecret() {
System.out.println("비밀: " + secret);
}
}
}
내부 클래스는 static 멤버를 가질 수 없음
내부(non-static inner) 클래스는 static 필드나 메서드를 가질 수 없습니다. 단, public static final int MY_CONST = 123; 같은 static final 상수는 예외입니다. 내부 클래스는 항상 외부 클래스의 인스턴스에 “묶여” 있기 때문입니다.
정적 중첩 클래스를 선언해야 한다면 static 한정자를 사용하세요(이에 대해서는 다음 강의에서 다룹니다).
내부 클래스의 가시성
내부 클래스는 private, protected, public이 될 수 있으며, 기본(package-private) 가시성도 사용할 수 있습니다.
public class Outer {
private class Inner { /* ... */ }
}
5. 유용한 팁
내부 클래스를 사용할 때
내부 클래스는 보기 좋으라고 쓰는 것이 아니라, 코드를 논리적으로 구성하기 위한 도구입니다. 다음과 같은 경우 사용하세요:
- 클래스가 한 곳에서만 필요할 때(예: 보조 이터레이터, 핸들러, 복잡한 구조의 일부).
- 클래스가 외부 클래스와 긴밀하게 결합되어 외부에서는 의미가 없을 때.
- 구현 세부를 패키지의 다른 클래스들로부터 숨기고 싶을 때.
유행이나 멋 때문에 내부 클래스를 쓰지 마세요! 다른 곳에서도 유용할 수 있다면 최상위 클래스로 분리하는 편이 좋습니다.
내부 클래스와 this
내부 클래스 안에서는 OuterClassName.this를 사용해 외부 클래스의 필드와 메서드에 접근할 수 있습니다.
class Car {
private String model = "Tesla";
class Engine {
void printModel() {
// 명시적으로 외부 객체에 접근
System.out.println("모델: " + Car.this.model);
}
}
}
내부 클래스와 외부 클래스가 같은 이름의 필드를 갖는 경우 특히 유용합니다.
내부 클래스를 사용하지 말아야 할 때
다음과 같은 경우에는 사용하지 마세요:
- 내부 클래스가 외부 클래스의 필드/메서드에 접근하지 않을 때
- 그 클래스가 프로그램의 다른 곳에서도 유용할 수 있을 때
- 외부 클래스가 지나치게 커지고 복잡해질 때
이런 경우에는 다음이 더 낫습니다:
- 정적 중첩 클래스(static class)로 만들기
- 클래스를 별도 파일로 분리하기
- 굳이 클래스를 만들기보다 일반 메서드로 대체하기
6. 내부 클래스를 다룰 때 흔한 실수
실수 № 1: 외부 클래스 객체 없이 내부 클래스를 생성하려는 경우.
new Inner()만 작성하면 컴파일러가 오류를 냅니다: "No enclosing instance of type Outer is accessible". 항상 외부 클래스 객체를 통해 내부 클래스를 생성하세요: outer.new Inner().
실수 № 2: 내부 클래스에 static 필드나 메서드를 선언하려는 경우.
내부 클래스는 상수를 제외하고 static 멤버를 가질 수 없습니다. 필요하다면 정적 중첩 클래스를 사용하세요. 즉, static 한정자를 붙여 선언하세요.
실수 № 3: 내부 클래스를 과도하게 사용하는 경우.
내부 클래스가 외부 클래스의 필드/메서드를 사용하지 않는다면, 아마도 static으로 만들거나 아예 밖으로 빼야 할 것입니다. “그냥” 내부 클래스를 만들지 마세요.
실수 № 4: 외부 클래스 필드 접근에서의 혼란.
내부 클래스와 외부 클래스가 같은 이름의 필드를 갖는다면, 외부 클래스의 필드에 명시적으로 접근하기 위해 OuterClassName.this.field를 사용하세요.
GO TO FULL VERSION