Search
Duplicate
📒

[Java Study] 10-1. 중첩, 내부 클래스

상태
완료
수업
Java Study
주제
Collection
4 more properties
참고

중첩 클래스

NOTE
중첩 클래스의 개념은 한 클래스 내부에 다른 클래스를 정의하는 방식으로, 클래스를 논리적으로 그룹화하여 캡슐화를 강화하고, 더 읽기 쉽고 유지보수가 편리한 코드를 작성하기 위해 사용됩니다.
중첩 클래스 종류
논리적 그룹화: 특정 클래스가 다른 클래스 내에서만 사용될 경우, 두 클래스를 함께 묶어 코드의 구조를 더욱 명확하게 할 수 있습니다.
캡슐화 향상: 중첩 클래스를 통해 외부 클래스의 private 멤버에 접근할 수 있게 되므로, 불필요한 public 메서드를 줄일 수 있습니다.
중첩은 위치상 내부에 있지만 소속이 아닌 것을 의미하며, 반면에 내부는 구성 요소로서 내부에 있는 것을 말합니다.
정적 중첩 클래스는 외부 클래스와 별개이며, 위치상으로만 중첩되어 있습니다.
내부 클래스는 외부 클래스의 구성 요소로서, 외부 클래스에 소속됩니다.

자바의 메모리 구조와 중첩 클래스

NOTE
자바의 메모리 구조를 이해하는 것은 중첩 클래스의 작동 방식을 깊게 이해하는데 중요한 키포인트 입니다.
자바 메모리 구조
메서드 영역: 클래스 관련 정보(실행 코드, 필드, 메서드 등..)및 static 변수가 저장됩니다.
스택 영역: 각 메서드 호출 시 생성되는 스택 프레임(지역 변수, 중간 연산 결과 등)을 관리합니다.
힙 영역: 객체 인스턴스와 배열이 생성되는 영역으로, 가비지 컬렉션의 대상이 됩니다.

정적 중첩 클래스

NOTE
정적 중첩 클래스는 클래스 내부에 위치하며, 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 = " + nestedInstanceValue); // System.out.println("outInstanceValue = " + outInstanceValue); // 접근불가 System.out.println("outClassValue = " + outClassValue); // 중첩 클래스 내부에서는 외부 클래스 명을 적지 않아도 됩니다. } } }
Java
복사
정적 중첩 클래스 변수접근

사용사례

정적 중첩 클래스는 외부 클래스와 관련된 작업을 수행하는 헬퍼 클래스나 유틸리티 클래스를 정의할 때 주로 사용됩니다.
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 = " + content); } } }
Java
복사
정적 중첩 클래스 활용예시

내부 클래스

NOTE
내부 클래스는 외부 클래스의 인스턴스를 구성하는 클래스로, 외부 클래스의 인스턴스에 속하게 됩니다.
내부 클래스는 특정 클래스가 다른 클래스 안에서만 사용되거나, 매우 밀접하게 연결된 경우에만 사용하는 것이 좋습니다. 또한, 내부 클래스와 외부 클래스의 변수명이 같을 경우, 내부 클래스의 우선순위가 더 높습니다.
내부 클래스 메모리 예시
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); } } }
Java
복사
정적 중첩과 달리 외부 클래스의 모든 필드에 접근이 가능합니다.

사용사례

내부 클래스를 사용하면 내부 클래스에서 private에 접근할 수 있어 불필요한 public 메서드를 줄일 수 있습니다. 또한, EngineCar가 생성된 후에만 생성할 수 있게 됩니다.
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); } // 내부 클래스 사용시 제거 public String getModel(){ return model; } // 내부 클래스 사용시 제거 public int getChargeLevel(){ return chargeLevel; } public void start(){ engine.start(); System.out.println(model + "시작 완료"); } } // 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() + "의 엔진을 사용합니다."); } }
Java
복사
내부 클래스 미사용 - 불필요한 public 메서드 증가, 읽기 어려움
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 + "시작 완료"); } public class Engine { public void start(){ System.out.println("충전 레벨 확인:" + chargeLevel); System.out.println(model + "의 엔진을 사용합니다."); } } }
Java
복사
내부 클래스 사용

지역 클래스

