클래스 안에 클래스를 중첩해서 정의할 수 있는데 이것을 중첩 클래스라 한다.
중첩 클래스에도 종류가 있다. 총 4가지가 있는데 크게 2가지로 분류한다.
1. 정적 중첩 클래스
2. 내부 클래스 종류
- 내부 클래스
- 지역 클래스
- 익명 클래스
중첩 클래스를 정의하는 위치는 변수의 선언 위치와 같다.
- 정적 중첩 클래스 -> 정적 변수와 같은 위치
- 내부 클래스 -> 인스턴스 변수와 같은 위치
- 지역 클래스 -> 지역 변수와 같은 위치
중첩과 내부의 차이는 무엇일까?
- 중첩(Nested): 어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계
(즉 내 안에 있지만 내 것이 아닌 것, 큰 나무 상자안에 전혀 다른 나무 상자를 넣는다.)
- 내부(Inner): 나의 내부에 있는 나를 구성하는 요소
(나의 심장은 나의 내부에서 나를 구성하는 요소이다.)
여기서 핵심은 바깥 클래스 입장에서 볼 때, 안에 있는 클래스가 나의 인스턴스에 소속이 되는가 되지 않는가의 차이다.
정적 중첩 클래스(static)은 바깥 클래스의 소속이 될 수 없다. 따라서 중첩이다.
내부 클래스는 바깥 클래스를 구성하는 요소이다. 따라서 바깥 클래스의 인스턴스에 소속된다.
*참고로 중첩 클래스는 정적 중첩 클래스와 내부 클래스 모두를 포함하는 뜻임
중첩 클래스를 사용하는 경우?
내부 클래스를 포함한 모든 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 아주 긴밀하게 연결
되어 있을 경우에만 사용해야 한다. 외부의 여러 클래스가 특정 중첩 클래스를 사용한다면 중첩 클래스로 만들면 안된다.
- 논리적 그룹화: 특정 클래스가 다른 하나의 클래스에서만 사용된다면 클래스 안에 포함하는 것이 더 논리적인 그룹화이다.
패키지를 열었을 때 굳이 노출 할 필요도 없고 보기에도 좋음
- 캡슐화 : 중첩 클래스는 바깥클래스의 private에 접근할 수 있다. 이로 인해 불필요한 public 메서드를 줄일 수 있다.
정적 중첩 클래스
package nested.nested;
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);
// 바깥 클래스의 클래스 멤버에 접근 가능
System.out.println(outClassValue);
}
}
}
정적 중첩 클래스는
- 앞에 static이 붙는다.
- 자신의 멤버에 당연히 접근 가능
- 바깥 클래스의 인스턴스에는 접근할 수 없음
- 바깥 클래스의 클래스 멤버에는 접근 가능
private 접근 제어자
- 중첩 클래스도 바깥 클래스와 같은 클래스 안에 있으므로 private 에 접근이 가능하다. (outClassValue를 보자)
package nested.nested;
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와 nested는 아무 관계없는 인스턴스이다. 단지 클래스 구조상 중첩해 두었을 뿐.
Nested.print() 메서드를 보면, 정적 중첩 클래스는 바깥 클래스의 정적 필드에는 접근할 수 있다. 그러나 바깥 인스턴스 필드엔
접근 할 수 없다. 인스턴스 참조가 없기 때문이다.
정리하자면 정적 중첩 클래스는 사실 다른 클래스를 중첩해 둔 것일 뿐이다. 둘은 아무 관계가 없다.
그냥 클래스 2개를 따로 만든거와 같다. 어차피 outClassValue는 static이므로 Nested.outClassValue로 접근 가능하다.
여기서 봐야 할 건 같은 클래스에 있으니 직접 Nested를 참조하지 않아도 private인 outClassValue에 접근이 가능한 부분이다.
정적 중첩클래스의 활용
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);
}
}
}
text를 입력받아서 NetworkMessage를 생성하고 출력하는 코드이다.
NetworkMessage가 별도의 클래스로 생성되어 관리되고 있다면, 이 코드를 사용하게 될 다른 개발자는 Network 클래스와
NetworkMessage 클래스를 둘 다 확인하고, 코드를 작성하는 과정을 겪게 될 것이다. 이를 중첩 클래스로 활용하면
private 접근 제어자를 확인 후 Network 내부에서만 단독으로 사용되는 클래스라고 인지할 수 있다.
이게 위에서 설명한 중첩 클래스를 사용해야되는 이유 중 논리적 그룹화에 대한 내용이다.
내부 클래스 (Inner)
내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가 된다. (내부 클래스는 바같 클래스 인스턴스에 소속된다)
정적 중첩 클래스
- static이 붙는다.
- 바깥 클래스 인스턴스에 소속되지 않는다.
내부 클래스
- static이 붙지 않는다.
- 바깥 클래스 인스턴스에 소속된다.
package nested.inner;
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);
// 외부 클래스의 인스턴스 멤버에 접근 가능
System.out.println(outInstanceValue);
// 외부 클래스의 클래스 멤버에 접근 가능
System.out.println(outClassValue);
}
}
}
코드를 보고 내부 클래스의 스코프를 확인해보자
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 내부클래스() 로 생성할 수 있다.
바깥 클래스를 먼저 생성해야 내부클래스의 인스턴스를 생성할 수 있다.
개념적인 부분의 내부 클래스 생성 이미지이다. 실제론 바깥 클래스의 내부에 생성되지 않는다. 하지만 '개념상' 인스턴스 안에
생성된다고 이해해도 된다.
내부 클래스의 활용
리팩토링 전 코드
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() + "의 엔진을 구동합니다.");
}
}
Engine은 Car 클래스에서만 사용된다.
엔진을 시작하기 위해선 getModel() 과 getChargeLevel() 이 필요한 상태
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 String getModel() {
return model;
}
// Engine 에서만 사용
public int getChargeLevel() {
return chargeLevel;
}
public void start() {
engine.start();
System.out.println(model + "시작 완료");
}
}
Car 클래스이 getModel과 getLevel을 메서드를 생성했다.
여기서 생기는 문제점은 model과 chargeLevel이 외부에 노출된다는 점이다. 이 두가지는 외부에서 사용할 일이 없는 코드다
리팩토링 후
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;
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 + "의 엔진을 구동합니다.");
}
}
}
Car 생성자에 new Engine으로 Car 인스턴스가 생성되면 Engine 인스턴스도 자동으로 생성하게 되었다.
또한 public이던 get 메서드들을 삭제하고 내부 클래스에서 바깥 클래스에 직접 참조할 수 있도록 코드가 변경되었다.
캡슐화의 관점에서 잘 생각해보면 내부클래스를 이해하는기 쉬울 것 같다
'Java' 카테고리의 다른 글
예외 처리 (1) (0) | 2024.12.10 |
---|---|
중첩 클래스, 내부 클래스 (Local, Anonymous) (0) | 2024.11.19 |
날짜와 시간 라이브러리 (2) (0) | 2024.11.12 |
날짜와 시간 라이브러리(1) (0) | 2024.11.12 |
열거형 - ENUM (2) | 2024.11.05 |