JAVA의 가장 기본이 되는 내용

중첩 클래스(정적 중첩 클래스, 내부 클래스)

cwchoiit 2024. 4. 14. 14:21
728x90
반응형
SMALL

참고자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을

www.inflearn.com

 

클래스 안에 클래스를 중첩해서 정의할 수 있는데, 이것을 중첩 클래스(Nested Class)라고 한다.

중첩 클래스 예시

class Outer {
	...
    class Nested {
    	...
    }
}

 

근데 중첩 클래스는 클래스를 정의하는 위치에 따라 다음과 같이 분류한다.

 

이 네가지가 모두 중첩 클래스라고 불리는데, 여기서 2가지로 분류가 가능하다.

  • 정적 중첩 클래스 (static)
  • 내부 클래스 종류
    • 내부 클래스
    • 지역 클래스
    • 익명 클래스

 

중첩 클래스는 변수를 선언하는 위치와 같다.

 

변수의 선언 위치

  • 클래스 변수 (static)
  • 인스턴스 변수
  • 지역 변수

중첩 클래스 선언 위치

  • 정적 중첩 클래스 -> 정적 변수(클래스 변수)와 같은 위치
  • 내부 클래스 -> 인스턴스 변수와 같은 위치
  • 지역 클래스, 익명 클래스 -> 지역 변수와 같은 위치
class Outer {
	...
    // 정적 중첩 클래스
    static class StaticNested {
    	...
    }
    
    // 내부 클래스
    class Inner {
    	...
    }
    
    public void method() {
    	...
        // 지역 클래스
        class Local() {...}
        
        Local local = new Local();
        ...
    }
}

 

익명 내부 클래스는 저렇게 클래스를 선언하고 사용하는 게 아니라, 추상 클래스나 인터페이스 같은것을 상속받은 클래스가 필요한데 이걸 굳이 클래스 파일로 만들어서 클래스라는 키워드를 사용해서 만드는게 아니라 한 번 사용하고 말거 같을 때 코드 내에서 선언할 수가 있다.

 

익명 내부 클래스 예시

context.execute(new Strategy() {
    @Override
    public void call() {
        log.info("비즈니스 로직 1 실행");
    }
});

 

그럼 정적 중첩 클래스와 내부 클래스를 분리하는 이유는 뭘까?

 

정적 중첩 클래스와 내부 클래스의 차이

정적 중첩 클래스

  • static이 붙는다.
  • 바깥 클래스의 인스턴스에 소속되지 않는다.

내부 클래스

  • static이 붙지 않는다.
  • 바깥 클래스의 인스턴스에 소속된다.
그러니까 엄밀히 말하면 정적 중첩 클래스내부 클래스는 완전히 다른 거고 내부 클래스정적 중첩 클래스라고 말하거나 정적 중첩 클래스내부 클래스라고 말하면 안된다. 근데 말하면서 그냥 섞어 쓰니까 상황과 문맥에 따라 잘 이해해서 받아들이면 된다.

 

 

중첩 클래스는 언제 사용할까?

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

 

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

  • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화된다. 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는 장점도 있다.
  • 캡슐화: 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불 필요한 public 메서드를 제거할 수 있다.

 

정적 중첩 클래스

정적 중첩 클래스의 사용 예시를 보자.

public class StaticNestedClass {

    private static int outerStaticValue = 1;
    private int outerInstanceValue = 2;

    static class InStaticNestedClass {
        private int inStaticNestedInstanceValue = 10;
        public void print() {
            // 본인의 인스턴스 변수에 당연히 접근 가능
            System.out.println("inStaticNestedInstanceValue = " + inStaticNestedInstanceValue);

            // 외부 클래스의 클래스 변수에 접근이 가능 (근데, private 이어도 가능한게 유일한 차이)
            System.out.println("outerStaticValue = " + outerStaticValue);

            // 외부 클래스의 인스턴스 변수에는 접근이 불가능, 왜냐하면 static을 생각해보면 됨 static이니까 같은 static만 접근이 가능하다고 보면 됨
            // 인스턴스 영역(힙 영역)에 저 변수에 접근할 수 있는 방법이 없음
            // System.out.println("outerStaticValue = " + outerInstanceValue);
        }
    }
}

 

정적 중첩 클래스는 외부의 클래스와 그냥 다른 클래스라고 보면 된다.

그러니까 다음 코드랑 동일하다고 보면 된다. 

class StaticNestedClass {

}

class InStaticNestedClass {

}

 

근데 한가지 다른 하나, 정적 중첩 클래스는 외부 클래스의 private 클래스 변수에 접근이 가능하다.

private에 접근이 가능한 이유는 내부에 있기 때문. private은 자기 자신에서는 접근이 가능한데 정적 중첩 클래스는 결국 {} 안에 존재하기 때문에. 

 

호출은 이렇게 하면 된다.

Main

