🎯1주차 Unit 2.6 — Nested / Inner / Anonymous 클래스

Psj·2026년 5월 7일

F-lab

목록 보기
28/230

🎯 Unit 2.6 — Nested / Inner / Anonymous 클래스

F-lab Java 1주차 / Phase 2 / Unit 2.6 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.

선수 지식: Unit 2.4 (다형성)
다음 Unit: Phase 3 — SOLID 5원칙

이 Unit의 의미: Phase 2의 마무리 + 람다(3주차)의 전신.
클래스 안의 클래스라는 개념을 이해해야 Spring 코드, 콜백 패턴, 이벤트 리스너를 자연스럽게 다룰 수 있다.


🌍 1. 세상 속 비유

Nested 클래스 = "집 안의 작은 방"

큰 집을 상상해보세요. 집 안에는 다양한 방이 있습니다:

  • 거실 — 모두가 사용하는 공간
  • 부엌 — 요리 도구들이 모인 공간
  • 서재 — 책과 책상이 있는 공간

각 방은:

  • 집의 일부 (집과 분리될 수 없음)
  • 집 안의 다른 공간 활용 가능 (거실에서 부엌으로 음식 가져옴)
  • 밖에서는 잘 안 보임 (집 안에서만 의미)

Nested 클래스도 마찬가지 — 외부 클래스 안에서만 의미 있는 작은 공간.


Anonymous 클래스 = "임시 메모지"

회의 중 잠깐 메모할 일이 생겼습니다:

  • 정식 노트를 꺼내기엔 번거로움
  • 포스트잇 한 장 에 쓱 적음
  • 회의 끝나면 버림 (또는 그 자리에서만 사용)

특징:

  • 이름 없음 (포스트잇은 "이름 붙은 노트"가 아님)
  • 즉석에서 만들어짐
  • 한 번만 사용

Anonymous 클래스 — 이름 없이 즉석에서 정의하는 일회용 클래스.


더 일상적인 비유 — "이벤트 리스너"

스마트폰 알람 앱에서:

"이 알람이 울리면 이 동작 을 해줘"

알람: 7시 30분 → ?
   ↓
당신: 그때만의 동작 정의
   "음악 끄고 + 커튼 열고 + 커피 머신 켜기"

이 동작은:

  • 그 알람만을 위한 코드
  • 재사용 안 함
  • 이름 붙일 필요 없음

익명 클래스의 정신. 자바에서는 클릭 리스너, 콜백 등에서 자주 사용.


🔥 2. 탄생 배경

왜 클래스 안에 클래스가 필요한가?

자바 초기 (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() { ... }
}

문제:

  • MyListIteratorMyList 만을 위한 클래스
  • 그런데 별도 파일에 있음 → 관계가 흐려짐
  • MyList 의 내부 정보 접근 어려움 (private 못 봄)
  • 다른 곳에서 MyListIterator 잘못 사용할 위험

Nested 클래스의 도입 — Java 1.1 (1997)

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

효과:

  • MyListIteratorMyList 의 일부임을 명확히
  • 외부 클래스의 private 멤버 접근 가능
  • 다른 곳에서 잘못 쓸 수 없음 (캡슐화)
  • 한 파일에 함께 → 관리 쉬움

익명 클래스의 등장 — 콜백의 시대

문제: 한 번만 쓸 클래스를 매번 정의하기 번거로움

// 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 프로그래밍, 이벤트 처리, 콜백 패턴 의 폭발적 활용.


Java 8의 람다 (2014) — 익명 클래스의 진화

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

람다의 본질:

  • 익명 클래스를 더 간결하게
  • 함수형 인터페이스 (메서드 1개) 한정
  • → 3주차에서 본격 학습

Nested/Inner/Anonymous는 람다의 전신. 진화의 역사를 알아야 람다를 정확히 이해.


핵심 통찰

"클래스 안의 클래스는 '강한 관계' 를 표현하는 도구다."

외부 클래스와 떼려야 뗄 수 없는 작은 클래스를 만들 때, 별도 파일이 아닌 안에 두면 관계가 명확 해지고 캡슐화가 강해진다.

