객체지향 프로그래밍에서는 클래스 간에 서로 긴밀한 관계를 맺고 상호작용합니다.
클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나, 특정 클래스와만 강하게 결합되는 경우에는 중첩 클래스로 선언하는 것이 유지보수와 가독성 측면에서 더 유리합니다.
중첩 클래스란 클래스 내부에 선언한 클래스를 말합니다.
중첩 클래스를 사용하면 외부 클래스의 멤버에 쉽게 접근할 수 있으며, 외부에는 중첩 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있습니다.
중첩 클래스는 선언 위치에 따라 두 가지로 나뉩니다.

중첩 클래스를 사용하는 가장 큰 이유는 클래스 간의 긴밀한 종속성과 구현의 논리적인 구조를 명확하게 표현하기 위해서입니다.
특히 다음과 같은 경우에 중첩 클래스를 사용합니다:
즉, 중첩 클래스에서 내부 클래스는 외부 클래스와 함께 움직이는 부품이나 구성 요소 역할을 합니다.
외부 클래스와 내부 클래스가 논리적으로 강한 결합 관계가 있고, 내부 클래스가 외부 클래스 없이는 존재할 필요가 없는 경우에 중첩 클래스를 사용해 캡슐화와 구현 은닉을 강화합니다.
자동차(Car)라는 객체가 있고, 엔진(Engine)은 자동차의 일부입니다. 자동차가 존재하지 않으면 엔진이 의미가 없습니다.
엔진만 따로 사용할 일은 거의 없고, 항상 자동차와 함께 존재하고 동작합니다.
class Car {
private String model;
Car(String model) {
this.model = model;
}
class Engine {
void start() {
System.out.println(model + " 엔진 시동 ON");
}
}
void run() {
Engine engine = new Engine(); // 내부 클래스 객체 생성
engine.start();
System.out.println(model + " 운행 중");
}
}
Map 자료구조 안에 있는 Entry는 Map이 없으면 의미가 없습니다.
Entry는 Map 내부에서 키와 값을 함께 다루는 역할을 하며, 외부에서는 별도로 쓸 일이 없기 때문에 중첩 인터페이스로 설계되었습니다.
Map<String, String> map = new HashMap<>();
map.put("key", "value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
Map.Entry로 중첩 인터페이스로 정의되어, Map을 통해서만 접근할 수 있도록 설계되었습니다.멤버 클래스는 외부 클래스 멤버로 선언되는 클래스입니다.
주로 외부 클래스와 강하게 연결된 기능을 외부에 노출하지 않고 캡슐화할 때 사용합니다.
멤버 클래스는 외부 클래스의 필드나 메서드처럼, 외부 클래스 전체에서 접근이 가능합니다.
멤버 클래스의 스코프는 외부 클래스 전체입니다.
멤버 클래스는 인스턴스 멤버 클래스, 정적 멤버 클래스로 구분됩니다.
인스턴스 멤버 클래스(Inner 클래스)는 외부 클래스의 인스턴스에 종속적인 클래스입니다.
외부 클래스 내부에 선언되며, 반드시 외부 클래스의 인스턴스가 있어야만 객체를 생성할 수 있습니다.
private 접근 제한자를 사용하여 캡슐화를 강화합니다.인스턴스 멤버 클래스의 스코프는 외부 클래스 전체입니다. 하지만 객체 생성은 외부 클래스 인스턴스가 반드시 필요합니다.
인스턴스 멤버 클래스는 외부 클래스의 인스턴스에 소속됩니다.
비유하자면, "외부 클래스가 부모, 내부 클래스가 자식"인 부모-자식 관계입니다.
내부 클래스가 외부 클래스의 인스턴스 변수와 메서드에 자유롭게 접근할 수 있는 이유도 이런 종속성 때문입니다.
class Outer {
private int data = 10;
class Inner {
void display() {
// 외부 클래스의 인스턴스 변수에 직접 접근 가능
System.out.println("외부 클래스의 data: " + data);
}
}
}
인스턴스 멤버 클래스 생성 방법은 크게 두 가지가 있습니다.
외부 클래스의 인스턴스 메서드 안에서 Inner 객체를 생성하고 사용하는 경우입니다.
class Outer {
class Inner {
void display() {
System.out.println("인스턴스 멤버 클래스");
}
}
void createInner() {
Inner inner = new Inner(); // 외부 클래스 인스턴스 메서드에서 내부 클래스 생성
inner.display();
}
}
createInner()만 호출하면 됩니다.외부에서 직접 내부 클래스를 생성하고 싶은 경우입니다.
Outer outer = new Outer(); // 외부 클래스 인스턴스 생성
Outer.Inner inner = outer.new Inner(); // 외부 인스턴스를 통해 내부 클래스 생성
inner.display(); // 내부 클래스 메서드 호출
내부 클래스를 외부에서 아예 사용할 수 없게 private으로 선언하고, 외부 클래스 내부에서만 생성 및 사용하도록 제한할 수 있습니다.
class Outer {
private class Inner { // 외부에서는 접근 불가
void display() {
System.out.println("인스턴스 멤버 클래스");
}
}
void createInner() {
Inner inner = new Inner(); // 외부 클래스 내부에서만 사용 가능
inner.display();
}
}
private 접근 제한자로 선언했습니다.만약 외부에서 private 내부 클래스 인스턴스를 직접 생성하려고 한다면 다음과 같은 오류가 발생합니다.
public class NestedClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.createInner(); // 정상 실행: 외부 클래스가 내부 클래스를 관리해줌
// 아래 코드는 컴파일 오류 발생
Outer.Inner inner = outer.new Inner(); // ❌ 오류: Inner는 private이므로 외부에서 접근 불가
inner.display();
}
}
Outer.createInner() 메서드는 내부에서 Inner 인스턴스를 생성하고 동작을 수행합니다.Outer.Inner 타입을 참조조차 할 수 없으므로, Outer.Inner inner = outer.new inner(); 구문은 컴파일 오류가 발생합니다.정적 멤버 클래스는 static 으로 선언되며, 외부 클래스의 인스턴스와 독립적으로 존재할 수 있습니다.
즉, 외부 클래스 객체 없이도 정적 멤버 클래스 객체를 생성할 수 있습니다.
정적 멤버 클래스의 스코프는 외부 클래스 전체이고, 생명주기는 외부 클래스와 별개로 존재 가능합니다. 정적 멤버 클래스는 외부 클래스 인스턴스가 필요 없습니다.
class Outer {
static class StaticInner {
void display() {
System.out.println("정적 멤버 클래스");
}
}
}
// 외부 클래스 인스턴스 없이 바로 생성 가능
Outer.StaticInner inner = new Outer.StaticInner();
inner.display();
로컬 클래스는 메서드 내부에 선언되는 클래스입니다.
선언된 메서드가 실행될 때만 클래스가 생성되고 사용할 수 있습니다.
로컬 클래스의 스코프는 선언된 메서드 내부이고, 생명주기는 메서드 실행 중에만 유효합니다 (메서드 종료 시 사라짐).
public class Outer {
void outerMethod() {
// 로컬 클래스 정의
class LocalInner {
void display() {
System.out.println("로컬 클래스");
}
}
// 로컬 클래스 객체 생성 및 메서드 호출
LocalInner local = new LocalInner();
local.display();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.outerMethod(); // 메서드 실행 중에만 LocalInner가 존재
}
}
익명 클래스는 이름이 없는 클래스로, 주로 일회성 객체를 즉시 생성하고 사용할 때 유용합니다.
로컬 클래스의 한 형태이며, 메서드 내부나 초기화 블록, 생성자 내부에서 사용됩니다.
익명 클래스의 스코프(Scope)는 생성된 위치 내부입니다.
즉, 익명 클래스를 정의하고 생성한 블록 내부에서만 참조가 가능합니다.
익명 클래스는 추상 클래스나 인터페이스를 구현하거나, 기존 일반 클래스를 상속하여 메서드를 재정의할 때 사용됩니다.
특히, 함수형 인터페이스를 구현하는 경우 익명 클래스를 람다식으로 간결하게 대체할 수 있습니다.
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("익명 클래스");
}
};
r.run(); // 익명 클래스 출력
익명 클래스 코드를 작성해보면, 중괄호({}) 뒤에 세미콜론이 반드시 필요한 것을 확인할 수 있습니다.
Predicate<String> isEmpty = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
}; // 세미콜론 필수
그 이유는 익명 클래스가 new 키워드를 통해 객체를 생성하는 "표현식(expression)"이기 때문입니다.
표현식(Expression): 실행 결과로 값을 반환하는 코드
문장(Statement): 실행 흐름을 제어하여 끝맺는 코드 (표현식 + 세미콜론)
new Predicate<String>() { ... }
이 부분이 객체를 생성하는 식(Expression)이고, 변수에 대입하거나 전달하는 값이 됩니다.
자바에서 표현식은 값이기 때문에 문장으로 끝날 때 반드시 세미콜론(;)을 붙여야 합니다.
중첩 클래스는 바깥 클래스와 긴밀하게 연결되어 있어서, 바깥 클래스의 멤버(필드, 메서드)에 접근할 수 있습니다.
| 구분 | 바깥 클래스 인스턴스 멤버 접근 | Outer.this 사용 |
|---|---|---|
| 인스턴스 멤버 클래스 (Inner Class) | ✅ 접근 가능 | ✅ 가능 |
| 정적 멤버 클래스 (Static Nested Class) | ❌ 접근 불가 | ❌ 불가 |
| 인스턴스 메서드 내부 로컬 클래스 | ✅ 접근 가능 | ✅ 가능 |
| 정적 메서드 내부 로컬 클래스 | ❌ 접근 불가 | ❌ 불가 |
중첩 클래스는 바깥 클래스의 멤버(필드, 메서드)에 접근할 수 있습니다.
하지만 어떤 중첩 클래스냐에 따라 접근 가능 범위가 다릅니다.
| 구분 | 접근 가능한 바깥 클래스 멤버 |
|---|---|
| 인스턴스 멤버 클래스 | 바깥 클래스의 모든 멤버 |
| 정적 멤버 클래스 | 바깥 클래스의 정적 멤버만 |
static 필드와 static 메서드)만 접근할 수 있습니다.class Outer {
private int instanceVar = 10;
private static int staticVar = 20;
// 인스턴스 멤버 클래스
class Inner {
void show() {
// 바깥 클래스 인스턴스 멤버와 정적 멤버 모두 접근 가능
System.out.println("인스턴스 변수: " + instanceVar);
System.out.println("정적 변수: " + staticVar);
}
}
// 정적 멤버 클래스
static class StaticInner {
void show() {
// 인스턴스 변수는 접근 불가 (컴파일 오류 발생)
// System.out.println(instanceVar); // ❌ 오류
// 정적 변수는 접근 가능
System.out.println("정적 변수: " + staticVar);
}
}
}
내부 클래스에서 바깥 클래스의 인스턴스를 명시적으로 참조하고 싶을 때는
바깥클래스이름.this 문법을 사용합니다.
특히 내부 클래스와 바깥 클래스에 동일한 이름의 필드나 메서드가 있을 경우, 혼동을 피하기 위해 사용됩니다.
class Outer {
private String name = "Outer";
class Inner {
private String name = "Inner";
void showNames() {
System.out.println("내부 클래스 name: " + this.name); // Inner
System.out.printnl("외부 클래스 name: " + Outer.this.name); // Outer
}
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.showNames();
}
}
this.name ➜ Inner 클래스 자신의 필드Outer.this.name ➜ Outer 클래스 인스턴스의 필드Outer.this를 사용하면 바깥 클래스 인스턴스를 명시적으로 참조할 수 있습니다.정적 멤버 클래스는 바깥 클래스 인스턴스와 독립적으로 존재하기 때문에 Outer.this를 사용할 수 없습니다.
static class StaticInner {
void show() {
// System.out.println(Outer.this.name); // ❌ 오류
}
}
로컬 클래스도 바깥 클래스의 인스턴스 멤버에 접근할 수 있습니다. 인스턴스 멤버 클래스와 비슷한 규칙이 적용됩니다.
| 항목 | 설명 |
|---|---|
| 바깥 멤버 접근 | 인스턴스 필드와 정적 필드 모두 접근 가능 |
| 명시적 참조 | Outer.this 사용 가능 (정적 메서드 안에서는 불가 ❌) |
| 로컬 변수 접근 | effectively final만 가능 |
class Outer {
private String outerField = "Outer Field";
void outerMethod() {
String localVar = "Local Variable"; // effectively final
class LocalInner {
private String outerField = "LocalInner Field";
void display() {
System.out.println("로컬 클래스 필드: " + outerField); // LocalInner Field
System.out.println("바깥 클래스 필드: " + Outer.this.outerField); // Outer Field
System.out.println("로컬 변수: " + localVar); // Local Variable
}
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.outerMethod();
}
}
}
Outer.this 참조 불가정적 메서드 안에서는 바깥 클래스 인스턴스가 없기 때문에 Outer.this를 사용할 수 없습니다.
정적 메서드는 클래스 자체에 소속되는 메서드로, 인스턴스를 만들지 않아도 호출이 가능합니다.
Outer.staticMethod(); // 인스턴스 없이 호출 가능
반면, Outer.this는 바깥 클래스의 인스턴스를 참조하는 키워드입니다. Outer라는 클래스의 특정 인스턴스가 생성되어 있어야만 의미가 있습니다.
그런데 정적 메서드 안에서는 애초에 바깥 클래스 인스턴스가 존재하지 않거나 존재가 보장되지 않기 때문에 Outer.this를 사용할 수 없습니다
class Outer {
private String outerField = "Outer Field";
static void staticMethod() { // 정적 메서드
class LocalInner {
void display() {
// 바깥 클래스 인스턴스가 없기 때문에 오류 발생
// System.out.println(Outer.this.outerField); // ❌ 컴파일 오류
}
}
LocalInner inner = new LocalInner();
inner.display();
}
public static void main(String[] args) {
Outer.staticMethod(); // 인스턴스 없이 호출됨!
}
}
중첩 인터페이스란 클래스 내부에서 선언된 인터페이스를 말합니다.
일반적인 인터페이스와는 달리, 특정 클래스와 긴밀한 관계가 있는 경우에 사용합니다.
특정 클래스와 밀접하게 연결된 구현체를 만들기 위해 클래스 안에 정의하고, 논리적인 종속 관계를 표현합니다.
특정 클래스 안에서만 사용될 목적으로 인터페이스를 정의하고 싶을 때 중첩 인터페이스를 사용합니다.
중첩 인터페이스는 다음과 같은 형식으로 선언합니다.
class A {
// [접근제한자] [static] interface 인터페이스이름
public static interface B {
// 상수 필드
// 추상 메서드
// 디폴트 메서드 (Java 8~)
// 정적 메서드 (Java 8~)
}
}
| 항목 | 설명 |
|---|---|
| 클래스 내부 선언 | 바깥 클래스와 논리적 종속 관계에 있는 경우 사용 |
static 키워드 | 생략해도 컴파일 시 자동으로 static이 붙음 |
| 접근 제한자 | public, protected, private 제한자를 붙일 수 있음 |
| 구현 클래스 위치 | 바깥 클래스 외부나 내부 어느 곳이든 구현 가능 |
static이 붙는 이유인터페이스는 원래 인스턴스를 만들 수 없기 때문에 인스턴스 멤버가 될 수 없습니다.
따라서 중첩 인터페이스는 항상 정적(static) 멤버로 취급됩니다. static을 생략해도 자바 컴파일러가 자동으로 붙여줍니다.
아래 예시는 Button 클래스에 클릭 이벤트를 처리하기 위한 중첩 인터페이스 OnClickListener를 선언하고 사용하는 방법입니다.
class Button {
// 중첩 인터페이스 선언
interface OnClickListener {
void onClick(); // 추상 메서드
}
private OnClickListener listener;
// 외부에서 리스너 객체를 주입받는 메서드
void setOnClickListener(OnClickListener listener) {
this.listener = listener;
}
// 버튼이 클릭되었을 때 동작하는 메서드
void click() {
if (listener != null) {
listener.onClick(); // 리스너의 onClick 실행
}
}
}
public class App {
public static void main(String[] args) {
Button button = new Button();
// 중첩 인터페이스 구현 (익명 클래스 사용)
Button.OnClickListener listener = new Button.OnClickListener() {
@Override
public void onClick() {
System.out.println("버튼이 클릭되었습니다!");
}
};
button.setOnClickListener(listener);
// 버튼 클릭 시 동작
button.click();
}
}