중첩 클래스 ,내부 클래스1

2025. 4. 7. 17:40Java

◎ 중첩 클래스

-> 클래스 안에 클래스를 중첩해서 정의할 수 있는데 이를 중첩 클래스라 한다.

class Outer{
	// 중첩 클래스
    class Nested{
    	...
    }
}

-> 중첩 클래스는 클래스를 정의하는 위치에 따라 아래와 같이 분류된다.

-> 정적 중첩 클래스 : 정적 변수와 같은 위치의 클래스

-> 내부 클래스 : 인스턴스 변수와 같은 위치의 클래스

-> 지역 클래스 : 지역 변수와 같은 위치의 클래스

 

◎ 중첩 클래스의 용도

-> 특정 클래스가 다른 하나의 클래스 내부에서만 사용되거나 둘이 긴밀하게 연결되어 있는 경우에만 사용한다. 외부의 여러 클래스가 특정 중첩 클래스를 사용한다면 중첩 클래스로 만들면 안된다.

-> 논리적 그룹화 : 특정 클래스가 다른 하나의 클래스에서만 사용되는 경우 해당 클래스 내에 포함하는 것이 논리적으로 더 그룹화가 된다. 

-> 캡슐화 : 중첩 클래스는 외부 클래스의 private 멤버에 접근 가능하다. 이 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있다. 

 

◎ 정적 중첩 클래스 VS 내부 클래스

-> 중첩(Nested) : 어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계

-> 내부(Inner) : 나의 내부에 있는 나를 구성하는 요소

-> 정적 중첩 클래스는 외부 클래스의 안에 있지만 외부 클래스와는 관계가 없는 전혀 다른 클래스다. static이 붙는다.

-> 내부 클래스는 외부 클래스의 있으면서 외부 클래스를 구성하는 요소 클래스이다.

 

◎ 내부 클래스의 종류

-> 내부 클래스(inner class) : 외부 클래스의 인스턴스의 멤버에 접근

-> 지역 클래스(local class) : 내부 클래스의 특징 + 지역 변수에 접근

-> 익명 클래스(anonymous class) : 지역 클래스의 특징 + 클래스의 이름이 없는 특별한 클래스

 

◎ 중첩 클래스

 

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);
        }
    }
}

-> 정적 중첩 클래스는 클래스 앞에 static이 붙는다.

-> 정적 중첩 클래스는 자신의 멤버와 외부 클래스의 클래스 멤버(static)에 접근할 수 있으며 외부 클래스의 인스턴스 멤버에는 접근할 수 없다.

-> 중첩 클래스는 외부 클래스와 같은 클래스 안에 있기 때문에 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("nested.getClass() = " + nested.getClass());
    }
}

-> 결과

-> 정적 중첩 클래스는 new 외부클래스.중첩클래스()로 생성한다.

-> new NestedOuter()로 만든 외부 클래스의 인스턴스와 new NestedOuter.Nested()로 만든 정적 중첩 클래스의 인스턴스는 서로 아무 관계가 없는 인스턴스이다. 단지 클래스 구조 상 중첩된 것이다.

-> 중첩 클래스.getClass()를 출력하면 class nested.nested.NestedOuter$Nested가 출력되는데 외부클래스$중첩 클래스의 조합으로 출력이 된 것이다.

-> 다음은 위 코드의 구조를 그림으로 나타낸 것이다.

-> 정적 중첩 클래스는 외부 클래스의 정적 필드에는 접근할 수 있다.(정적 필드에 접근할 때 인스턴스 참조가 필요 없고, static은 메서드 영역에서 공통으로 관리되기 때문) 하지만 외부 클래스가 만든 인스턴스 필드에는 외부 인스턴스의 참조가 없기 때문에 접근할 수 없다.

 

◎ 정적 중첩 클래스의 활용

-> 정적 중첩 클래스를 활용하는 방법에 대해 알아본다. 먼저 아래 코드는 정적 중첩 클래스를 사용하지 않았을 때의 코드이다.

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

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

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

-> Network는 text를 입력받아 NetworkMessage를 생성해서 메시지를 출력한다.

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

-> 결과

-> NetworkMessage는 외부에서 사용되지 않고  Network에서만 사용된다. 외부의 호출 없이 단순히 Network에서만 사용되는데 굳이 클래스 파일을 두 개로 분리해서 만들면 코드를 이해하는데 시간도 걸리고 NetworkMessage가 Network에서만 사용하려는 의미도 파악하기 쉽지 않다.