익명 클래스는 더 나아가 이름조차 필요 없는 일회용 객체 를 즉석에서 만드는 도구. 이 사고방식이 람다로 진화.


💣 3. 없으면 생기는 문제

Nested 클래스가 없다면

시나리오: ILIC 운임 시스템의 페이지네이션 결과

// ❌ 별도 파일로 분리한다면
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 는 페이지네이션에서만 쓰는데 공용 위치에 있음
  • 다른 영역에서 잘못 사용할 위험
  • 파일 폭발 (관련 클래스마다 별도 파일)

해결 — Nested 클래스

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

효과:

  • PageRequestFareService 와 관련됨이 명확
  • 다른 곳에서 잘못 사용 어려움
  • 한 파일에 모든 관련 코드

익명 클래스가 없다면 — 콜백 지옥

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

효과:

  • 그 자리에 정의 + 사용
  • 흐름이 한눈에 보임
  • 별도 파일 불필요

람다로 더 간결하게 (Java 8+)

public class FareService {
    public void processInTransaction() {
        transactionTemplate.execute(status -> {
            // 트랜잭션 안에서 할 일
            return null;
        });
    }
}

익명 클래스 → 람다로의 진화. 같은 일, 짧은 표현.


✅ 4. 해결책 — 4가지 종류와 문법

자바의 클래스 종류 ⭐

1. Top-Level Class — 일반 클래스 (별도 파일)

2. Nested Class — 클래스 안의 클래스
   ├── Static Nested Class — static 키워드 있음
   └── Inner Class — static 없음 (= 내부 클래스)
        ├── Member Inner — 클래스 본문에 정의
        ├── Local Inner — 메서드 안에 정의
        └── Anonymous Inner — 이름 없는 즉석 정의

용어 정리:

  • Nested Class = 큰 우산 (모든 안의 클래스)
  • Inner Class = static 없는 Nested
  • Anonymous Class = 이름 없는 Inner

1. Static Nested Class

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

특징 ⭐ :

  • 외부 클래스의 인스턴스 없이 생성 가능
  • 외부 클래스의 static 멤버만 접근
  • 가장 단순하고 안전
  • Builder 패턴, DTO 등에 자주 사용

언제 사용:

  • 외부 클래스와 강한 관계가 있지만 외부 인스턴스에 접근할 필요 X
  • 외부 클래스의 보조 클래스 역할

2. Inner Class — Member Inner

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

특징 ⭐ :

  • 외부 클래스 인스턴스 필요 (생성 시 점)
  • 외부 클래스의 인스턴스 멤버 접근 가능
  • 숨겨진 외부 참조 보유 (메모리 누수 위험)

언제 사용:

  • Iterator 같은 자료구조의 보조 객체
  • 외부 클래스의 상태에 강하게 의존

3. Local Inner Class — 메서드 안의 클래스

public class Outer {
    public void doSomething() {
        // 메서드 안에서만 의미 있는 클래스
        class Helper {
            public void help() {
                System.out.println("도움");
            }
        }
        
        Helper h = new Helper();
        h.help();
    }
}

특징:

  • 메서드 내부에서만 사용
  • 외부 메서드의 지역 변수 접근 (final 또는 effectively final만)

언제 사용:

  • 메서드 안에서만 필요한 도우미
  • 거의 안 씀 (현대에는 람다로 대체)

4. Anonymous Inner Class — 익명 클래스 ⭐

이름 없이 즉석에서 정의 + 인스턴스 생성:

public class FareService {
    public void process() {
        // 한 줄짜리 콜백
        Runnable task = new Runnable() {  // ← 익명 클래스
            @Override
            public void run() {
                System.out.println("작업 실행");
            }
        };
        
        task.run();
    }
}

문법 ⭐ :

new 인터페이스또는클래스() {
    // 메서드 구현
};

특징:

  • 이름 없음
  • 한 번만 사용 (변수에 담거나 즉시 전달)
  • 인터페이스 구현 또는 클래스 상속

