2025. 5. 28. 11:56ㆍJAVA/JAVA 기초 문법
🖥️ 중첩 클래스란?
중첩 클래스란, 클래스 내부에 정의된 클래스(자바 프로그래밍 언어에서는 클래스 내에 다른 클래스를 정의할 수 있다)를 말한다. 다른 클래스의 멤버처럼 존재하는 클래스로, 외부 클래스와 논리적으로 밀접한 관계가 있을 때 사용한다.
public class OuterClass {
...
class NestedClass {
...
}
}
Non-static 중첩 클래스는 inner 클래스라고 한다.
static으로 선언된 중첩 클래스들은 static nested 클래스하고 한다.
외부 클래스 → 내부 클래스를 정의한, 감싸고 있는 클래스.
📂 중첩 클래스의 분류
| 유형 | static 여부 | 외부 클래스 멤버 접근 | 주요 특징 |
| non-static 중첩 클래스 (inner 클래스) |
❌ static 아님!! | ✅ 외부 인스턴스 멤버 접근 가능 (private으로 선언되어도 가능) |
외부 클래스 객체와 강하게 연결됨 |
| static 중첩 클래스 | ✅ static | ❌ 외부 인스턴스 멤버 접근 불가 | 외부 클래스의 static 멤버처럼 사용 |
| 지역 클래스 (Local Class) |
❌ | ✅ (제한적) | 메서드/블록 내부에 정의됨 |
| 익명 클래스 (Anonymous Class) |
❌ | ✅ (제한적) | 클래스 이름 없이 일회성 사용 |
- Non-static 중첩 클래스(inner 클래스)는 이 클래스를 포함하고 있는 외부 클래스의 다른 멤버에 접근할 수 있으며,
그 멤버들이 private으로 선언되어 있더라도 접근가능
→ inner 클래스는 외부 클래스의 멤버처럼 동작하고, 컴파일러가 내부적으로 접근 경로를 만들어줌 - 반면, static 중첩 클래스는, 이 클래스를 포함하고 있는 외부 클래스의 다른 멤버에 접근할 수 없다.
→ 외부 클래스의 static 멤버처럼 작동, 외부 클래스의 인스턴스와 독립적 - Outer class의 멤버로서,
중첩 클래스는 private, public, protected, package-private(default)로 선언될 수 있다.
(외부 클래스는 public 또는 package-private으로만 선언될 수 있다.)
📖 왜 중첩 클래스를 사용할까?
- 논리적 그룹화 : 특정 클래스에서만 사용될 클래스를 외부로 분리하지 않고 그룹화
- 캡슐화 강화 : 외부에서 직접 접근이 불가능하여 은닉성 증가
- 코드 간결화 : 이벤트 처리 등 간단한 구현에 유리(특히 익명 클래스)
📃 예제
public class Person {
private String name;
private int age;
private Address address;
public Person() {}
public Person(String name, int age, String city) {
this.name = name;
this.age = age;
this.address = new Address(city);
}
public void print() {
System.out.println("이름 : " + name);
address.printAddress();
}
// 중첩 클래스 : Address는 Person 내부에서만 의미가 있음
public class Address {
private String city;
public Address() {}
public Address(String city) {
this.city = city;
}
public void printAddress() {
System.out.println("주소 : " + city);
}
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("Bob", 20, "Seoul");
person.print();
// ❌ 외부에서 Address에 직접 접근할 수 없음(캡슐화)
// Person.Address address = new Person.Address("Bob", 20, "Seoul");
// 불가능 -> non-static 중첩 클래스는 외부에서 직접 생성 어렵고, 접근 자체도 원칙적으로 막아야 함
}
}
출력시
이름 : Bob
주소 : Seoul
📖 내부 클래스에서의 Serialization(직렬화)
Java에서 Serialization(직렬화)는 객체를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있도록 만드는 과정이다. 자바 컴파일러가 내부 클래스와 같은 특정 구조를 컴파일할 때, 소스 코드에 해당 구조가 없는 합성 구조를 생성한다. 합성 구조에는 클래스, 메서드, 필드 및 기타 구조가 포함된다.
그런데 내부 클래스는 외부 클래스와 강한 연결을 갖고 있기 때문에, 직렬화할 때 특별한 주의가 필요하다.
⌨️ Inner Class
Inner Class는 다른 클래스 내부에 정의된 클래스다. Java에서는 특정 클래스가 다른 클래스와 밀접하게 연관된 경우, 이를 논리적으로 묶기 위해 내부 클래스를 사용한다.
내부 클래스는 외부 클래스 객체의 메소드와 필드에 직접 접근할 수 있다. 또한, 내부 클래스는 외부 클래스 인스턴스와 연관되어 있기 때문에, 스스로 어떤 static 멤버도 정의할 수 없다!!
내부 클래스의 인스턴스 객체들은 외부 클래스의 인스턴스 내부에 존재한다.
public class OuterClass {
private int outerField;
private InnerClass innerClassInstance; // InnerClass 객체 참조 변수
public OuterClass(int outerField) {
this.outerField = outerField;
this.innerClassInstance = new InnerClass(); // 디폴트 생성자로 초기화
}
// 외부 클래스의 인스턴스 메서드
public void outerMethod() {
System.out.println("Outer class method.");
}
// non-static 내부 클래스
public class InnerClass {
private int innerField;
// 디폴트 생성자
public InnerClass() {
this.innerField = 0;
}
// 파라미터를 받는 생성자
public InnerClass(int innerField) {
this.innerField = innerField;
}
// 내부 클래스의 메서드
public void innerMethod() {
// 내부 클래스는 외부 클래스의 필드와 메서드에 접근할 수 있음
System.out.println("Outer field: " + outerField);
outerMethod();
// 내부 클래스의 자체 필드에 접근
System.out.println("Inner field: " + innerField);
// this 키워드 사용
System.out.println("Inner class this: " + this);
System.out.println("Outer class this: " + OuterClass.this);
}
// 내부 클래스에서 외부 클래스의 메서드를 호출하는 메서드
public void callOuterMethod() {
OuterClass.this.outerMethod();
}
// 외부 클래스의 필드를 수정하는 메서드
public void modifyOuterField(int newValue) {
OuterClass.this.outerField = newValue;
System.out.println("Outer field modified to: " + outerField);
}
}
// OuterClass에서 InnerClass의 메서드 호출
public void callInnerMethod() {
innerClassInstance.innerMethod();
}
// OuterClass에서 InnerClass 인스턴스를 변경하는 메서드
public void setInnerClassInstance(int innerField) {
this.innerClassInstance = new InnerClass(innerField);
}
// OuterClass에서 InnerClass 인스턴스를 생성하는 메서드
public InnerClass createInnerClassInstance(int innerField) {
return new InnerClass(innerField);
}
}
public static void testInnerClasses() {
OuterClass outer = new OuterClass(10);
// 디폴트 생성자를 사용하여 비정적(non-static) 내부 클래스의 인스턴스 생성
// OuterClass 외부에서 OuterClass의 non-static nested class를 생성하려면,
// 항상 OuterClass 인스턴스를 먼저 생성하여야 함!!!
InnerClass inner1 = outer.new InnerClass();
inner1.innerMethod();
// 파라미터를 전달받는 생성자를 사용하여 내부 클래스의 인스턴스 생성
InnerClass inner2 = outer.new InnerClass(20);
inner2.innerMethod();
// 외부 클래스의 메서드를 내부 클래스에서 호출
inner2.callOuterMethod();
// 내부 클래스에서 외부 클래스의 필드를 수정
inner2.modifyOuterField(100);
// 외부 클래스에서 내부 클래스의 메서드 호출
outer.callInnerMethod();
// 외부 클래스에서 내부 클래스의 인스턴스를 변경
outer.setInnerClassInstance(50);
outer.callInnerMethod();
// 외부 클래스에서 내부 클래스의 인스턴스를 생성하는 메서드 사용
InnerClass inner3 = outer.createInnerClassInstance(30);
inner3.innerMethod();
}
InnerClass의 인스턴스는 OuterClass의 인스턴스 내부에서만 존재할 수 있고, InnerClass의 인스턴스를 포함하는 OuterClass 인스턴스의 메소드와 필드에 직접 접근할 수 있다.
내부 클래스를 인스턴스화 하려면, 먼저 외부 클래스를 인스턴스화 해야 한다. 그 다음, 외부 객체 내부에 내부 객체를 생성한다.
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
⌨️ Static Nested Class(정적 중첩 클래스)
Static Nested Class는 외부 클래스 내부에 선언되지만, 정적(static)으로 선언된 중첩 클래스로 외부 클래스에서 정의된 인스턴스 필드나 메소드를 참조할 수 없다. 오직 객체 참조를 통해서만 사용할 수 있다.
즉, 외부 클래스의 인스턴스 없이도 독립적으로 사용 가능한 클래스!!
public class OuterClass {
...
static class StaticNestedClass {
...
}
}
💬 참고 : 정적 중첩 클래스는 다른 최상위 클래스들처럼 정적 중첩 클래스를 포함하는 외부 클래스의 인스턴스 멤버들과 상호작용합니다. 실제로, 정적 중첩 클래스는 행동적으로는 최상위 클래스이지만, 패키징의 편의를 위해 다른 최상위 클래스에 중첩된 클래스입니다.
정적 중첩 클래스의 인스턴스 방법은 최상위 클래스를 인스턴스화 하는 방법과 동일하다
StaticNestedClass staticNestedClass = new StaticNestedClass();
📃 InnerClass와 Nested Static Class 예제
다음 예제는 OuterClass와 TopLevelClass는 외부 클래스(OuterClass)의 멤버 중 어떤 것들이 내부 클래스(InnerClass), 중첩 정적 클래스(StaticNestedClass), 그리고 최상위 클래스(TopLevelClass)에 접근할 수 있는지 보여준다.
public class OuterClass {
// 인스턴스 필드
String outerField = "Outer field";
// 정적(static) 필드
static String staticOuterField = "Static outer field";
// 정적 중첩 클래스 인스턴스 참조 변수
StaticNestedClass staticNestedInstance;
// 생성자
public OuterClass() {
// 정적 중첩 클래스 인스턴스를 생성하여 필드에 저장
staticNestedInstance = new StaticNestedClass();
}
// ✅ 인스턴스 내부 클래스 (Inner Class)
class InnerClass {
void accessMembers() {
// 외부 클래스의 인스턴스 필드와 정적 필드 모두 접근 가능
System.out.println("InnerClass accessing: " + outerField);
System.out.println("InnerClass accessing: " + staticOuterField);
}
}
// ✅ 정적 중첩 클래스 (Static Nested Class)
static class StaticNestedClass {
void accessMembers(OuterClass outer) {
// 외부 클래스의 인스턴스 필드에는 외부 인스턴스를 통해서만 접근 가능
System.out.println("StaticNestedClass accessing: " + outer.outerField);
// 외부 클래스의 정적 필드는 직접 접근 가능
System.out.println("StaticNestedClass accessing: " + staticOuterField);
}
}
// static nested class는 사실상 outer class의 인스턴스와 상관 없다.!!
// -> outer class의 인스턴스를 만들지 않아도, 스스로 인스턴스를 만들수 있어서
// 외부에서 정적 중첩 클래스의 메서드를 호출
public void callStaticNestedMethod() {
staticNestedInstance.accessMembers(this);
}
}
public class TopLevelClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field OuterClass.outerField
// System.out.println(OuterClass.outerField);
System.out.println("TopLevelClass accessing: " + outer.outerField);
System.out.println("TopLevelClass accessing: " + OuterClass.staticOuterField);
}
}
import com.Tistory.NestedClassExample.nestedclass.OuterClass.StaticNestedClass;
public class Main {
public static void main(String[] args) {
System.out.println("Inner class:");
System.out.println("------------");
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
innerObject.accessMembers();
System.out.println("\nStatic nested class:");
System.out.println("--------------------");
StaticNestedClass staticNestedObject = new StaticNestedClass();
staticNestedObject.accessMembers(outerObject);
System.out.println("\nStatic nested class accessed from OuterClass:");
System.out.println("---------------------------------------------");
outerObject.callStaticNestedMethod();
System.out.println("\nTop-level class:");
System.out.println("----------------");
TopLevelClass topLevelObject = new TopLevelClass();
topLevelObject.accessMembers(outerObject);
}
}
출력시
Inner class:
------------
InnerClass accessing: Outer field
InnerClass accessing: Static outer field
Static nested class:
--------------------
StaticNestedClass accessing: Outer field
StaticNestedClass accessing: Static outer field
Static nested class accessed from OuterClass:
---------------------------------------------
StaticNestedClass accessing: Outer field
StaticNestedClass accessing: Static outer field
Top-level class:
----------------
TopLevelClass accessing: Outer field
TopLevelClass accessing: Static outer field
이처럼 정적 중첩 클래스는 다른 최상위 클래스처럼 외부 클래스의 인스턴스 멤버와 상호 작용한다.
> 💬 요약: Static Nested Class
> - 외부 클래스 내부에서 static으로 정의된 중첩 클래스
> - 외부 클래스 인스턴스 없이 독립적으로 생성 가능
> - 외부 클래스의 static 멤버는 직접 접근, 인스턴스 멤버는 외부 참조 필요
> - 주로 유틸 클래스, 논리적 그룹화를 위한 구조에 적합
🔍 Inner Class vs Static Nested Class
| Inner Class | Static Nested Class | |
| 선언 방식 | class Inner {} | static class staticNested {} |
| 외부 인스턴스 필요 | ✅ 필요 | ❌ 불필요 |
| 외부 인스턴스 필드 접근 | 직접 가능 | 외부 인스턴스 전달해야 접근 가능 |
| 메모리 사용 | 외부 클래스 참조를 가져 더 무거움 | 독립적으로 가벼움 |
| 사용 예 | UI 이벤트 핸들러, 일회성 연산 | 유틸 클래스, 데이터 그룹 구조 |
💥 Shadowing 문제 발생 가능 💥
외부 범위의 변수/필드와 같은 이름의 변수를 내부 범위에 다시 선언하거나 정의함으로 외부 변수에 지접 접근이 가려지는 현상이 발생할 수 있다.
요악하면 "같은 이름의 지역 변수나 필드를 안쪽에서 다시 선언하면, 바깥쪽 건 안 보이게 된다"
public class OuterClass {
static String name = "Outer";
static class StaticNested {
String name = "StaticNested"; // 외부 static 변수 'name'을 쉐도잉함
void printNames() {
// StaticNested의 name
System.out.println("this.name = " + name);
// Outer 클래스의 name (명시적 접근)
System.out.println("Outer.name = " + OuterClass.name);
}
}
}
public class Main {
public static void main(String[] args) {
OuterClass.StaticNested nested = new OuterClass.StaticNested();
nested.printNames();
}
}
출력시
this.name = StaticNested
Outer.name = Outer
🤔 왜 문제일까?
| ❗ 코드 혼란 | 겉보기엔 같은 이름을 써도, 실제로 어떤 값이 참조되는지 모호해짐 |
| ❗ 접근 실수 | 의도는 외부 필드 접근이었지만 내부 필드가 가려서 엉뚱한 값 사용 가능 |
| ❗ 버그 유발 | 사용자가 실수로 내부 필드가 외부 필드를 덮어쓴 사실을 모르면 버그 발생 가능 |
🛠️ 해결 방법
1. 명시적으로 외부 클래스 이름을 붙여서 사용
Outer.name // 정적 멤버 접근
outer.outerField // 인스턴스 멤버 접근
2. 다른 이름으로 변수/필드 선언
// 이름을 다르게 지정해 충돌 자체를 피함
String nestedName = "StaticNested";
3. this 또는 OuterClass.this 사용(내부 클래스일 경우)
public class Outer {
String name = "Outer";
class Inner {
String name = "Inner";
void printNames() {
System.out.println("name = " + name); // Inner의 name
System.out.println("this.name = " + this.name); // Inner의 name
System.out.println("Outer.this.name = " + Outer.this.name); // Outer의 name
}
}
}
⌨️ Local and Anonymous Classes
내부 클래스에는 두가지 추가적인 유형이 있다.
메서드의 본문 내에 내부 클래스를 선언할수 있다. 이러한 클래스들은 로컬 클래스로 알려져 있다.
또한, 클래스의 이름을 지정하지 않고 메서드 본문 내에 내부 클래스를 선언할 수도 있다.
이러한 클래스들은 익명 클래스로 알려져 있다.
▶️ Local Class(지역클래스)
로컬 클래스는 균형 잡힌 중괄호 사이에 있는 하나 이상의 statement 그룹인 블록 내에 정의된 클래스로, 메소드 본문 내에서 로컬 클래스를 정의하는 것을 볼 수 있다.
해당 메서드의 지역 변수처럼 동작하며, 오직 해당 메서드 내에서만 사용된다!!
로컬 클래스는 어떤 블록 내에서도 정의할 수 있다.
예를 들어, 메서드 본문, for 루프, if 절에서 로컬 클래스를 정의할 수 있다.
public class Outer {
void doSomething() {
int num = 10;
// 지역 클래스 정의
class LocalPrinter {
void print() {
System.out.println("num = " + num);
}
}
// 지역 클래스 인스턴스 생성 및 사용
LocalPrinter printer = new LocalPrinter();
printer.print();
}
}
🔶 특징
- 클래스 이름 존재함 -> 이 예제에서는 LocalPrinter
- 메서드 내부에서만 정의되고 사용됨
- 로컬 클래스를 포함하는 클래스(외부 클래스)의 멤버에 접근 가능
- 로컬 변수에도 접근가능, 단 final로 선언된 로컬 변수에만 접근가능
❓ 왜 Local Class는 static으로 정의할 수 없을까?
로컬 클래스는 어떠한 정적 멤버(static)도 정의하거나 선언할 수 없기 때문에 inner 클래스와 유사하다.
로컬 클래스는 로컬 클래스를 포함하는 블록의 인스턴스 멤버에 접근할 수 있기 때문에 non-static입니다. 따라서 대부분의 종류의 static 선언을 포함할 수 없다.
즉, 로컬 클래스는 메서드 내부에서 정의되며, 외부 클래스의 인스턴스와 밀접하게 연결되어 있기 때문이다.
따라서 static 처럼 독립적인 성격을 가질 수 없다.
▶️ Anonymous Class (익명 클래스)
익명 클래스는 이름이 없는 클래스로, 일회성 객체를 즉석에서 정의하고 생성한다.
주로 인터페이스나 추상 클래스를 상속 또는 구현할 때 사용된다
public class Main {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Runnable 실행");
}
};
r.run();
}
}
🔶 특징
- 클래스 이름 없음 (이름 없이 정의)
- 즉시 인스턴스 생성됨
- 인터페이스 구현 또는 추상 클래스 상속이 필수
- 내부에 생성자 정의 불가 (상속은 가능하나 생성자 호출 불가)
🔍 Local Class vs Anonymous Class
| Local Class | Anonymous Class | |
| 클래스 이름 | ✅ 있음 | ❌ 없음 |
| 재사용성 | ✅ 있음 (메서드 내에서 여러 번 사용 가능) | ❌ 없음 (일회성) |
| 구현/상속 제약 | 일반 클래스처럼 자유로움 | 반드시 인터페이스 구현 or 클래스 상속 |
| 생성자 사용 | ✅ 가능 | ❌ 불가 |
| 코드 길이 | 다소 길어질 수 있음 | 간결함(람다 스타일) |
| 사용 목적 | 복잡한 지역 클래스 구조 정의 | 짧고 간단한 일회성 동작 구현 |
🧭 결론
중첩 클래스는 외부 클래스 내부에 정의되는 클래스
◾ static 유무에 따라 크게 Static Nested Class vs Inner Class로 나뉨
◾ Inner Class는 외부 인스턴스와 연결됨, Static은 독립적으로 동작
◾ Local / Anonymous 클래스는 메서드 내부에서 임시 용도로 사용
◾ 용도에 따라 적절한 중첩 클래스를 선택하면 구조적이고 유지보수 쉬운 코드를 만들 수 있음
'JAVA > JAVA 기초 문법' 카테고리의 다른 글
| Anonymous Class(익명 클래스) (0) | 2025.05.28 |
|---|---|
| Explicit(명시적)과 Implicit(암시적) (0) | 2025.05.14 |
| Control Flow Statements (1) | 2025.05.14 |
| Operators (연산자) (0) | 2025.04.30 |
| Array(배열) (0) | 2025.04.23 |