F-lab Java 1주차 / Phase 2 / Unit 2.6 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 2.4 (다형성)
다음 Unit: Phase 3 — SOLID 5원칙이 Unit의 의미: Phase 2의 마무리 + 람다(3주차)의 전신.
클래스 안의 클래스라는 개념을 이해해야 Spring 코드, 콜백 패턴, 이벤트 리스너를 자연스럽게 다룰 수 있다.
큰 집을 상상해보세요. 집 안에는 다양한 방이 있습니다:
각 방은:
→ Nested 클래스도 마찬가지 — 외부 클래스 안에서만 의미 있는 작은 공간.
회의 중 잠깐 메모할 일이 생겼습니다:
특징:
→ Anonymous 클래스 — 이름 없이 즉석에서 정의하는 일회용 클래스.
스마트폰 알람 앱에서:
"이 알람이 울리면 이 동작 을 해줘"
알람: 7시 30분 → ?
↓
당신: 그때만의 동작 정의
"음악 끄고 + 커튼 열고 + 커피 머신 켜기"
이 동작은:
→ 익명 클래스의 정신. 자바에서는 클릭 리스너, 콜백 등에서 자주 사용.
자바 초기 (1.0, 1995) 에는 모든 클래스가 별도 파일 이었습니다. 그러다 Java 1.1 (1997) 에서 Nested 클래스가 도입.
그 전의 문제 — 작은 도우미 클래스의 외로움:
// MyList.java
public class MyList {
private Object[] data;
public Iterator iterator() {
return new MyListIterator(this);
}
}
// MyListIterator.java — 별도 파일
public class MyListIterator implements Iterator {
private MyList list;
private int index;
public MyListIterator(MyList list) {
this.list = list;
}
public boolean hasNext() { ... }
public Object next() { ... }
}
문제:
MyListIterator 는 MyList 만을 위한 클래스MyList 의 내부 정보 접근 어려움 (private 못 봄)MyListIterator 잘못 사용할 위험public class MyList {
private Object[] data;
// 클래스 안의 클래스 ⭐
private class MyListIterator implements Iterator {
private int index;
public boolean hasNext() {
return index < data.length; // 외부 클래스의 private 접근 가능!
}
public Object next() { ... }
}
public Iterator iterator() {
return new MyListIterator();
}
}
효과:
MyListIterator 가 MyList 의 일부임을 명확히문제: 한 번만 쓸 클래스를 매번 정의하기 번거로움
// Java 1.1 이전 — 별도 클래스 정의 필수
public class MyButtonClickListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("클릭됨");
}
}
button.addActionListener(new MyButtonClickListener());
// 버튼 1개를 위해 별도 파일/클래스 정의 ❌
Java 1.1의 익명 클래스:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("클릭됨");
}
});
// 한 번에 끝 ✅
→ GUI 프로그래밍, 이벤트 처리, 콜백 패턴 의 폭발적 활용.
20년 가까이 익명 클래스가 콜백의 표준이었지만, 너무 장황하다 는 문제:
// 익명 클래스
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
// Java 8 람다 — 같은 일을 한 줄에
Comparator<String> comp = (a, b) -> a.length() - b.length();
람다의 본질:
→ Nested/Inner/Anonymous는 람다의 전신. 진화의 역사를 알아야 람다를 정확히 이해.
"클래스 안의 클래스는 '강한 관계' 를 표현하는 도구다."
외부 클래스와 떼려야 뗄 수 없는 작은 클래스를 만들 때, 별도 파일이 아닌 안에 두면 관계가 명확 해지고 캡슐화가 강해진다.
익명 클래스는 더 나아가 이름조차 필요 없는 일회용 객체 를 즉석에서 만드는 도구. 이 사고방식이 람다로 진화.
// ❌ 별도 파일로 분리한다면
public class PageRequest {
private int page;
private int size;
}
public class PageResult {
private List<?> data;
private int totalCount;
private int currentPage;
}
public class FareService {
public PageResult findByPage(PageRequest request) { ... }
}
문제:
PageRequest, PageResult 는 페이지네이션에서만 쓰는데 공용 위치에 있음public class FareService {
// 메서드의 입력 — 외부에서도 사용
public static class PageRequest {
private int page;
private int size;
}
// 메서드의 결과 — 외부에서도 사용
public static class PageResult<T> {
private List<T> data;
private int totalCount;
private int currentPage;
}
public PageResult<Fare> findByPage(PageRequest request) {
// ...
}
}
// 사용
FareService.PageRequest request = new FareService.PageRequest(0, 10);
FareService.PageResult<Fare> result = fareService.findByPage(request);
효과:
PageRequest 가 FareService 와 관련됨이 명확// Spring의 트랜잭션 콜백
public class FareService {
public void processInTransaction() {
transactionTemplate.execute(new MyTransactionCallback());
}
}
// 별도 클래스 정의
public class MyTransactionCallback implements TransactionCallback<Void> {
@Override
public Void doInTransaction(TransactionStatus status) {
// 트랜잭션 안에서 할 일
return null;
}
}
문제:
public class FareService {
public void processInTransaction() {
transactionTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
// 트랜잭션 안에서 할 일
return null;
}
});
}
}
효과:
public class FareService {
public void processInTransaction() {
transactionTemplate.execute(status -> {
// 트랜잭션 안에서 할 일
return null;
});
}
}
→ 익명 클래스 → 람다로의 진화. 같은 일, 짧은 표현.
1. Top-Level Class — 일반 클래스 (별도 파일)
2. Nested Class — 클래스 안의 클래스
├── Static Nested Class — static 키워드 있음
└── Inner Class — static 없음 (= 내부 클래스)
├── Member Inner — 클래스 본문에 정의
├── Local Inner — 메서드 안에 정의
└── Anonymous Inner — 이름 없는 즉석 정의
용어 정리:
public class Outer {
private static String staticData = "static";
private String instanceData = "instance";
public static class StaticNested {
public void show() {
System.out.println(staticData); // ✅ static 멤버 접근 OK
// System.out.println(instanceData); // ❌ 인스턴스 멤버 접근 X
}
}
}
// 사용 — 외부 클래스 인스턴스 불필요
Outer.StaticNested nested = new Outer.StaticNested();
nested.show();
특징 ⭐ :
언제 사용:
public class Outer {
private String instanceData = "instance";
public class Inner {
public void show() {
System.out.println(instanceData); // ✅ 외부 인스턴스 멤버 접근 OK
}
}
}
// 사용 — 외부 클래스 인스턴스 먼저 필요
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // ⚠️ 특이한 문법
inner.show();
특징 ⭐ :
언제 사용:
public class Outer {
public void doSomething() {
// 메서드 안에서만 의미 있는 클래스
class Helper {
public void help() {
System.out.println("도움");
}
}
Helper h = new Helper();
h.help();
}
}
특징:
언제 사용:
이름 없이 즉석에서 정의 + 인스턴스 생성:
public class FareService {
public void process() {
// 한 줄짜리 콜백
Runnable task = new Runnable() { // ← 익명 클래스
@Override
public void run() {
System.out.println("작업 실행");
}
};
task.run();
}
}
문법 ⭐ :
new 인터페이스또는클래스() {
// 메서드 구현
};
특징:
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("스레드 실행");
}
};
thread.start();
| Static Nested | Member Inner | Local Inner | Anonymous | |
|---|---|---|---|---|
| 위치 | 클래스 본문 | 클래스 본문 | 메서드 안 | 즉석 |
| 이름 | O | O | O | X |
| static 키워드 | O | X | X | X |
| 외부 인스턴스 필요 | X | O | (메서드 호출 시) | (메서드 호출 시) |
| 외부 멤버 접근 | static만 | 모두 | 모두 + 지역변수 | 모두 + 지역변수 |
| 사용 빈도 | 높음 | 중간 | 매우 낮음 | 높음 (람다로 대체 중) |
자바 컴파일러는 Nested 클래스를 별도 .class 파일 로 분리합니다:
// Outer.java
public class Outer {
public static class StaticNested { ... }
public class Inner { ... }
public void method() {
class Local { ... }
Runnable r = new Runnable() { ... };
}
}
컴파일 결과:
Outer.class
Outer$StaticNested.class ← static nested
Outer$Inner.class ← inner
Outer$1Local.class ← local
Outer$1.class ← anonymous
$ 기호 ⭐ — 자바의 nested 클래스 명명 규칙.
→ JVM 입장에서는 모두 별도의 클래스.
Inner Class는 외부 클래스의 인스턴스 참조 를 자동으로 보유:
public class Outer {
private String data = "outer data";
public class Inner {
public void show() {
System.out.println(data); // 어떻게 접근?
}
}
}
컴파일러가 변환 (의사 코드):
public class Outer {
private String data = "outer data";
}
public class Outer$Inner {
private final Outer this$0; // ← 숨겨진 외부 참조
public Outer$Inner(Outer outer) {
this.this$0 = outer;
}
public void show() {
System.out.println(this$0.data); // 외부 참조로 접근
}
}
중요한 함의 ⭐⭐ :
public class Outer {
private int count = 0;
public void process() {
Runnable r = new Runnable() {
@Override
public void run() {
count++; // 외부 필드 접근
}
};
}
}
컴파일러가 생성:
class Outer$1 implements Runnable {
private final Outer this$0;
public Outer$1(Outer outer) {
this.this$0 = outer;
}
@Override
public void run() {
this$0.count++;
}
}
→ 익명 클래스도 결국 별도 .class 파일 + 외부 참조 보유.
Local/Anonymous 클래스가 메서드 지역변수 를 사용할 때:
public void process() {
int count = 10; // 지역변수
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(count); // ✅ 사용 가능
// count = 20; // ❌ 컴파일 에러
}
};
}
규칙 ⭐ :
왜?:
effectively final:
public void process() {
int count = 10; // 한 번도 재할당 안 됨 → effectively final
Runnable r = () -> System.out.println(count); // ✅
}
// vs
public void process() {
int count = 10;
count = 20; // 재할당 → effectively final 아님
Runnable r = () -> System.out.println(count); // ❌ 컴파일 에러
}
// 익명 클래스 (Java 1.1+)
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("클릭");
}
});
// 람다 (Java 8+)
button.addActionListener(e -> System.out.println("클릭"));
람다가 가능한 조건 — 함수형 인터페이스 (메서드 1개):
@FunctionalInterface
public interface ActionListener {
void actionPerformed(ActionEvent e); // 메서드 1개
}
람다의 한계:
→ 3주차에서 본격 학습.
public class Fare {
private final Long id;
private final int amount;
private final String currency;
private final Customer customer;
private Fare(Builder builder) {
this.id = builder.id;
this.amount = builder.amount;
this.currency = builder.currency;
this.customer = builder.customer;
}
public static Builder builder() {
return new Builder();
}
// Static Nested Class — Builder
public static class Builder {
private Long id;
private int amount;
private String currency = "KRW"; // 기본값
private Customer customer;
public Builder id(Long id) {
this.id = id;
return this;
}
public Builder amount(int amount) {
if (amount < 0) throw new IllegalArgumentException();
this.amount = amount;
return this;
}
public Builder currency(String currency) {
this.currency = currency;
return this;
}
public Builder customer(Customer customer) {
this.customer = customer;
return this;
}
public Fare build() {
return new Fare(this);
}
}
}
// 사용
Fare fare = Fare.builder()
.id(1L)
.amount(50000)
.currency("USD")
.customer(alice)
.build();
효과:
Builder 가 Fare 와 강하게 연관됨이 명확→ Lombok의 @Builder 도 내부적으로 이 패턴.
public class FareController {
@PostMapping("/api/fares")
public ApiResponse<FareResponse> create(@RequestBody CreateFareRequest request) {
Fare fare = fareService.create(request);
return ApiResponse.success(FareResponse.from(fare));
}
// 응답 DTO — Static Nested
public static class CreateFareRequest {
private Long customerId;
private int amount;
// getters/setters
}
public static class FareResponse {
private Long id;
private int amount;
private String status;
public static FareResponse from(Fare fare) {
FareResponse response = new FareResponse();
response.id = fare.getId();
response.amount = fare.getAmount();
response.status = fare.getStatus().name();
return response;
}
}
}
효과:
public class FareCollection {
private Fare[] fares;
private int size;
public Iterator<Fare> iterator() {
return new FareIterator(); // 외부 인스턴스의 it
}
// Inner Class — 외부의 fares, size 직접 접근
private class FareIterator implements Iterator<Fare> {
private int index = 0;
@Override
public boolean hasNext() {
return index < size; // 외부 클래스의 size 접근
}
@Override
public Fare next() {
return fares[index++]; // 외부 클래스의 fares 접근
}
}
}
// 사용
FareCollection collection = new FareCollection();
for (Fare fare : collection) { // for-each 가능
// ...
}
효과:
FareIterator 가 FareCollection 의 내부에 강하게 의존→ ArrayList, LinkedList 등 자바 컬렉션이 정확히 이 패턴.
@Service
public class FareService {
@Autowired
private TransactionTemplate transactionTemplate;
public void processInTransaction(Long fareId) {
// 익명 클래스로 콜백 정의
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Fare fare = fareRepository.findById(fareId).orElseThrow();
fare.process();
fareRepository.save(fare);
}
});
}
}
// 람다로 (Java 8+)
public void processInTransactionLambda(Long fareId) {
transactionTemplate.execute(status -> {
Fare fare = fareRepository.findById(fareId).orElseThrow();
fare.process();
fareRepository.save(fare);
return null;
});
}
→ 콜백은 익명 클래스의 대표 사용처. 람다로 진화.
public class IlicEventBus {
private final List<EventListener> listeners = new ArrayList<>();
public void register(EventListener listener) {
listeners.add(listener);
}
}
public class IlicApplication {
public void init() {
// 운임 이벤트 리스너 등록
eventBus.register(new EventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof FareCreatedEvent fareEvent) {
System.out.println("운임 생성됨: " + fareEvent.getFareId());
}
}
});
// 결제 이벤트 리스너 등록
eventBus.register(new EventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof PaymentCompletedEvent paymentEvent) {
System.out.println("결제 완료: " + paymentEvent.getPaymentId());
}
}
});
}
}
// 람다로 (인터페이스 메서드 1개일 때)
eventBus.register(event -> {
if (event instanceof FareCreatedEvent fareEvent) {
System.out.println("운임 생성됨: " + fareEvent.getFareId());
}
});
List<Fare> fares = ...;
// 익명 클래스
fares.sort(new Comparator<Fare>() {
@Override
public int compare(Fare a, Fare b) {
return Integer.compare(a.getAmount(), b.getAmount());
}
});
// 람다 (Java 8+)
fares.sort((a, b) -> Integer.compare(a.getAmount(), b.getAmount()));
// 메서드 참조 (더 간결)
fares.sort(Comparator.comparingInt(Fare::getAmount));
→ 익명 클래스 → 람다 → 메서드 참조 의 진화 흐름.
public class FareSearchCriteria {
private final Range<Integer> amountRange;
private final Range<LocalDate> dateRange;
private final List<FareStatus> statuses;
// Static Nested — Range 클래스 (FareSearchCriteria 전용)
public static class Range<T extends Comparable<T>> {
private final T from;
private final T to;
public Range(T from, T to) {
if (from != null && to != null && from.compareTo(to) > 0) {
throw new IllegalArgumentException("from > to");
}
this.from = from;
this.to = to;
}
public boolean contains(T value) {
if (from != null && value.compareTo(from) < 0) return false;
if (to != null && value.compareTo(to) > 0) return false;
return true;
}
}
}
// 사용
FareSearchCriteria.Range<Integer> amountRange = new FareSearchCriteria.Range<>(1000, 100000);
public class Outer {
private byte[] hugeData = new byte[100 * 1024 * 1024]; // 100MB
public class Inner {
// Outer 참조 자동 보유
}
public Inner createInner() {
return new Inner();
}
}
// 사용
Outer outer = new Outer();
Inner inner = outer.createInner();
outer = null; // outer 참조 해제 시도
// 그러나 inner가 살아있으면 outer도 GC 안 됨 ❌
// → 100MB 누수
해결 — Static Nested 사용:
public class Outer {
private byte[] hugeData = new byte[100 * 1024 * 1024];
public static class StaticInner {
// Outer 참조 없음 ✅
}
}
원칙 ⭐ :
"필요 없으면 Static Nested"
"Inner Class는 정말 외부 인스턴스에 접근해야 할 때만"
→ Joshua Bloch (Effective Java) 강력 권장.
public class Outer {
public void process() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(this); // ⚠️ 이 this는 누구?
System.out.println(Outer.this); // ← 외부 클래스의 this
}
};
}
}
규칙:
this = 익명 클래스 인스턴스외부클래스명.this = 외부 클래스 인스턴스public void process() {
int count = 0;
Runnable r = new Runnable() {
@Override
public void run() {
count++; // ❌ 컴파일 에러 — effectively final 아님
}
};
}
해결 — 변경 가능한 컨테이너 사용:
public void process() {
int[] count = {0}; // 배열은 참조가 final이면 OK
// 또는
AtomicInteger count = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
count[0]++; // ✅
// count.incrementAndGet(); // AtomicInteger
}
};
}
→ 약간의 우회. 진짜 권장은 상태를 외부에서 관리.
// ❌ 자바 8+ 환경에서 너무 장황
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
doSomething();
}
});
// ✅ 람다로 간결하게
button.addActionListener(e -> doSomething());
조건 ⭐ :
// ❌ 가독성 지옥
service.process(new Runnable() {
@Override
public void run() {
helper.execute(new Callback() {
@Override
public void onComplete() {
another.handle(new Handler() {
@Override
public void handle() {
// ... 콜백 지옥 ❌
}
});
}
});
}
});
해결 — 람다 + 메서드 분리:
service.process(this::doWork);
private void doWork() {
helper.execute(this::onComplete);
}
private void onComplete() {
another.handle(this::onHandle);
}
private void onHandle() { ... }
→ 콜백 지옥은 비동기 프로그래밍의 고전적 문제. 현대에는 CompletableFuture, Reactive 로 해결.
public class Outer {
public class Helper { // ⚠️ static 빠짐 — Inner Class 됨
// ...
}
}
// 사용 시
Outer.Helper h = new Outer.Helper(); // ❌ 컴파일 에러
// Outer 인스턴스 필요
Outer outer = new Outer();
Outer.Helper h = outer.new Helper(); // ⚠️ 특이한 문법
규칙:
"기본은 static, 정말 필요할 때만 인스턴스 멤버 접근용 inner"
Runnable r = new Runnable() {
@Override
public void run() { ... }
public void newMethod() { // ⚠️ 정의는 가능
// ...
}
};
r.newMethod(); // ❌ 컴파일 에러 — Runnable에 없음
→ 익명 클래스에 새 메서드 추가해도 외부에서 호출 불가 (인터페이스 타입으로만 다룸).
→ 새 메서드가 필요하면 이름 있는 클래스 사용.
[Phase 2 완료]
↓
[Phase 3: SOLID 5원칙] — 객체 설계의 황금률
↓
[Phase 4: JVM 메모리 모델]
↓
... 계속
[Anonymous Class (Java 1.1)]
↓
[Lambda (Java 8)]
↓
[Method Reference (Java 8)]
↓
[Stream API (Java 8)]
→ 3주차에서 본격 학습.
1주차 내:
미래 주차:
"외부 클래스의 인스턴스 멤버에 접근해야 하나?"
↓
YES ─→ Inner Class
| (Iterator, View 등 강한 결합)
|
NO ──→ Static Nested Class ⭐ (대부분의 경우)
(Builder, DTO, 보조 클래스)
원칙: 기본은 static, 정말 필요할 때만 inner.
| 질문 | 이 Unit에서의 답 |
|---|---|
| "Inner Class와 Static Nested Class의 차이?" | static 유무 + 외부 인스턴스 접근 차이 |
| "Inner Class의 메모리 누수 위험?" | 외부 참조 자동 보유 → 외부 GC 방해 |
| "익명 클래스란?" | 이름 없이 즉석에서 정의하는 일회용 클래스 |
| "익명 클래스와 람다의 차이?" | 람다는 함수형 인터페이스 한정, 더 간결 |
| "Local Inner Class의 effectively final?" | 지역변수는 변경 불가여야 사용 가능 |
1️⃣ Nested 클래스는 "강한 관계" 를 표현하는 도구다.
외부 클래스와 떼려야 뗄 수 없는 작은 클래스를 안에 두면 관계가 명확해지고 캡슐화가 강해진다. Static Nested (외부 참조 없음) 와 Inner (외부 참조 보유) 로 나뉘는데, 메모리 누수 방지를 위해 가능하면 Static Nested 권장.
2️⃣ 익명 클래스는 "이름 없는 일회용 객체" 를 즉석에서 만든다.
한 번만 쓸 콜백이나 리스너에 적합. 컴파일러가
Outer$1.class같은 별도 .class 파일로 변환. 함수형 인터페이스인 경우 Java 8+ 람다로 더 간결하게 표현 가능 — 익명 클래스가 람다의 전신이다.3️⃣ Local Inner의 지역변수는 effectively final 이어야 한다.
메서드 안의 클래스가 외부 메서드의 지역변수를 사용하려면 그 변수는 final 또는 사실상 final (재할당 안 됨). 컴파일러가 지역변수를 클래스 안에 복사하기 때문에, 원본과 복사본의 일관성을 위한 강제. 이 규칙은 람다에도 그대로 적용된다.
Q1: 익명 클래스는 언제 람다로 대체할 수 있는가?
조건: 함수형 인터페이스 (메서드 1개) 의 익명 클래스만 람다로 대체 가능.
가능한 경우:
// 인터페이스가 메서드 1개
@FunctionalInterface
public interface Runnable {
void run();
}
// 익명 클래스
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("실행");
}
};
// 람다로 변환 ✅
Runnable r = () -> System.out.println("실행");
대표 함수형 인터페이스:
Runnable (run)Comparator<T> (compare)Consumer<T> (accept)Function<T, R> (apply)Supplier<T> (get)Predicate<T> (test)불가능한 경우:
public interface MultiInterface {
void methodA();
void methodB();
}
// 익명 클래스만 가능 — 람다 X
MultiInterface m = new MultiInterface() {
@Override public void methodA() { ... }
@Override public void methodB() { ... }
};
// 익명 클래스로 클래스 상속
Thread t = new Thread() {
@Override
public void run() { ... }
};
// 람다는 인터페이스 한정 — 클래스 상속 X
// 위 코드를 람다로? 가능 — Thread는 Runnable을 받음
Thread t = new Thread(() -> { ... });
Runnable r = new Runnable() {
private int count = 0; // 상태 보유
@Override
public void run() {
count++;
System.out.println(count);
}
};
// 람다는 상태 보유 X — 외부 변수 활용해야
람다의 한계 정리:
→ 간단한 콜백은 람다, 복잡한 객체는 익명 클래스 또는 명명 클래스.
Q2: 내부 클래스가 외부 클래스의 private 필드에 접근 가능한 이유는?
한 줄 답: 컴파일러가 외부 클래스의 인스턴스 참조를 자동으로 내부 클래스에 주입 하기 때문.
상세 설명:
public class Outer {
private String secret = "비밀";
public class Inner {
public void reveal() {
System.out.println(secret); // ✅ private 접근 OK
}
}
}
컴파일러가 변환 (의사 코드):
public class Outer {
private String secret = "비밀";
// 합성 메서드 자동 생성 ⭐
static String access$000(Outer outer) {
return outer.secret;
}
}
public class Outer$Inner {
final Outer this$0; // 외부 인스턴스 참조 자동 보유
public Outer$Inner(Outer outer) {
this.this$0 = outer;
}
public void reveal() {
// 합성 메서드를 통해 접근
System.out.println(Outer.access$000(this$0));
}
}
핵심 메커니즘 ⭐ :
컴파일러가 외부 인스턴스 참조 주입
this$0 라는 숨겨진 필드에 저장합성 메서드 (Synthetic Method) 자동 생성
access$000, access$100 같은 이름JVM 입장에서는 private 직접 접근 X
왜 이렇게 동작? — 언어의 편의성을 위한 설계:
"내부 클래스는 외부 클래스의 일부" 라는 직관을 코드에서도 유지하기 위해
private 멤버에도 자연스럽게 접근할 수 있도록 컴파일러가 도와줌
Static Nested와 비교:
public class Outer {
private static String staticSecret = "static 비밀";
private String instanceSecret = "instance 비밀";
public static class StaticNested {
public void show() {
System.out.println(staticSecret); // ✅ static 접근 OK
// System.out.println(instanceSecret); // ❌ 외부 인스턴스 없음
}
}
}
→ Static Nested는 외부 인스턴스 참조 없음 → 인스턴스 멤버 접근 X.
Inner의 위험성 ⚠️ :
이 자동 외부 참조 때문에:
결론:
"외부 인스턴스 멤버에 접근해야 한다면 Inner Class,
그렇지 않다면 Static Nested Class를 사용하라" — Joshua Bloch (Effective Java)
| Unit | 주제 | 핵심 |
|---|---|---|
| 2.1 | 메서드의 구조 | 시그니처, 접근 제어자, 오버로딩 |
| 2.2 | 가변인자 | 타입... 변수명, 배열로 변환 |
| 2.3 | 상속과 생성자 체이닝 | extends, super(), 단일 상속 |
| 2.4 | 다형성 ★★★ | VMT, 동적 바인딩, OCP의 토대 |
| 2.5 | instanceof와 형변환 | 안전한 다운캐스팅, 패턴 매칭 |
| 2.6 | Nested/Inner/Anonymous | 람다의 전신, 메모리 누수 주의 |
"클래스를 만들 줄 안다 ≠ OOP를 안다"
메서드, 상속, 다형성, 형변환, 내부 클래스 — 이 모두가 객체들이 협력하는 방식 을 표현하는 도구. 이 도구들을 잘 조합해야 SOLID 원칙 (Phase 3) 으로 좋은 설계가 가능하다.
Phase 2의 도구들을 올바르게 사용하는 5가지 원칙:
→ Phase 2의 OOP 3대 축이 SOLID로 꽃핀다.