익명 클래스의 두 가지 형태

형태 1: 인터페이스 구현

Comparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
};

형태 2: 클래스 상속

Thread thread = new Thread() {
    @Override
    public void run() {
        System.out.println("스레드 실행");
    }
};
thread.start();

4가지 비교 매트릭스 ⭐

Static NestedMember InnerLocal InnerAnonymous
위치클래스 본문클래스 본문메서드 안즉석
이름OOOX
static 키워드OXXX
외부 인스턴스 필요XO(메서드 호출 시)(메서드 호출 시)
외부 멤버 접근static만모두모두 + 지역변수모두 + 지역변수
사용 빈도높음중간매우 낮음높음 (람다로 대체 중)

🏗️ 5. 내부 동작 원리

컴파일 결과 — 별도 .class 파일 생성 ⭐

자바 컴파일러는 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의 숨은 참조 ⚠️

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);  // 외부 참조로 접근
    }
}

중요한 함의 ⭐⭐ :

  • Inner 인스턴스는 외부 인스턴스에 강하게 의존
  • Inner 가 살아있으면 외부도 GC 안 됨 → 메모리 누수 위험
  • 이런 이유로 Static Nested 권장 (외부 참조 없음)

Anonymous 클래스의 변환

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 Inner와 effectively final ⭐

Local/Anonymous 클래스가 메서드 지역변수 를 사용할 때:

public void process() {
    int count = 10;  // 지역변수
    
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println(count);  // ✅ 사용 가능
            // count = 20;  // ❌ 컴파일 에러
        }
    };
}

규칙 ⭐ :

  • 지역변수는 final 또는 effectively final (사실상 final) 이어야 함
  • = 변경 불가

왜?:

  • Inner 클래스는 지역변수를 자기 안에 복사 (외부 메서드 종료 후에도 살아있을 수 있어서)
  • 원본과 복사본이 다르면 혼란 → 변경 금지로 일관성 유지

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개
}

람다의 한계:

  • 메서드 1개인 인터페이스만
  • 메서드 여러 개나 클래스 상속은 익명 클래스 필요

→ 3주차에서 본격 학습.


💻 6. 실전 코드 예시

예시 1: Static Nested Class — Builder 패턴

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

효과:

  • BuilderFare 와 강하게 연관됨이 명확
  • 외부에서 잘못 사용 불가
  • 깔끔한 API

→ Lombok의 @Builder 도 내부적으로 이 패턴.


예시 2: Static Nested Class — DTO/응답 래퍼

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

효과:

  • Request/Response가 컨트롤러와 강하게 연결
  • 다른 컨트롤러에서 잘못 사용 X
  • 한 파일에서 흐름 파악 가능

예시 3: Inner Class — Iterator 패턴

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 가능
    // ...
}

효과:

  • FareIteratorFareCollection 의 내부에 강하게 의존
  • 외부 클래스의 private 멤버 자유롭게 접근
  • 다른 곳에서 사용 불가 (private)

→ ArrayList, LinkedList 등 자바 컬렉션이 정확히 이 패턴.


예시 4: Anonymous Class — Spring 콜백

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

콜백은 익명 클래스의 대표 사용처. 람다로 진화.


예시 5: Anonymous Class — 이벤트 리스너

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

예시 6: Anonymous Class — Comparator

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

익명 클래스 → 람다 → 메서드 참조 의 진화 흐름.


예시 7: ILIC 도메인의 Nested 활용

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

⚠️ 7. 주의사항 & 흔한 실수

실수 1: 무분별한 Inner Class 사용 — 메모리 누수 ⚠️

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) 강력 권장.


실수 2: 익명 클래스에서 this 헷갈림

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 = 외부 클래스 인스턴스

실수 3: Local Inner의 지역변수 변경 시도

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

→ 약간의 우회. 진짜 권장은 상태를 외부에서 관리.


실수 4: 익명 클래스를 람다로 바꿀 수 있는 걸 모름

// ❌ 자바 8+ 환경에서 너무 장황
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        doSomething();
    }
});

