객체지향 프로그래밍을 하다 보면 "이 클래스는 다른 데선 절대 안 쓰이고, 이 클래스 안에서만 쓰인다"는 구조를 마주하게 된다. 문제는 이런 클래스들을 바깥에 그냥 선언하면 외부에 불필요하게 노출되고, 클래스 간 관계도 흐릿해진다는 점이다.
바로 이런 상황에서 필요한 개념이 중첩 클래스(Nested Class)다.
중첩 클래스는 왜 필요한가?
클래스를 나누는 이유는 관심사를 분리하고, 각 기능을 명확히 하기 위해서다. 그런데 클래스 간 관계가 지나치게 밀접한 경우, 굳이 바깥에 따로 빼는 것이 오히려 가독성과 유지보수성을 해칠 수 있다.
예를 들어 어떤 클래스 A가 내부적으로만 쓰는 작은 객체 B를 가지고 있고, B는 다른 클래스에서 전혀 쓰이지 않는다고 하자. 이런 경우 B를 굳이 외부에 따로 선언해놓는 것은 다음과 같은 문제를 낳는다.
- 패키지 구조가 복잡해진다.
- 외부 클래스에서 잘못 접근할 수 있다.
- 두 클래스의 관계가 코드상 드러나지 않는다.
이럴 때 중첩 클래스를 사용하면, B를 A의 안쪽에 감춰두면서 둘의 관계를 명확하게 표현할 수 있다.
중첩 클래스의 종류는 어떻게 나뉘나?
자바에서 중첩 클래스는 선언 방식에 따라 다음과 같이 네 가지로 나뉜다. 크게 보면 두 범주로 구분된다.
1. 정적 중첩 클래스 (static이 붙는 클래스)
- 바깥 클래스의 인스턴스에 소속되지 않는다.
- 바깥 클래스의 정적(static) 멤버에는 접근할 수 있다.
- 일반적으로 두 클래스가 논리적으로만 연결된 경우 사용한다.
2. 내부 클래스 (static이 없는 클래스)
- 바깥 클래스의 인스턴스에 소속된다.
- 바깥 클래스의 모든 멤버(정적, 인스턴스 포함)에 접근 가능하다.
- 바깥 클래스와 논리적으로나 실행적으로 강하게 연결된 경우에 사용한다.
이 내부 클래스는 다시 다음과 같이 세분화된다.
인스턴스 내부 클래스 : 바깥 인스턴스와 연결, 멤버처럼 동작
지역 클래스 : 매서드 내부에서 선언되는 클래스
익명 클래스 : 이름이 없는 일회성 클래스

