6.5 내부 클래스 (Inner Class)
클래스 안에 또 다른 클래스를 정의하는 것을 내부 클래스(Inner Class) 라고 합니다. 외부에서는 불필요하고 특정 클래스와 강하게 연관된 코드를 논리적으로 그룹화할 때 사용합니다.
내부 클래스의 종류
| 종류 | 선언 위치 | 특징 |
|---|---|---|
| 멤버 내부 클래스 | 클래스 멤버로 | 외부 클래스의 모든 멤버에 접근 가능 |
| 정적 내부 클래스 | static 붙여서 | 외부 클래스의 static 멤버만 접근 가능 |
| 지역 내부 클래스 | 메서드 내부에 | 해당 메서드 안에서만 사용 |
| 익명 내부 클래스 | 클래스 선언과 동시에 인스턴스 생성 | 일회성 구현에 사용 |
1. 멤버 내부 클래스
가장 일반적인 형태입니다. 외부 클래스의 모든 멤버(private 포함) 에 접근할 수 있습니다.
public class Outer {
private String outerField = "외부 클래스 필드";
class Inner {
void display() {
// 외부 클래스의 private 필드에도 접근 가능!
System.out.println("내부에서 접근: " + outerField);
}
}
}
// 사용 방법: 외부 클래스 객체를 먼저 만들어야 함
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.display(); // 내부에서 접근: 외부 클래스 필드
2. 정적 내부 클래스 (Static Nested Class)
static이 붙은 내부 클래스입니다. 외부 클래스 인스턴스 없이 사용할 수 있습니다. 외부 클래스의 static 멤버에만 접근할 수 있습니다.
public class Database {
private static String url = "jdbc:mysql://localhost";
// 정적 내부 클래스: Builder 패턴에서 자주 사용
static class Builder {
private String host = "localhost";
private int port = 3306;
private String dbName;
Builder host(String host) { this.host = host; return this; }
Builder port(int port) { this.port = port; return this; }
Builder dbName(String db) { this.dbName = db; return this; }
Database build() {
return new Database(host, port, dbName);
}
}
private Database(String host, int port, String dbName) {
System.out.printf("연결: %s:%d/%s%n", host, port, dbName);
}
}
// 사용: 외부 클래스 객체 없이 바로 사용 가능
Database db = new Database.Builder()
.host("myserver.com")
.port(5432)
.dbName("shop")
.build();
실무에서 가장 많이 쓰이는 형태입니다.
Builder패턴,Entry클래스(예:Map.Entry<K,V>)가 대표적입니다.
3. 지역 내부 클래스
메서드 안에서 선언되며, 그 메서드 안에서만 사용할 수 있습니다. 잘 사용되지 않으며, 대부분의 경우 람다식으로 대체됩니다.
public class LocalInnerExample {
void doSomething() {
final int localVar = 100; // effectively final이어야 함
class LocalHelper {
void help() {
System.out.println("지역 변수 사용: " + localVar);
}
}
LocalHelper helper = new LocalHelper();
helper.help(); // 지역 변수 사용: 100
}
}
4. 익명 내부 클래스 (Anonymous Inner Class)
이름 없이 선언과 동시에 인스턴스를 만드는 클래스입니다. 인터페이스나 추상 클래스를 일회성으로 구현할 때 사용합니다. 자바 8 이후로는 대부분 람다식으로 대체되었습니다.
interface Greeting {
void greet(String name);
}
public class AnonymousExample {
public static void main(String[] args) {
// 익명 내부 클래스 (Java 8 이전 방식)
Greeting formal = new Greeting() {
@Override
public void greet(String name) {
System.out.println("안녕하세요, " + name + " 님.");
}
};
// 람다식 (Java 8+ 권장)
Greeting casual = name -> System.out.println("안녕, " + name + "!");
formal.greet("홍길동"); // 안녕하세요, 홍길동 님.
casual.greet("철수"); // 안녕, 철수!
}
}
실전 예제: 이벤트 핸들러 (Swing 스타일)
import javax.swing.*;
import java.awt.event.*;
JButton button = new JButton("클릭!");
// 익명 클래스로 버튼 클릭 이벤트 처리
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("버튼이 클릭되었습니다!");
}
});
// 람다식으로 동일하게 (Java 8+)
button.addActionListener(e -> System.out.println("버튼이 클릭되었습니다!"));
고수 팁
내부 클래스 선택 가이드:
- 멤버 내부 클래스: 외부 인스턴스의 상태에 강하게 의존할 때 (예: 반복자 구현)
- 정적 내부 클래스:
Builder패턴, 외부 클래스와 논리적으로 연관된 헬퍼 클래스 → 가장 많이 쓰임 - 익명 내부 클래스: 람다가 지원되지 않는 인터페이스(추상 메서드 2개 이상) 또는 레거시 코드
- 람다식: 함수형 인터페이스(추상 메서드 1개)를 구현할 때는 무조건 람다 사용 권장
메모리 주의: 멤버 내부 클래스 인스턴스는 외부 클래스 인스턴스에 대한 참조를 암묵적으로 보유합니다. 외부 객체를 오래 살아남게 하여 메모리 누수를 일으킬 수 있으므로, 외부 인스턴스가 필요 없다면 항상 static nested class를 선택하세요.