// ✅ 람다로 간결하게
button.addActionListener(e -> doSomething());

조건 ⭐ :

  • 인터페이스가 함수형 인터페이스 (메서드 1개)
  • 람다가 가능

실수 5: 너무 깊은 중첩

// ❌ 가독성 지옥
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 로 해결.


실수 6: Static 키워드 빠뜨림

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"


실수 7: Anonymous Class에서 새 메서드 추가 시도

Runnable r = new Runnable() {
    @Override
    public void run() { ... }
    
    public void newMethod() {  // ⚠️ 정의는 가능
        // ...
    }
};

r.newMethod();  // ❌ 컴파일 에러 — Runnable에 없음

→ 익명 클래스에 새 메서드 추가해도 외부에서 호출 불가 (인터페이스 타입으로만 다룸).

→ 새 메서드가 필요하면 이름 있는 클래스 사용.


🔗 8. 연관 개념 맵

직접 이어지는 학습

[Phase 2 완료]
        ↓
[Phase 3: SOLID 5원칙] — 객체 설계의 황금률
        ↓
[Phase 4: JVM 메모리 모델]
        ↓
... 계속

람다로의 진화 (3주차)

[Anonymous Class (Java 1.1)]
        ↓
[Lambda (Java 8)]
        ↓
[Method Reference (Java 8)]
        ↓
[Stream API (Java 8)]

→ 3주차에서 본격 학습.


이 Unit의 개념이 활용되는 곳

1주차 내:

  • Phase 6 (컬렉션): Iterator는 Inner Class 패턴

미래 주차:

  • 3주차 (람다): 익명 클래스의 진화 형태
  • 5주차 (Spring): ApplicationListener, Configuration 콜백 등
  • 8-9주차 (AOP): Pointcut, Advice 정의 시 익명 클래스/람다
  • 15주차 (Spring MVC): 다양한 콜백 패턴

Static Nested vs Inner — 의사결정

"외부 클래스의 인스턴스 멤버에 접근해야 하나?"
        ↓
   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?"지역변수는 변경 불가여야 사용 가능

📝 9. 핵심 요약 — 3줄 정리

1️⃣ Nested 클래스는 "강한 관계" 를 표현하는 도구다.

외부 클래스와 떼려야 뗄 수 없는 작은 클래스를 안에 두면 관계가 명확해지고 캡슐화가 강해진다. Static Nested (외부 참조 없음) 와 Inner (외부 참조 보유) 로 나뉘는데, 메모리 누수 방지를 위해 가능하면 Static Nested 권장.

2️⃣ 익명 클래스는 "이름 없는 일회용 객체" 를 즉석에서 만든다.

한 번만 쓸 콜백이나 리스너에 적합. 컴파일러가 Outer$1.class 같은 별도 .class 파일로 변환. 함수형 인터페이스인 경우 Java 8+ 람다로 더 간결하게 표현 가능 — 익명 클래스가 람다의 전신이다.

3️⃣ Local Inner의 지역변수는 effectively final 이어야 한다.

메서드 안의 클래스가 외부 메서드의 지역변수를 사용하려면 그 변수는 final 또는 사실상 final (재할당 안 됨). 컴파일러가 지역변수를 클래스 안에 복사하기 때문에, 원본과 복사본의 일관성을 위한 강제. 이 규칙은 람다에도 그대로 적용된다.


🎓 학습 자기 점검

기본 이해

  • 4가지 종류 (Static Nested / Member Inner / Local Inner / Anonymous) 를 구별할 수 있다
  • Static Nested와 Inner의 결정적 차이를 안다 (외부 참조)
  • 익명 클래스의 문법을 작성할 수 있다
  • effectively final의 의미를 안다

실전 적용

  • ILIC 코드에서 Builder/DTO를 Static Nested로 만들 수 있다
  • 익명 클래스를 람다로 변환할 수 있다 (함수형 인터페이스인 경우)
  • Inner Class 사용 시 메모리 누수 위험을 평가할 수 있다