-> 다음은 위 코드를 정적 중첩 클래스로 리팩토링한 코드이다.

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

    // NetworkMessage를 정적 중첩 클래스로 선언
    private static class NetworkMessage {
        private String content;

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

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

-> Network 내에서만 사용되는 NetworkMessage 클래스를 private으로 정적 중첩 클래스로 만들었다. 이렇게 하면 NetworkMessage는 외부에서 접근할 수 없고 Network에서만 사용할 수 있게되고, 하나의 클래스 파일에 NetworkMessage의 의도를 더욱 쉽게 파악할 수 있다.

-> 중첩 클래스(내부 클래스 포함)의 용도는 자신이 소속된 외부 클래스 안에서 사용되는 것이다. 만약 자신이 소속된 클래스 뿐만 아니라 외부에서도 생성하고 사용한다면 중첩 클래스의 용도에 맞지 않을 수 있다. 이런 경우에는 클래스를 따로 빼서 만드는 것이 더 낫다.

 

◎ 내부 클래스

-> 외부 클래스의 인스턴스를 이루는 요소가 되는 클래스이다. 외부 클래스의 인스턴스에 소속된다.

-> 다음은 내부 클래스가 사용된 코드이다.

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(outClassValue);
        }
    }
}
public class InnerOuterMain {
    public static void main(String[] args) {
        InnerOuter outer = new InnerOuter();
        InnerOuter.Inner inner = outer.new Inner(); // 내부 클래스의 인스턴스 생성
        inner.print();

        System.out.println("inner.getClass() = " + inner.getClass());
    }
}

-> 결과

-> 내부 클래스는 외부 클래스의 인스턴스에 소속된다. 따라서 외부 클래스의 인스턴스 정보가 있어야 내부 클래스를 생성할 수 있다.

-> 내부 클래스는 외부클래스 인스턴스 참조.new 내부 클래스()로 생성한다.

-> 다음은 위 코드의 구조를 나타낸 그림이다.

-> 내부 인스턴스는 외부 인스턴스의 참조를 보관한다. 해당 참조를 가지고 외부 인스턴스의 멤버에 접근할 수 있다.

 

◎ 내부 클래스 활용

-> 내부 클래스를 활용하는 방법에 대해 알아본다. 먼저 다음은 내부 클래스를 사용하지 않는 코드이다.

// Car에서만 사용
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 클래스에서만 사용되는 클래스다. 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(this);
    }

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

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

    public void start() {
        engine.start();
        System.out.println(model + " 시작 완료");
    }
}

-> Car 클래스는 Engine에 필요한 메서드를 제공한다. 또한 해당 메서드들(getModel(), getChargeLevel())은 Engine 클래스 외에는 사용하지 않는다. 이는 특정 클래스에서만 사용될 것을 불필요하게 외부에 노출해야 한다는 단점이 있다.

public class CarMain {
    public static void main(String[] args) {
        Car car = new Car("Damas", 10);
        car.start();
    }
}

-> 결과

-> 다음은 위 코드를 내부 클래스를 사용하도록 리팩토링한 코드이다.

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 + " 엔진 구동");
        }
    }
}

-> Engine 클래스를 Car의 내부 클래스로 만들었다.

-> Engine 클래스는 Car의 내부 클래스이므로 car의 인스턴스 변수인 chargeLevel, model에 직접 접근할 수 있다. 이제 Engine 클래스에서 Car 인스턴스의 참조를 받아와서 car 인스턴스의 메서드를 호출하지 않아도 된다.

-> 외부 클래스에서 내부 클래스의 인스턴스를 생성할 때는 외부 클래스의 이름을 생략할 수 있다. : new Engine();

-> 외부 클래스에서 내부 클래스의 인스턴스를 생성할 때 내부 클래스의 인스턴스는 자신을 생성한 바깥 클래스의 인스턴스를 자동으로 참조한다.

-> 리팩토링 이후 getModel(), getChargeLevel()과 같은 메서드를 모두 제거해서 필요한 메서드만 외부에 노출하여 Car의 캡슐화를 더 높였다.

 

 

 

'Java' 카테고리의 다른 글

열거형 - ENUM  (0) 2025.04.01
래퍼, Class 클래스  (0) 2025.03.27
String 클래스  (0) 2024.11.18
불변객체  (0) 2024.10.31
Object 클래스  (0) 2024.10.29