본문 바로가기
카테고리 없음

[java-mid1] 7. 중첩 클래스, 내부 클래스1

by Lpromotion 2025. 3. 24.
"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.

목차
 

1. 중첩 클래스, 내부 클래스란?

중첩 클래스(Nested Class)는 클래스 안에 정의된 또 다른 클래스를 말한다.

마치 반복문 안에 반복문을 두는 중첩 for문처럼, 클래스 내부에 클래스가 위치한다는 점에서 유래된 개념이다.

class Outer {
    class Nested {
        ...
    }
}

 

중첩 클래스의 분류

중첩 클래스는 선언된 위치에 따라 다음과 같이 분류된다.

 

중첩 클래스는 정의 위치에 따라 4가지로 구분된다. 이들은 크게 정적 중첩 클래스내부 클래스 종류 두 갈래로 나뉜다.
분류 기준 중첩 클래스 종류 설명
바깥 클래스 기준 정적 중첩 클래스 (Static Nested Class) static 키워드로 선언되며, 바깥 클래스의 인스턴스에 소속되지 않는다.
바깥 클래스의
인스턴스 기준
내부 클래스 (Inner Class) 바깥 클래스의 인스턴스에 소속되며, static 키워드 없이 정의된다.

 

내부 클래스 종류

  • 내부 클래스 (Inner Class): 바깥 클래스의 인스턴스에 소속되며, 바깥 클래스의 인스턴스 변수 및 메서드 접근 가능
  • 지역 클래스 (Local Class): 메서드 블록 안에서 선언되며, 지역 변수에 접근 가능
  • 익명 클래스 (Anonymous Class): 이름 없이 정의되는 지역 클래스의 특수한 형태

 

중첩 클래스와 변수 선언 위치의 대응

중첩 클래스는 변수의 선언 위치와 매우 유사하게 위치가 결정된다.

변수 선언 위치 중첩 클래스 종류
정적 변수 위치 (클래스 영역) 정적 중첩 클래스
인스턴스 변수 위치 내부 클래스
지역 변수 위치 지역 클래스 (익명 클래스 포함)

예시

class Outer {
    static class StaticNested { } // 정적 중첩 클래스
    class Inner { }              // 내부 클래스

    public void process() {
        int localVar = 0;
        class Local { }         // 지역 클래스
        Local local = new Local();
    }
} 

 

정적 중첩 클래스 vs 내부 클래스

구분 정적 중첩 클래스 내부 클래스
static 키워드 O X
바깥 클래스 인스턴스에 소속 X O
바깥 클래스의 정적 멤버 접근 가능 가능
바깥 클래스의 인스턴스 멤버 접근 불가능 가능

즉, 정적 중첩 클래스는 바깥 클래스와 아무 관련이 없는 독립적인 클래스이며,
내부 클래스는 바깥 클래스의 인스턴스를 구성하는 요소이다.

 

중첩(Nested)과 내부(Inner)의 의미 차이

개념 의미
중첩(Nested) 구조적으로 안에 위치할 뿐, 소속 관계가 없음
내부(Inner) 나의 일부로서 구성 요소임 (소속 관계 존재)

예시 비유

  • Nested: 큰 나무 상자 안에 전혀 관련 없는 작은 상자가 있는 경우 → 단순한 위치적 포함
  • Inner: 심장은 나의 내부에 있는 나를 구성하는 요소 → 구성상의 소속

 

인스턴스 소속 여부로 구분

중첩 클래스의 구분은 바깥 클래스 인스턴스에 소속되는가를 기준으로 나눌 수 있다.

  • 정적 중첩 클래스: 바깥 클래스 인스턴스에 소속되지 않음
    → 즉, 바깥 인스턴스 없이도 사용할 수 있는 완전히 독립적인 클래스
  • 내부 클래스: 바깥 클래스 인스턴스에 소속됨
    → 바깥 인스턴스가 생성되어야만 내부 인스턴스를 생성할 수 있음

 

중첩 클래스 용어 정리

용어 설명
중첩 클래스 정적 중첩 클래스 + 내부 클래스 종류 모두 포함
정적 중첩 클래스 static이 붙은 중첩 클래스 하나만 의미
내부 클래스 내부 클래스 + 지역 클래스 + 익명 클래스

 

참고: 실무에서의 용어 사용
실무에서는 중첩 클래스와 내부 클래스를 구분 없이 혼용하는 경우가 많다.
일반적으로 클래스 안에 클래스가 있으면 중첩 클래스라 부르고, 내부 클래스도 그 안에 포함되는 개념으로 본다.
다만 엄밀히 말하면, static이 붙은 정적 중첩 클래스는 내부 클래스가 아니다.
따라서 실무에서는 문맥에 따라 중첩 클래스 또는 내부 클래스라는 용어를 유연하게 해석하면 된다.

 