면접 대비 (1-2분 답변)

  • "왜 클래스 안에 클래스를?" 답변 가능
  • "Static Nested와 Inner의 차이?" 답변 가능
  • "익명 클래스와 람다의 관계?" 답변 가능
  • "Inner Class의 메모리 누수 위험?" 답변 가능

자기 점검 질문 답변

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)

불가능한 경우:

1. 메서드 2개 이상 인터페이스

public interface MultiInterface {
    void methodA();
    void methodB();
}

// 익명 클래스만 가능 — 람다 X
MultiInterface m = new MultiInterface() {
    @Override public void methodA() { ... }
    @Override public void methodB() { ... }
};

2. 클래스 상속 (인터페이스가 아닌)

// 익명 클래스로 클래스 상속
Thread t = new Thread() {
    @Override
    public void run() { ... }
};

// 람다는 인터페이스 한정 — 클래스 상속 X
// 위 코드를 람다로? 가능 — Thread는 Runnable을 받음
Thread t = new Thread(() -> { ... });

3. 추가 필드/상태가 필요한 경우

Runnable r = new Runnable() {
    private int count = 0;  // 상태 보유
    
    @Override
    public void run() {
        count++;
        System.out.println(count);
    }
};
// 람다는 상태 보유 X — 외부 변수 활용해야

람다의 한계 정리:

  • ✅ 함수형 인터페이스 (메서드 1개)
  • ❌ 클래스 상속
  • ❌ 메서드 여러 개
  • ❌ 추가 필드/상태

간단한 콜백은 람다, 복잡한 객체는 익명 클래스 또는 명명 클래스.


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

핵심 메커니즘 ⭐ :

  1. 컴파일러가 외부 인스턴스 참조 주입

    • Inner 인스턴스 생성 시 자동으로 외부 인스턴스 받음
    • this$0 라는 숨겨진 필드에 저장
  2. 합성 메서드 (Synthetic Method) 자동 생성

    • private 필드 접근을 위해 컴파일러가 자동 생성
    • access$000, access$100 같은 이름
  3. 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 살아있으면 외부도 GC 안 됨 → 메모리 누수
  • 직렬화 시 외부 객체까지 직렬화 → 예상치 못한 동작

결론:

"외부 인스턴스 멤버에 접근해야 한다면 Inner Class,
그렇지 않다면 Static Nested Class를 사용하라" — Joshua Bloch (Effective Java)


Phase 2 완료 — 다음으로

  • Phase 2 졸업 — OOP 3대 축 완성!
  • Phase 3 (SOLID 원칙) 학습 준비 완료
  • 객체 설계의 황금률을 만날 준비

🎓 Phase 2 전체 학습 정리

학습한 6개 Unit

Unit주제핵심
2.1메서드의 구조시그니처, 접근 제어자, 오버로딩
2.2가변인자타입... 변수명, 배열로 변환
2.3상속과 생성자 체이닝extends, super(), 단일 상속
2.4다형성 ★★★VMT, 동적 바인딩, OCP의 토대
2.5instanceof와 형변환안전한 다운캐스팅, 패턴 매칭
2.6Nested/Inner/Anonymous람다의 전신, 메모리 누수 주의

Phase 2의 핵심 통찰

"클래스를 만들 줄 안다 ≠ OOP를 안다"

메서드, 상속, 다형성, 형변환, 내부 클래스 — 이 모두가 객체들이 협력하는 방식 을 표현하는 도구. 이 도구들을 잘 조합해야 SOLID 원칙 (Phase 3) 으로 좋은 설계가 가능하다.

Phase 3 미리보기 — SOLID 원칙

Phase 2의 도구들을 올바르게 사용하는 5가지 원칙:

  • SRP — 단일 책임
  • OCP — 개방-폐쇄 (다형성의 결과 ⭐)
  • LSP — 리스코프 치환 (상속의 진짜 규칙)
  • ISP — 인터페이스 분리
  • DIP — 의존 역전 (다형성의 응용)

→ Phase 2의 OOP 3대 축이 SOLID로 꽃핀다.

profile
Software Developer

0개의 댓글