정적 중첩 클래스 vs 내부 클래스, 뭐가 어떻게 다른가?
두 클래스의 핵심적인 차이는 바깥 클래스 인스턴스와의 관계다.
| static 키워드 | 필요함 | 없음 |
| 바깥 클래스 인스턴스 필요 여부 | 필요 없음 | 필요함 |
| 바깥 클래스의 인스턴스 변수 접근 | 불가능 | 가능 |
| 외부에서 생성할 때 | new Outer.Nested() | new Outer().new Inner() |
| 용도 | 논리적 묶음 | 구성 요소(구현 세부사항) |
이 차이는 객체 생성 방식에서도 명확히 드러난다.
// 정적 중첩 클래스
Outer.Nested nested = new Outer.Nested();
// 내부 클래스
Outer outer = new Outer(); //즉 상위를 인스턴스를 먼저 생성
Outer.Inner inner = outer.new Inner();
중첩(Nested)
어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계
내부(Inner)
나의 내부에 있는 나를 구성하는 요소
코드 예제로 이해해보자
1. 정적 중첩 클래스
public class Network {
private static class Message {
private final String content;
public Message(String content) {
this.content = content;
}
public void print() {
System.out.println(content);
}
}
public void send(String text) {
Message msg = new Message(text);
msg.print();
}
}
- Message는 오직 Network 안에서만 쓰이며, 외부에 공개할 필요가 없다.
- static이 붙어 있으므로 바깥 클래스 인스턴스 없이 생성 가능하다.
- 이런 구조는 캡슐화에 효과적이다.
2. 내부 클래스
public class Car {
private String model = "Model Y";
private int battery = 90;
private class Engine {
public void start() {
System.out.println("배터리 상태: " + battery);
System.out.println(model + "의 엔진을 구동합니다.");
}
}
public void run() {
Engine engine = new Engine();
engine.start();
}
}
- Engine은 Car 객체의 내부 동작을 책임진다.
- battery, model 같은 인스턴스 필드에 직접 접근 가능하다.
- Engine은 Car의 구성 요소이자 구현 세부사항이다.
이런 구조는 외부에 불필요한 정보 노출을 막고, Car의 책임을 명확히 분리하면서도 내부 구현은 유연하게 유지할 수 있도록 한다.
중첩 클래스를 언제 쓰는 것이 좋은가?
중첩 클래스는 아무 때나 쓰는 문법이 아니다. 다음과 같은 경우에만 사용하는 것이 권장된다.
- 해당 클래스가 바깥 클래스 안에서만 의미가 있을 때
- 두 클래스가 논리적으로 강하게 연결되어 있을 때
- 캡슐화를 강화하고 싶은 경우
반대로 여러 클래스에서 재사용되는 객체라면, 중첩 클래스로 만들면 오히려 확장성과 가독성을 해칠 수 있다. 이런 경우에는 일반 클래스 형태로 선언하는 것이 더 낫다.
분리된 클래스 구조에서 발생하는 문제점과 리팩토링의 효과
내부 클래스를 사용하기 전, Car와 Engine을 서로 완전히 독립된 외부 클래스로 분리해 두었을 때는 몇 가지 불편함이 있었다. 예를 들어 다음과 같은 방식이다.
// 외부에 선언된 Engine 클래스
public class Engine {
private Car car;
public Engine(Car car) {
this.car = car;
}
public void start() {
System.out.println("충전 레벨 확인: " + car.getChargeLevel());
System.out.println(car.getModel() + "의 엔진을 구동합니다.");
}
}
// Car 클래스
public class Car {
private String model;
private int chargeLevel;
private Engine engine;
public Car(String model, int chargeLevel) {
this.model = model;
this.chargeLevel = chargeLevel;
this.engine = new Engine(this);
}
public String getModel() {
return model;
}
public int getChargeLevel() {
return chargeLevel;
}
public void start() {
engine.start();
}
}
이 구조는 얼핏 보기엔 깔끔해 보이지만, 실제로는 여러 가지 문제를 내포하고 있다.
문제점 1. 캡슐화 약화
Engine이 Car의 정보를 사용하기 위해서는 반드시 getModel(), getChargeLevel() 같은 공개 메서드를 만들어야 한다.
하지만 이 메서드들은 엔진에서만 필요하며 외부에서는 쓰이지 않는다.
결과적으로 Car 클래스의 내부 상태가 불필요하게 외부에 노출되며, 이는 객체지향 설계 원칙인 정보 은닉(encapsulation)을 위반하게 된다.
문제점 2. 순환 참조 구조
Engine이 Car를 생성자로 전달받아 보관하고 있고, Car는 다시 Engine을 멤버 변수로 가진다.
이런 양방향 참조는 객체 관계를 복잡하게 만들고, 메모리 관리나 테스트 코드 작성 시 예기치 않은 문제를 유발할 수 있다.
문제점 3. 의미적 분리의 모호함
코드를 처음 읽는 사람은 Engine이 단독으로도 독립적인 구성 요소인지, Car의 일부인지 명확하게 알기 어렵다.
클래스가 나란히 존재하되 그 관계가 문맥적으로 드러나지 않기 때문에, 의도를 코드만으로 파악하기 힘들다.
내부 클래스로 리팩토링한 후의 이점
Engine을 Car 내부 클래스로 옮기면 이런 문제들이 자연스럽게 해소된다.
public class Car {
private String model;
private int chargeLevel;
private Engine engine;
public Car(String model, int chargeLevel) {
this.model = model;
this.chargeLevel = chargeLevel;
this.engine = new Engine();
}
public void start() {
engine.start();
System.out.println(model + " 시작 완료");
}
// 내부 클래스
private class Engine {
public void start() {
System.out.println("충전 레벨 확인: " + chargeLevel);
System.out.println(model + "의 엔진을 구동합니다.");
}
}
}
효과 1. 불필요한 메서드 제거
getModel()이나 getChargeLevel() 같은 메서드를 외부에 노출할 필요가 없어졌다.
내부 클래스는 바깥 클래스의 멤버에 직접 접근할 수 있기 때문에, 외부 노출 없이도 필요한 정보를 가져올 수 있다.
효과 2. 관계의 명확성 증가
Engine은 이제 Car의 일부로서만 존재한다는 것이 코드 구조 자체로 드러난다.
외부에서는 Engine에 직접 접근할 수 없고, 오직 Car의 행동을 통해 간접적으로만 엔진 기능이 실행된다.
설계 의도가 명확하게 코드로 표현된 것이다.
효과 3. 의존성 최소화
순환 참조 없이 Engine은 자연스럽게 바깥 Car 인스턴스를 참조할 수 있다.
객체 간 의존성이 단방향으로 정리되면서 테스트, 확장, 유지보수가 쉬워진다.
이처럼 내부 클래스로의 리팩토링은 단순한 코드 이동이 아니라, 설계적 안정성과 의미적 명확성을 높이는 중요한 도구가 될 수 있다.
특히 ‘구성 요소가 어디까지 외부에 노출돼야 하는가’라는 문제에 대해 구조적으로 답을 제시할 수 있는 수단이라는 점에서 더욱 의미가 깊다.
실무에서의 용어 사용 – 중첩 클래스 vs 내부 클래스?
이론적으로는 중첩 클래스(Nested Class)와 내부 클래스(Inner Class)를 명확히 구분해야 한다.
정적 중첩 클래스는 static 키워드가 붙어 있고, 바깥 클래스의 인스턴스에 소속되지 않는다. 반면, 내부 클래스는 바깥 클래스의 인스턴스와 연결되어 있고, 바깥 클래스의 멤버에 자유롭게 접근할 수 있다.
하지만 실무에서는 이 둘을 엄격하게 구분하지 않는 경우가 대부분이라고 한다.
현업에서는 클래스 안에 또 다른 클래스가 존재하면, 중첩 클래스 또는 내부 클래스라고 혼용해서 부른다.
정확히 말하자면 static이 붙은 정적 중첩 클래스는 내부 클래스라고 부르면 안 되지만, 용어를 철저히 따지기보다는 문맥에 따라 의미를 이해하는 관행이 더 일반적이다.
따라서 문서를 작성하거나 동료와 커뮤니케이션할 때는 “정적 중첩 클래스인지”, “내부 클래스인지” 정확히 구분해 말하는 것이 가장 좋지만,
상황에 따라 “중첩 클래스”라는 포괄적 표현 하나로 전달되는 경우도 많다는 점을 염두에 두면 좋다.
즉, 중첩 클래스는 기술적인 분류이자 포괄 개념이며, 내부 클래스는 그 하위 분류 중 하나라는 구조를 기억해두면 헷갈리지 않는다.
실제 상황에서는 코드를 보며 어떤 클래스인지 파악해서 이해하면 문제 없다.
마무리하며
중첩 클래스와 내부 클래스는 단순히 ‘클래스를 안에 넣을 수 있다’는 문법 차원이 아니라, 클래스 간의 관계를 구조적으로 표현하는 도구다.
정적 중첩 클래스는 단순한 논리적 그룹화를 위한 것이고, 내부 클래스는 바깥 클래스의 일부로서 존재하는 구성 요소다. 각각의 용도를 정확히 이해하고 사용한다면, 자바 코드의 구조는 더 명확해지고 유지보수는 훨씬 쉬워진다.
어디에 정의할 것인가를 고민하는 그 순간, 중첩 클래스를 떠올릴 수 있다면 당신은 자바의 진짜 구조적 사고에 한 발 더 가까워진 것이다.
'자바' 카테고리의 다른 글
| [JAVA] 중첩 클래스. 내부 클래스3 (2) | 2025.07.09 |
|---|---|
| [JAVA] 중첩 클래스, 내부 클래스 2 (2) | 2025.07.09 |
| [JAVA] 날짜와 시간 (1) | 2025.07.06 |
| [JAVA] 열거형(ENUM)에 대하여 (1) | 2025.06.29 |
| [JAVA] 래퍼 클레스에 대하여 (3) | 2025.06.24 |