중첩 클래스는 언제 사용해야 하나?

중첩 클래스는 아래 조건 중 하나라도 해당될 때만 사용하는 것이 바람직하다.

  • 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우
  • 두 클래스가 아주 긴밀하게 연결되어 있는 경우

반대로, 여러 클래스에서 해당 클래스를 사용하는 경우에는 중첩 클래스로 만들지 말고 독립된 클래스로 구성하는 것이 적절하다.

 

중첩 클래스를 사용하는 이유

  • 논리적 그룹화
    • 어떤 클래스가 특정 클래스 안에서만 사용된다면 함께 두는 것이 구조적으로 논리적이다.
    • 패키지를 열었을 때 외부에 공개할 필요가 없는 클래스가 노출되지 않아 깔끔하다.
  • 캡슐화
    • 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다.
    • 이를 통해 두 클래스를 더욱 밀접하게 연결하고, 외부에 불필요한 public 메서드를 노출하지 않아도 된다.

 

 

2. 정적 중첩 클래스

정적 중첩 클래스(Static Nested Class)는 클래스 내부에 `static` 키워드로 선언된 클래스이다.
이 클래스는 바깥 클래스의 인스턴스와 전혀 관계가 없는 독립적인 클래스이며, 바깥 클래스의 인스턴스 없이도 생성하고 사용할 수 있다.

정적 중첩 클래스는 일반적으로 바깥 클래스의 정적(`static`) 멤버에 접근할 수 있지만, 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다.
단, 정적이든 인스턴스든 바깥 클래스의 private 멤버에는 접근할 수 있다.
그 이유는 정적 중첩 클래스도 같은 클래스 내부로 간주되기 때문이다.

 

예제

public class NestedOuter {

    private static int outClassValue = 3;
    private int outInstanceValue = 2;

    static class Nested {
        private int nestedInstanceValue = 1;

        public void print() {
            // 자신의 멤버 접근
            System.out.println(nestedInstanceValue);

            // 바깥 클래스의 인스턴스 멤버 접근 - 불가능
            // System.out.println(outInstanceValue);

            // 바깥 클래스의 정적 멤버 접근 - 가능 (private도 포함)
            System.out.println(NestedOuter.outClassValue);
        }
    }
}
접근 대상 접근 가능 여부 설명
자신의 필드 가능 nestedInstanceValue 접근 가능
바깥 클래스 정적 필드 가능 NestedOuter.outClassValue 접근 가능
바깥 클래스 인스턴스 필드 불가능 outInstanceValue에 접근 시 컴파일 에러 발생

`NestedOuter.outClassValue` 처럼 클래스명을 명시하지 않고 `outClassValue` 만 작성해도 접근 가능하다. 이 경우 컴파일러가 바깥 클래스의 정적 필드를 자동으로 탐색하여 연결한다.

 

private 접근 제어자

  • `private` 접근 제어자는 같은 클래스 안에 있을 때만 접근할 수 있다.
  • 중첩 클래스도 바깥 클래스와 같은 클래스 안에 있다. 따라서 중첩 클래스는 바깥 클래스의 `private` 접근 제어자에 접근할 수 있다.

 

인스턴스 생성 및 사용 예

public class NestedOuterMain {
    public static void main(String[] args) {
        // 바깥 클래스 인스턴스 생성 (필요 없음)
        NestedOuter outer = new NestedOuter();

        // 정적 중첩 클래스 인스턴스 생성
        NestedOuter.Nested nested = new NestedOuter.Nested();

        // 메서드 실행
        nested.print();

        // 클래스 이름 확인
        System.out.println("nestedClass = " + nested.getClass());
    }
}
1
3
nestedClass = class nested.nested.NestedOuter$Nested
  • 정적 중첩 클래스는 `new 바깥클래스.중첩클래스()` 로 생성할 수 있다.
  • 중첩 클래스는 `NestedOuter.Nested` 와 같이 `바깥 클래스.중첩클래스` 로 접근할 수 있다.
  • 정적 중첩 클래스의 인스턴스와 바깥 클래스 인스턴스는 아무 관계가 없다.
    • `NestedOuter outer = new NestedOuter();` 는 생략해도 된다.
  • 중첩 클래스의 실제 클래스 이름은 `바깥클래스명$중첩클래스명` 형식으로 나타난다.

 

인스턴스가 생성된 상태

 

바깥 클래스의 멤버에 접근

 