NOTE
지역 클래스는 특정 메소드 내부에서만 정의되고 사용되는 클래스입니다. 해당 메소드의 실행 동안만 존재하며, 메소드의 완료와 함께 사라지게 됩니다.
지역 클래스는 특정 메소드 내부에서만 정의되어, 그 메소드 내부에서만 인스턴스화되고 사용될 수 있습니다. 이러한 특성 때문에, 지역 클래스는 그 메소드에서만 필요한 로직을 구현하는데 사용됩니다.
public class LocalOuterV1 { private int outInstanceVar = 3; public void process(int paramVar) { int localVar = 1; // 지역 클래스 class LocalPrinter{ int value = 0; public void printData(){ System.out.println("value = " + value); System.out.println("localVar = " + localVar); System.out.println("paramVar = " + paramVar); System.out.println("outInstanceVar = " + outInstanceVar); } } LocalPrinter printer = new LocalPrinter(); printer.printData(); } public static void main(String[] args) { LocalOuterV1 localOuterV1 = new LocalOuterV1(); localOuterV1.process(2); } }
Java
복사
지역 클래스 예제

지역변수 캡쳐

지역 클래스는 외부 메소드의 멤버에 접근 가능합니다. 그런데 메소드가 끝나고 나서도 내부 클래스는 메소드의 필드 값을 호출할 수 있습니다.
public class LocalOuterV3 { private int outInstanceVar = 3; public Printer process(int paramVar) { int localVar = 1; // 지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다. class LocalPrinter implements Printer { int value = 0; @Override public void print(){ System.out.println("value = " + value); // 인스턴스는 지역 변수보다 더 오래 살아 남는다. System.out.println("localVar = " + localVar); System.out.println("paramVar = " + paramVar); System.out.println("outInstanceVar = " + outInstanceVar); } } LocalPrinter printer = new LocalPrinter(); // printer.print(); 여기서 실행하지 않고 Printer 인스턴스만 반환 // localVar = 10; 불가능(캡쳐한 변수는 사실상 final 이다) return printer; } public static void main(String[] args) { LocalOuterV3 localOuterV1 = new LocalOuterV3(); Printer printer = localOuterV1.process(2); // printer.print()를 나중에 실행한다. process()의 스낵 프레임이 사라진뒤에 실행한다. // process()가 스택에서 제거되었는데 localVar값도 출력됨 printer.print(); // => 메소드는 끝났는데 어떻게 메소드 필드를 호출하지? // ... } }
Java
복사
메서드가 끝났는데 메서드 필드를 호출할 수 있다.
이유는 지역 클래스는 외부 클래스에서 사용하는 값들을 미리 복사(캡쳐)해서 가지고 있기 때문입니다.
지역 변수를 캡쳐해서 들고 있다.
지역변수 캡쳐로 인해 process()가 스택 메모리에서 제거되어도 메소드 내에서 사용되는 지역변수에 접근이 가능합니다. 단 이렇게 사용되는 지역변수는 동기화 문제가 있어 수정할 수 없습니다. (사실상 final)

익명클래스

NOTE
익명 클래스는 이름이 없는 클래스로, 선언과 동시에 객체가 생성됩니다. 주로 단일 사용 객체를 만들 때나 인터페이스의 간단한 구현체를 필요로 할 때 사용됩니다.
public interface Printer { void print(); }
Java
복사
public class AnonymousOuter { private int outInstanceVar = 3; public void process(int paramVar) { int localVar = 1; // 클래스 이름없이 생성! Printer printer = new Printer() { int value = 0; @Override public void print() { System.out.println("value = " + value); System.out.println("localVar = " + localVar); System.out.println("paramVar = " + paramVar); System.out.println("outInstanceVar = " + outInstanceVar); } }; // ... } }
Java
복사
익명 클래스 예제
익명 클래스는 다음과 같은 특징을 가집니다.
이름이 없기 때문에 생성자를 정의할 수 없으며, 상속받은 클래스나 구현한 인터페이스의 메소드만을 오버라이드 할 수 있습니다.
외부 클래스의 변수에 접근할 수 있으며, 이를 통해 메소드의 파라미터나 로컬 변수의 값을 사용할 수 있습니다.
익명 클래스는 AnonymousOuter$1과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의됩니다.

람다 표현식 활용

자바8에서 도입된 람다 표현식은 익명 클래스의 개념을 한 단계 발전시켜, 메소드를 하나의 식으로 표현할 수 있게 해줍니다. 특히 단일 메소드 인터페이스를 구현할 때 코드를 매우 간결하게 작성할 수 있습니다.
public class Ex1Main { public static void hello(Process process) { System.out.println("프로그램 시작"); process.run(); System.out.println("프로그램 종료"); } // 내부 클래스 public static void main(String[] args) { class Dice implements Process { @Override public void run() { int randomValue = new Random().nextInt(6) + 1; System.out.println("주사위 = " + randomValue); } } class Sum implements Process { @Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println("i = " + i); } } } Process dice = new Dice(); Process sum = new Sum(); System.out.println("Hello 실행"); hello(dice); hello(sum); } }
Java
복사
람다 리팩토링 이전코드
public class Ex2Main { // 람다 함수 인터페이스를 받는다. public static void hello(Runnable runnable){ System.out.println("프로그램 시작"); runnable.run(); System.out.println("프로그램 종료"); } public static void main(String[] args) { // 람다 사용 hello(() -> { int randomValue = new Random().nextInt(6) + 1; System.out.println("주사위 = " + randomValue); }); // 람다 사용 hello(() -> { for (int i = 0; i < 3; i++) { System.out.println("i = " + i); } }); } }
Java
복사
람다 리팩토링 코드 (코드 길이가 훨씬 줄어들었다!)