중첩 클래스, 내부 클래스(1)
#Java
정리
중첩 클래스, 내부 클래스란?
클래스 안에 클래스를 중첩해서 정의할 수 있는데, 이것을 중첩 클래스(Nested Class)라 한다.
class Outer {
...
//중첩 클래스
class Nested {
...
}
}
중첩 클래스의 분류
중첩 클래스는 총 4가지가 있고, 크게 2가지로 분류할 수 있다.
- 정적 중첩 클래스
- 정적 변수오 같은 위치
- 내부 클래스 종류
- 내부 클래스
- 인스턴스 변수와 같은 위치
- 바깥 클래스의 인스턴스의 멤버에 접근
- 지역 클래스
- 지역 변수와 같은 위치
- 내부 클래스의 특징 + 지역 변수에 접근
- 익명 클래스
- 지역 클래스의 특징 + 클래스의 이름이 없는 특별한 클래스
- 내부 클래스
중첩 클래스를 정의하는 위치는 변수의 선언 위치와 같다.
여기서 정적 중첩 클래스와 내부 클래스로 분류하는 것을 확인할 수 있다.
그럼 중첩이라는 단어와 내부라는 단어는 무슨 차이가 있는 것일까?
- 중첩(Nested): 어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계
- 중첩(Nested)은 나의 안에 있지만 내것이 아닌 것을 말한다
- ex) 큰 나무 상자에 작은 나무 상자를 넣을 때 두 상자의 관계
- 정적 중첩 클래스는 바깥 클래스의 안에 있지만 바깥 클래스와 전혀 관계가 없다.
- static이 붙는다.
- 내부(Inner): 나의 내부에 있는 나를 구성하는 요소
- 내부(Inner)는 나의 내부에서 나를 구성하는 요소
- ex) ‘나’ 와 심장
- 내부 클래스는 바깥 클래스의 내부에 있으면서 바깥 클래스를 구성하는 요소
- static이 붙지 않는다.
실무에서는 중첩, 내부라는 단어를 명확히 구분하지 않고, 중첩 클래스 또는 내부 클래스라고 이야기한다. 알잘딱깔센으로 이해하자.
중첩 클래스는 언제 사용해야 하나?
- 내부 클래스를 포함한 모든 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용해야 한다.
- 외부의 여러 클래스가 특정 중첩 클래스를 사용한다면 중첩 클래스로 만들면 안된다.
- ex) 클래스 A 안에 중첩 클래스 B가 있다고 해보자. 클래스 B가 클래스 A 외부의 클래스들과 연관이 있으면 밖으로 빼는게 좋다.
중첩 클래스를 사용하는 이유
- 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화 된다.
패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는 장점도 있다. - 캡슐화: 중첩 클래스는 바깥 클래스의
private
멤버에 접근할 수 있다.
이렇게 해서 둘을 긴밀하게 연결하고 불필요한public
메서드를 제거할 수 있다.
이 부분은 말로 이해하기는 어렵기 때문에 이후에 예제를 통해서 알아보자.
정적 중첩 클래스
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(outclassValue);
}
}
}
- 정적 중첩 클래스는 앞에
static
이 붙는다. - 정적 중첩 클래스는
- 자신의 멤버에는 당연히 접근할 수 있다.
- 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다.
- 바깥 클래스의 클래스 멤버에는 접근할 수 있다.
참고로 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());
}
}
- 정적 중첩 클래스는
new 바깥클래스.중첩클래스()
로 생성할 수 있다. - 중첩 클래스는
NestedOuter.Nested
와 같이 바깥 클래스.중첩클래스로 접근할 수 있다. - 중첩 클래스와 Outer 클래스는 아무런 관계가 없으므로 중첩 클래스를 생성할 때 Outer 클래스를 생성할 필요가 없다.
중첩 클래스를 출력해보면 중첩 클래스의 이름은 NestedOuter$Nested
와 같이 바깥 클래스, $
, 중첩 클래스의 조합으로 만들어진다.
정적 중첩 클래스는 바깥 클래스의 정적 필드에는 접근할 수 있다. 하지만 바깥 클래스가 만든 인스턴스 필드에는 바로 접근할 수 없다. 바깥 인스턴스의 참조가 없기 때문이다.
정리
쉽게 이야기해서 다음과 같이 정적 중첩 클래스를 만들지 않고, 그냥 클래스 2개를 따로 만든 것과 같다.
class NestedOuter {
}
class Nested {
}
이 코드와 정적 중첩 클래스의 유일한 차이는 같은 클래스에 있으니 private
접근 제어자에 접근할 수 있다는 정도이다.
정적 중첩 클래스를 사용하도록 리팩터링
NetworkMessage
는 Network
객체 안에서만 사용되는 객체이다.
ex1
패키지를 열어보면 다음 두 클래스가 보일 것이다. (main
은 제외)
- Network
- NetworkMessage
해당 패키지를 처음 확인한 개발자는 Network
와 NetworkMessage
를 둘 다 사용해야 하나? 라고 생각할 것이다. NetworkMessage
에 메시지를 담아서 Network
에 전달해야 하나?와 같은 여러가지 생각을 할 것이다.
아니면 NetworkMessage
가 다른 여러 클래스에서 사용되겠구나라고 생각할 것이다.
이제 정적 중첩 클래스로 리팩터링 해보자.
package nested.nested.ex2;
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);
}
}
}
NetworkMessage
의 접근 제어자를 private
설정했다. 따라서 외부에서 NetworkMessage
에 접근할 수 없다.
- 예)
new Network.NetworkMessage()
처럼 접근할 수 없다.
package nested.nested.ex2;
public class NetworkMain {
public static void main(String[] args) {
Network network = new Network();
network.sendMessage("hello java");
}
}
ex2
패키지를 열어보면 다음 하나의 클래스가 보일 것이다. (main
은 제외)
- Network
Network 관련 라이브러리를 사용하기 위해서 ex2
패키지를 열어본 개발자는 해당 클래스만 확인할 것이다.
추가로 NetworkMessage
가 중첩 클래스에 private
접근 제어자로 되어 있는 것을 보고,Network
내부에서만 단독으로 사용하는 클래스라고 바로 인지할 수 있다.
중첩 클래스 접근
- 나의 클래스에 포함된 중첩 클래스가 아니라 다른 곳에 있는 중첩 클래스에 접근할 때는
바깥클래스.중첩클래스
로 접근해야 한다.- 이런 경우는 중첩 클래스를 사용하지 않는게 좋다.
- 나의 클래스에 포함된 중첩 클래스에 접근할 때는 바깥 클래스 이름을 적지 않아도 된다.
내부 클래스
중첩 클래스와 다르게 내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가 된다.
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);
// 외부 클래스의 클래스 멤버에 접근 가능
System.out.println(outClassValue);
}
}
}
- 내부 클래스는 앞에
static
이 붙지 않는다. 쉽게 이야기해서 인스턴스 멤버가 된다. - 내부 클래스는:
- 자신의 멤버에는 당연히 접근할 수 있다.
- 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
- 바깥 클래스의 클래스 멤버에 접근할 수 있다.
- 바깥 클래스의 내부에 있으므로 바깥 클래스의 private에 접근할 수 있다.
package nested.inner;
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());
}
}
- 내부 클래스는 바깥 클래스의 인스턴스에 소속된다. 따라서 바깥 클래스의 인스턴스 정보를 알아야 생성할 수 있다.
- 내부 클래스는 바깥클래스의 인스턴스 참조.new 내부클래스()로 생성할 수 있다.
- 바깥 클래스의 인스턴스를 먼저 생성해야 내부 클래스의 인스턴스를 생성할 수 있다.
innerClass = class nested.inner.InnerOuter$Inner
개념 - 내부 클래스의 생성
- 개념상 바깥 클래스의 인스턴스 내부에서 내부 클래스의 인스턴스가 생성된다.
- 따라서 내부 인스턴스는 바깥 인스턴스를 알기 때문에 바깥 인스턴스의 멤버에 접근할 수 있다.
실제 - 내부 클래스의 생성
- 실제로 내부 인스턴스가 바깥 인스턴스 안에 생성되는 것은 아니다. 하지만 개념상 인스턴스 안에 생성된다고 하면 충분하다.
- 실제로는 내부 인스턴스는 바깥 인스턴스의 참조를 보관한다. 이 참조를 통해 바깥 인스턴스의 멤버에 접근할 수 있다.
내부 클래스의 활용
리팩터링 전
package nested.inner.ex1;
// 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
클래스에서만 사용된다. - 엔진을 시작하기 위해서는 차의 충전 레벨과 차량의 이름이 필요하다.
Car.getXXX
로 값을 가져와야 한다.Car
클래스는 엔진에 필요한 메서드들을 제공해야 한다.getChargeLevel()
,getModel()
은 엔진에서만 사용하고, 다른 곳에서는 사용하지 않는다.
Car
클래스는 엔진에서만 사용하는 기능을 위해 메서드를 추가해서, 모델 이름과 충전 레벨을 외부에 노출해야 한다.
개발자는 두 클래스의 코드를 모두 확인하고 나서야 아~ Car
클래스만 사용하면 되는구나, Engine
은 단순히 Car
안에서만 사용되는구나라고 이해할 수 있다.
리팩토링 후
package nested.inner.ex2;
public class Car {
private String model;
private int chargeLevel;
private Engine engine;
public Car(String model, int chargeLevel) {
this.model = model;
this.chargeLevel = chargeLevel;
engine = new Engine();
}
public void start() {
engine.start();
System.out.println(model + " 시작 완료");
}
private class Engine {- 엔진을 내부 클래스로 만들었다.
- `Engine.start()`를 기존과 비교해보자.
- `Car`의 인스턴스 변수인 `chargeLevel`에 직접 접근할 수 있다.
- `Car`의 인스턴스 변수인 `model`에 직접 접근할 수 있다.
public void start() {
System.out.println("충전 레벨 확인: " + chargeLevel);
System.out.println(model + "의 엔진을 구동합니다.");
}
}
}
- 엔진을 내부 클래스로 만들었다.
Engine.start()
를 기존과 비교해보자.Car
의 인스턴스 변수인chargeLevel
에 직접 접근할 수 있다.Car
의 인스턴스 변수인model
에 직접 접근할 수 있다.
내부 클래스 생성
- 바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때는 바깥 클래스 이름을 생략할 수 있다.
- 예)
new Engine()
- 바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때 내부 클래스의 인스턴스는 자신을 생성한 바깥 클래스의 인스턴스를 자동으로 참조한다
- this가 있다고 생각해도 된다.
- 예)
리팩토링 후에는 getModel()
, getChargeLevel()
과 같은 메서드를 모두 제거했다.
결과적으로 꼭 필요한 메서드만 외부에 노출함으로써 Car
의 캡슐화를 더 높일 수 있었다.
중첩 클래스는 언제 사용해야 하나?
- 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용해야 한다. 외부 여러 곳에서 특정 클래스를 사용한다면 중첩 클래스로 사용하면 안된다.
중첩 클래스를 사용하는 이유
- 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화가 된다.
패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는 장점도 있다. - 캡슐화: 중첩 클래스는 바깥 클래스의
private
멤버에 접근할 수 있다.
이렇게 해서 둘을 긴밀하게 연결하고 불필요한public
메서드를 제거할 수 있다.
같은 이름의 바깥 변수 접근
바깥 클래스의 인스턴스 변수 이름과 내부 클래스의 인스턴스 변수 이름이 같으면 어떻게 될까?
변수의 이름이 같기 때문에 어떤 변수를 먼저 사용할지 우선순위가 필요하다.
프로그래밍에서 우선순위는 대부분 더 가깝거나, 더 구체적인 것이 우선권을 가진다. 중첩 클래스에서도 마찬가지다. 이렇게 다른 변수들을 가려서 보이지 않게 하는 것을 섀도잉(Shadowing)이라 한다.
다만, 처음부터 이름이 서로 다르게 구분짓는 것이 더 나은 방법이다.
'Programming Language > Java' 카테고리의 다른 글
[Java] 25. 예외 처리 - 이론 (0) | 2025.01.13 |
---|---|
[Java] 24. 중첩 클래스, 내부 클래스(2) (0) | 2025.01.13 |
[Java] 22. 날짜와 시간 (0) | 2025.01.13 |
[Java] 21. 열거형 - ENUM (0) | 2024.11.08 |
[Java] 20. Wrapper/Class/System/Random 클래스 (0) | 2024.11.06 |