public class StaticNestedMain {
    public static void main(String[] args) {
        StaticNestedClass.InStaticNestedClass inStaticNestedClass = new StaticNestedClass.InStaticNestedClass();

        inStaticNestedClass.print();
    }
}

 

 

그럼 정적 중첩 클래스는 어느 순간에 써야하는지 예제를 통해 알아보자.

 

정적 중첩 클래스의 사용 예시

 

NetworkMessage

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

 

이렇게 두 개의 클래스가 있다. 이때 NetworkMessage 클래스는 외부 어디에서도 사용하지 않고 Network 클래스에서만 사용하고 있다고 가정해보자.

 

이걸 만든 사람과 이걸 가져다가 사용하는 사람이 서로 다른 사람이라고 가정할 때 가져다가 사용하는 사람은 이렇게 두 개의 클래스를 보면 이런 생각이 든다. 어? 뭘 사용해야하지? 가 첫번째. 두번째는 어떻게 잘 파악해서 "아 Network 클래스에서 NetworkMessage를 가져다가 사용하니까 Network 클래스를 사용하면 되겠구나." 라고 생각을 어찌저찌 하긴 할 것이다.

 

그런데 이렇게 짠 코드가 아니라 다음 코드를 보자.

Refactoring된 Network

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

 

이렇게 정적 중첩 클래스로 안에서 만들고 심지어 이 클래스의 접근제어자가 private이면 외부에서는 저 클래스에 아예 접근 자체를 하지 못하게 하겠다는 강한 의지이기 때문에 이걸 가져다가 사용하는 개발자는 아무런 고민할 필요가 없다. 애시당초에 클래스 파일자체가 이것 하나만 있으니 다른 클래스를 더 볼 필요도 없고 이렇게 Network에서만 사용되는 클래스라면 내부에 정적 중첩 클래스로 선언해서 사용하면 파일 개수도 줄고 가시성도 좋아진다.

 

정적 중첩 클래스는 이런 장점과 사용성이 있다. 내부 클래스 말고 정적 중첩 클래스(static)로 선언한 이유는 외부 클래스의 인스턴스 변수에 접근할 필요가 없으면 이게 더 확실한 의도를 보여줄 수 있기 때문.

 

내부 클래스

정적 중첩 클래스는 외부 클래스와 아무런 관련이 없다.

서로 다른 클래스다. 근데 한가지! 외부 클래스의 private 클래스 변수에도 접근이 가능하다는 점.

원래 클래스 변수면 어디서나 접근이 가능한데, private이면 그 클래스 내부에서만 접근이 가능한데 정적 중첩 클래스도 그 클래스 내부 {}에 존재하기 때문에 가능한 원리.

 

근데 이제 내부 클래스는 외부 클래스와 밀접한 관련이 있다. 그냥 외부 클래스의 인스턴스를 이루는 요소가 된다.

 

정적 중첩 클래스

  • 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 = " + innerInstanceValue);

            //외부 클래스의 인스턴스 변수
            System.out.println("outInstanceValue = " + outInstanceValue);

            //외부 클래스의 클래스 변수
            System.out.println("outClassValue = " + outClassValue);
        }
    }
}

내부 클래스는 앞에 static이 붙지 않고 인스턴스 멤버가 된다.

그래서 내부 클래스는 자신의 멤버에도 접근이 당연히 가능하고, 바깥 클래스의 인스턴스 멤버에도 접근이 가능하고, 바깥 클래스의 클래스 멤버에는 당연히 접근이 가능하다. 근데 여기서 private도 접근이 가능하다는 것만 좀 주의하면 된다.

 

그래서 이 내부 클래스는 어떻게 호출하고 사용할까?

public class InnerOuterMain {
    public static void main(String[] args) {
        InnerOuter outer = new InnerOuter();
        InnerOuter.Inner inner = outer.new Inner();

        inner.print();
    }
}

내부 클래스는 다시 한번 말하지만 외부 클래스의 인스턴스에 속한다. 그래서 외부 클래스 없이는 내부 클래스 자체적으로 인스턴스를 생성할 수 없다. 그래서 아래처럼 외부 클래스의 인스턴스를 생성하고 그 외부 클래스 인스턴스의 내부 클래스를 만들어야 한다.

InnerOuter outer = new InnerOuter();
InnerOuter.Inner inner = outer.new Inner();

 

그래서 외부 클래스와 내부 클래스를 개념적으로 살펴보면 다음 그림처럼 생각하면 된다.

외부 클래스의 인스턴스가 내부 클래스의 인스턴스를 포함하고 있고 이런 구조이기 때문에 내부 클래스의 인스턴스는 바깥 인스턴스를 알기 때문에 바깥 인스턴스의 멤버에 접근이 가능하다.

 

이렇게만 알아둬도 충분한데 실제로 구조는 조금 다르다. 근데 저렇게 알아두면 된다.

실제 구조는 다음과 같다.

내부 인스턴스가 외부 인스턴스의 참조값을 가지고 있기 때문에 바깥 인스턴스의 멤버에 접근이 가능한 게 실제 구조다.

 