정적 중첩 클래스의 접근 정리

  • 정적(static) 필드는 접근 가능
    • static 필드는 클래스가 메모리에 로드될 때 Method Area(메서드 영역)에 저장됨
    • 클래스 이름으로 접근 가능 (`NestedOuter.outClassValue`)
    • 클래스 전체가 정적 중첩 클래스에게는 “열려” 있는 상태 (정적 중첨 클래스는 “외부 클래스의 static 멤버”처럼 취급됨)
  • 인스턴스 필드는 접근 불가
    • 인스턴스 필드는 `new NestedOuter()` 처럼 외부(바깥) 클래스의 객체가 실제로 생성될 때 힙 영역에 올라가는 멤버
    • 정적 중첩 클래스는 외부 클래스의 인스턴스를 자동으로 참조하지 않음
    • 즉, 외부 클래스의 객체를 알 수 없기 때문에 접근할 방법이 없음

 

일반 클래스와의 비교

정적 중첩 클래스는 아래와 같이 전혀 관계 없는 일반 클래스 두 개를 정의하는 것과 사실상 동일하다.

class NestedOuter { }

class Nested { }

차이점은 오직 하나이다

  • 정적 중첩 클래스는 바깥 클래스와 같은 클래스 범위 안에 정의되므로 바깥 클래스의 private 필드에 접근할 수 있다.

 

정리

  • 정적 중첩 클래스는 `static` 키워드가 붙은 클래스이다.
  • 바깥 클래스의 인스턴스에 소속되지 않으며, 독립적으로 생성되고 동작한다.
  • 바깥 클래스의 정적 멤버에는 접근 가능하고, 인스턴스 멤버에는 접근 불가하다.
  • 바깥 클래스의 private 멤버에도 접근 가능하다. (같은 클래스 내 정의이기 때문)
  • 바깥 클래스 내부에서만 사용되는 특정 기능 클래스를 논리적으로 그룹화하고 캡슐화하는 데 유용하다.
  • 인스턴스 생성 시 `new 바깥클래스.중첩클래스()` 형식으로 접근한다.

 

 

3. 정적 중첩 클래스의 활용

리팩토링 전 구조

다음은 메시지를 출력하는 `NetworkMessage` 클래스를 외부에 정의하고, `Network` 클래스가 이를 사용하는 구조이다.

 

NetworkMessage

// Network 객체 안에서만 사용
public class NetworkMessage {
    private String content;

    public NetworkMessage(String content) {
        this.content = content;
    }

    public void print() {
        System.out.println(content);
    }
}

 

Network

public class Network {
    public void sendMessage(String text) {
        NetworkMessage networkMessage = new NetworkMessage(text);
        networkMessage.print();
    }
}
 

NetworkMain

public class NetworkMain {
    public static void main(String[] args) {
        Network network = new Network();
        network.sendMessage("Hello Java");
    }
}

 

실행 결과

Hello Java

 

문제점

  • `NetworkMessage` 는 오직 `Network` 클래스에서만 사용된다.
  • 그러나 외부 패키지에서 `NetworkMessage` 가 보이기 때문에 다른 개발자에게 혼란을 줄 수 있다.
    • 예: NetworkMessage도 외부에서 직접 사용해야 하나?
  • NetworkMessage가 외부에 노출될 필요가 없는 클래스임에도 패키지 수준에 공개되어 있다.

 

리팩토링 후: 정적 중첩 클래스로 감싸기

`NetworkMessage` 를 `Network` 클래스 내부의 정적 중첩 클래스로 리팩토링하면 다음과 같다. (`NetworkMain` 은 동일)

public class Network {
    public void sendMessage(String text) {
        NetworkMessage networkMessage = new NetworkMessage(text);
        networkMessage.print();
    }

    private static class NetworkMessage {
        private String content;

        public NetworkMessage(String content) {
            this.content = content;
        }

        public void print() {
            System.out.println(content);
        }
    }
}

 

실행 결과

Hello Java

 

리팩토링 결과의 장점

  • 논리적 그룹화
    • `NetworkMessage` 는 `Network` 안에서만 사용되므로, 내부에 정의하는 것이 더 구조적으로 적절하다.
    • 개발자가 `Network` 클래스를 열었을 때 관련 로직이 한 곳에 모여 있어 이해하기 쉽다.
  • 캡슐화
    • `private static class` 로 선언했기 때문에 외부에서는 절대 접근할 수 없다.
    • 외부에 노출하지 않아도 되는 정보를 숨겨 유지보수성과 안전성이 향상된다.
    • 예: `new Network.NetworkMessage()` 같은 외부 접근 불가.
  • 명확한 역할 구분
    • `Network` 만 외부에 사용되며, `NetworkMessage` 는 내부 구현 세부사항으로 분리된다.

 

중첩 클래스의 접근

  • 다른 클래스에서 중첩 클래스에 접근하려면 (외부에서 접근할 때)
    `바깥클래스명.중첩클래스명` 형식으로 작성해야 한다.
NestedOuter.Nested nested = new NestedOuter.Nested();
  • 바깥 클래스 내부에서 자신의 중첩 클래스에 접근할 때는 (내부에서 접근할 때)
    바깥 클래스 이름 없이 바로 사용할 수 있다.
public class Network {
    public void sendMessage(String text) {
        NetworkMessage message = new NetworkMessage(text);
    }

    private static class NetworkMessage { ... }
}
  • 중첩 클래스는 자신이 정의된 바깥 클래스 내부에서만 사용하는 것이 바람직하다.
    외부에서 생성하거나 사용해야 한다면, 중첩 클래스로 두지 말고 클래스를 분리하는 것이 좋다.

 

 

4. 내부 클래스

내부 클래스(Inner Class)는 바깥 클래스의 인스턴스에 소속된 클래스이며, static 키워드 없이 바깥 클래스 내부에 선언된다.
바깥 클래스의 인스턴스 멤버(필드, 메서드 포함)에 접근할 수 있으며, 캡슐화와 논리적 구조화를 강화할 수 있는 특징이 있다.

 

내부 클래스 vs 정적 중첩 클래스 비교

구분 내부 클래스 정적 중첩 클래스
선언 키워드 static 없음 static 있음
바깥 클래스 인스턴스 참조 O (자동 참조) X (참조 불가)
바깥 클래스 멤버 접근 인스턴스 & static 모두 접근 가능 static만 접근 가능
생성 방식 바깥 인스턴스를 통해 생성 직접 생성 가능

 

예제

public class InnerOuter {

    private static int outClassValue = 3;
    private int outInstanceValue = 2;

    class Inner {
        private int innerInstanceValue = 1;

        public void print() {
            // 내부 클래스의 자기 필드
            System.out.println(innerInstanceValue);
            // 바깥 클래스의 인스턴스 필드, private도 가능
            System.out.println(outInstanceValue);
            // 바깥 클래스의 정적 필드, private도 가능
            System.out.println(InnerOuter.outClassValue);
        }
    }
}


접근 제어자와 멤버 접근

  • 내부 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다.
  • 이는 내부 클래스도 같은 클래스 내부로 간주되기 때문이다.

 

인스턴스 생성 및 실행 예시

public class InnerOuterMain {
    public static void main(String[] args) {
        InnerOuter outer = new InnerOuter();
        InnerOuter.Inner inner = outer.new Inner(); // 바깥 인스턴스를 통해 내부 클래스 생성
        inner.print();

        System.out.println("innerClass = " + inner.getClass());
    }
}
1
2
3
innerClass = class nested.inner.InnerOuter$Inner
  • 내부 클래스는 바깥 클래스의 인스턴스에 소속되므로, 생성 시 반드시 바깥 인스턴스를 필요로 한다.
  • 내부 클래스는 `바깥클래스의 인스턴스 참조.new 내부클래스()`로 생성할 수 있다.
    • 위 예시의 `outer.new Inner()`에서 `outer`는 바깥 클래스의 인스턴스 참조를 가진다.

 

개념 & 실제 - 내부 클래스의 생성

InnerOuter outer = new InnerOuter();
InnerOuter.Inner inner = outer.new Inner();
  • 개념적으로 내부 클래스 인스턴스는 바깥 클래스 인스턴스 내부에 생성되는 것처럼 작동한다.
  • 실제로는 내부 클래스 인스턴스가 바깥 클래스 인스턴스를 참조 필드로 가지고 동작한다.

실제 - 내부 클래스 생성

 

정리

  • 내부 클래스는 `static` 키워드 없이 선언되며, 바깥 클래스 인스턴스에 종속된다.
  • 바깥 클래스의 인스턴스 및 정적 멤버 모두에 접근할 수 있다.
  • 바깥 클래스의 `private` 멤버도 접근 가능하다.
  • 내부 클래스의 인스턴스를 생성하려면 반드시 바깥 클래스 인스턴스를 먼저 생성해야 한다.
  • 내부 클래스는 바깥 클래스의 구성 요소로서 동작하며, 정적 중첩 클래스와는 소속과 접근 범위 면에서 차이를 가진다.

 

 

5. 내부 클래스의 활용