정리를 하자면

정적 중첩 클래스는 외부 클래스와 아무런 관련이 없다. 서로 다른 클래스이지만 외부 클래스가 관리하는 영역 {} 안에 있기 때문에 private으로 선언해도 접근이 가능할 뿐이다.

 

내부 클래스는 외부 클래스의 인스턴스에 속한다. 그렇기 때문에 내부 클래스는 생성할 때 외부 클래스의 인스턴스에 속한채로 생성되어야 한다. 그리고 내부 인스턴스는 외부 인스턴스를 알고 있기 때문에 외부 인스턴스 멤버에 접근이 가능하다.

 

 

내부 클래스의 용도

내부 클래스를 사용하지 않았을 때 코드를 먼저 보고 내부 클래스를 사용하면 어떻게 더 좋아지는지 보자.

 

내부 클래스를 사용하지 않았을 때

Car

package nested.inner.ex1;

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 int getChargeLevel() {
        return chargeLevel;
    }

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

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

Engine

package nested.inner.ex1;

// Car Class 에서만 사용
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() + "의 엔진을 구동합니다.");
    }
}

Main

package nested.inner.ex1;

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

 

지금 보면 어떤 문제가 있냐면 두가지 문제가 있는데,

1. Car 클래스에서만 사용하는 Engine 클래스를 두개의 클래스 파일로 나뉘어져 사용 중

2. Engine 클래스에서만 사용하는 Car 클래스에서 만든 메서드가 있음

 

이걸 어떻게 좋은 코드로 변경해볼까?

내부 클래스를 사용했을 때

Car

package nested.inner.ex2;

public class Car {
    private final String model;
    private final int chargeLevel;
    private final Engine engine;

    private class Engine {
        public void start() {
            System.out.println("충전 레벨 확인 = " + chargeLevel);
            System.out.println(model + " 의 엔진을 구동합니다.");
        }
    }

    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 + " 시작 완료");
    }
}

 

Car 클래스에서만 사용되는 Engine 클래스라면 그냥 Car 안에 private 내부 클래스로 선언하면 된다.

그리고 내부 클래스로 선언했기 때문에 Engine 클래스는 더이상 Car에 대한 인스턴스를 따로 받을 필요가 없다.

그리고 Engine 클래스는 내부 클래스이므로 외부 클래스의 인스턴스 멤버에 접근이 가능하기 때문에 다이렉트로 필드에 접근하면 된다. 그 말은 Car 클래스에서 불필요한 메서드를(불필요 하다기보단 오로지 Engine만을 위해 만든 메서드) 없앨 수 있다. 그리고 이 말은 캡슐화를 더 강력하게 해준다. 

 

Main

package nested.inner.ex2;

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

 

슈퍼 간단해졌다. 이게 내부 클래스를 사용하는 이유이다.

 

이제 다시 처음으로 돌아가서 왜 중첩 클래스를 사용하냐?!

중첩 클래스(정적 중첩 클래스, 내부 클래스)는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용해야 한다. 외부 여러곳에서 특정 클래스를 사용한다면 중첩 클래스로 사용하면 안된다.

 

사용하는 이유는 크게 두가지이다.

  • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스에서만 사용된다면 해당 클래스 안에 포함하는 게 더 논리적으로 그룹화가 된다. 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는다는 장점도 있다.
  • 캡슐화: 중첩 클래스는 바깥 클래스의 private 멤버에 접근이 가능하다. 그 말은 중첩클래스가 아니고 이 클래스만을 위해서 만든 메서드는 필요가 없다는 뜻이다. (위에 내부 클래스를 사용하지 않았을 때 Car 예시처럼 오로지 Engine 클래스만을 위해 public으로 메서드를 만들었다면 캡슐화가 약해지는 것이다.)

 

같은 이름의 바깥 변수 접근

아래 같은 경우를 말하는 것.

package nested.inner.ex2;

public class Shadowing {
    private int value = 3;

    private class InnerShadowing {
        private int value = 5;

        public void shadowing() {
            int value = 10;
            System.out.println("value = " + value); // 이 메서드의 지역 변수 value
            System.out.println("this.value = " + this.value); // 내부 클래스의 인스턴스 변수 value
            System.out.println("Shadowing.this = " + Shadowing.this.value); // 바깥 클래스의 value
        }
    }

    public static void main(String[] args) {
        Shadowing shadowing = new Shadowing();
        InnerShadowing innerShadowing = shadowing.new InnerShadowing();

        innerShadowing.shadowing();
    }
}

외부 클래스와 내부 클래스에서 같은 이름으로 변수를 만들었을 때 접근하는 방법이다.

근데, 이렇게 변수 이름을 모호하게 작성 안하는게 가장 좋은 방법이다.

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

익명 클래스  (0) 2024.04.21
지역 클래스  (0) 2024.04.21
Stream API  (2) 2024.04.07
날짜와 시간  (0) 2024.04.04
Enum  (0) 2024.04.03