내부 클래스로 리팩토링 전 구조

Engine 클래스는 Car 클래스 내부에서만 사용되지만, 외부 클래스처럼 정의되어 있다.

 

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 인스턴스의 참조를 생성자에서 보관한다.

 

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); // Car 인스턴스를 전달
    }

    public String getModel() { // Engine에서만 사용
        return model;
    }

    public int getChargeLevel() { // Engine에서만 사용
        return chargeLevel;
    }

    public void start() {
        engine.start();
        System.out.println(model + " 시작 완료");
    }
}
  • Car 클래스는 엔진에서만 사용하는 기능을 위해 메서드를 추가해서, 모델 이름과 충전 레벨을 외부 에 노출해야 한다.

 

CarMain

public class CarMain {
    public static void main(String[] args) {
        Car myCar = new Car("Model Y", 100);
        myCar.start();
    }
} 
충전 레벨 확인: 100  
Model Y의 엔진을 구동합니다.  
Model Y 시작 완료

 

문제점

  • Engine 클래스는 외부에 존재하지만, Car에서만 사용된다.
  • `getModel()`, `getChargeLevel()`은 외부에 노출되어 있지만, 실제로는 Engine에서만 사용된다.
  • 결과적으로 Car 클래스의 내부 전용 정보가 외부에 공개되고 있어 캡슐화가 약화된다.

 

리팩토링 후: 내부 클래스로 변환

Engine 클래스를 Car 클래스 내부의 private 내부 클래스로 리팩토링하면 다음과 같다.

 

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 + "의 엔진을 구동합니다."); // 직접 접근
        }
    }
}

 

CarMain (동일)

충전 레벨 확인: 100  
Model Y의 엔진을 구동합니다.  
Model Y 시작 완료

 

리팩토링 결과의 장점

  • 캡슐화 강화
    • `getModel()`, `getChargeLevel()`과 같은 불필요한 getter 메서드 제거 가능
    • 내부 클래스는 바깥 클래스의 private 멤버에 직접 접근할 수 있으므로 내부 정보를 외부에 노출하지 않아도 됨
  • 구조적 명확성
    • Engine은 오직 Car에서만 사용되므로 내부 클래스로 정의하는 것이 논리적으로 타당하다.
    • 클래스 간 의존성을 줄이고, 역할이 명확하게 구분된다.
  • 외부에 불필요한 클래스 노출 방지
    • Engine 클래스가 외부에 보이지 않기 때문에 혼란이나 오용 가능성이 줄어든다.

 

 

6. 같은 이름의 바깥 변수 접근

내부 클래스에서 바깥 클래스와 동일한 이름의 변수를 선언하면, 이름이 중첩되면서 가까운 범위의 변수가 우선된다.
이러한 현상을 섀도잉(Shadowing)이라 한다.

 

public class ShadowingMain {
    public int value = 1;

    class Inner {
        public int value = 2;

        void go() {
            int value = 3;

            System.out.println("value = " + value); // 지역 변수
            System.out.println("this.value = " + this.value); // 내부 클래스 인스턴스 변수
            System.out.println("ShadowingMain.value = " + ShadowingMain.this.value); // 바깥 클래스 인스턴스 변수
        }
    }

    public static void main(String[] args) {
        ShadowingMain main = new ShadowingMain();
        Inner inner = main.new Inner();
        inner.go();
    }
}
value = 3  
this.value = 2  
ShadowingMain.value = 1

 

같은 이름의 변수가 여러 범위에 선언되었을 경우, 가까운 범위와 더 구체적인 변수부터 우선적으로 참조된다.

  • `value`: `go()` 메서드의 지역 변수 (가장 가까움)
  • `this.value`: Inner 클래스의 인스턴스 변수
  • `ShadowingMain.this.value`: 바깥 클래스 ShadowingMain의 인스턴스 변수

이렇게 다른 변수들을 가려서 보이지 않게 하는 것을 섀도잉(Shadowing)이라 한다.

이처럼 이름이 같은 변수가 여러 스코프에 존재할 때, 명확한 변수 참조를 위해 `this`, `OuterClassName.this` 와 같은 형식을 사용한다.

 

주의 사항

  • 변수 이름이 중복되면 가독성이 떨어지고 혼란을 일으킬 수 있다.
  • 되도록이면 스코프마다 변수 이름을 구분하여 명확하게 작성하는 것이 바람직하다.
  • 내부 클래스에서 바깥 클래스의 멤버에 접근할 일이 있다면, 애초에 변수 이름을 다르게 정의하거나 `바깥클래스명.this.변수명` 형식을 사용할 수 있다.
반응형

댓글