내부 클래스

이언덕·2026년 4월 18일

아이티센 부트캠프

목록 보기
56/115
post-thumbnail

내부 클래스란 무엇인가

내부 클래스는 클래스 안에 다시 정의하는 클래스다.
쉽게 말하면, 바깥 클래스 안에서 함께 쓰는 기능을 따로 묶어 두기 위해 클래스 안에 또 하나의 클래스를 넣는 구조다.


처음 보면 “클래스 하나만 만들면 되지, 왜 안에 또 클래스를 넣지?”라는 생각이 들 수 있다.
하지만 모든 기능이 클래스 바깥으로 따로 빠져야 하는 것은 아니다.
어떤 기능은 바깥 클래스와 아주 가깝게 연결되어 있어서, 안쪽에 함께 두는 편이 더 자연스럽다.


예를 들어 바깥 클래스의 필드나 메서드 흐름과 바로 연결되는 기능이 있다고 하자.
이런 기능까지 전부 바깥으로 꺼내 버리면, 이 기능이 누구와 연결된 것인지 한눈에 들어오지 않을 수 있다.
반대로 내부 클래스로 두면 “이 기능은 이 클래스 안에서 같이 쓰는 기능이구나”를 바로 파악하기 쉽다.


즉, 내부 클래스는 단순히 문법 모양이 특이한 클래스가 아니다.
바깥 클래스와 밀접하게 연결된 기능을 안쪽에 묶어 두는 구조라는 점이 핵심이다.


내부 클래스의 뜻

내부 클래스는 말 그대로 바깥 클래스 안에서 정의되는 클래스다.
그런데 여기서 중요한 것은 “안에 있다”는 위치 설명만이 아니다.
왜 안에 두었는지도 같이 봐야 한다.


내부 클래스는 보통 바깥 클래스와 함께 이해해야 하는 기능을 담는다.
즉, 바깥 클래스와 거의 같이 움직이는 역할을 안쪽에 정리해 둔 구조라고 보면 된다.
이 점이 완전히 독립적으로 따로 쓰는 일반 클래스와의 차이다.

먼저 이렇게 이해하면 된다

  • 내부 클래스는 바깥 클래스 안에 정의된 클래스다.
  • 바깥 클래스와 함께 쓰는 기능을 묶어 둔 구조다.
  • 완전히 독립적으로 돌아가는 클래스와는 느낌이 다르다.

그래서 내부 클래스를 볼 때는 “클래스 안에 있네”에서 끝내면 안 된다.
왜 바깥 클래스 안에 두었는지까지 같이 이해해야 한다.


또 내부 클래스는 바깥 클래스의 멤버를 사용할 수 있다.
이 점 때문에 바깥 클래스와 가까운 작업을 만들 때 잘 어울린다.
즉, 안쪽에 두는 이유와 실제 사용 방식이 자연스럽게 이어진다.


왜 클래스 안에 또 클래스를 넣는가

내부 클래스를 쓰는 가장 큰 이유는 관련 있는 기능을 가까운 곳에 두기 위해서다.
서로 연결된 기능인데 완전히 바깥으로 분리해 버리면 구조가 퍼져 보일 수 있다.
반대로 바깥 클래스 안에 함께 두면, 어떤 기능이 누구와 연결된 것인지 더 쉽게 보인다.


쉽게 말하면 정리의 문제다.
바깥 클래스와 거의 같이 움직이는 기능이라면, 바깥에 따로 떼어 두는 것보다 안쪽에 두는 편이 더 자연스러울 수 있다.
그래서 코드를 읽을 때도 흐름을 따라가기 쉬워진다.

이런 경우에 내부 클래스를 떠올리면 된다

  • 바깥 클래스와 아주 밀접한 기능일 때
  • 클래스 바깥에 따로 공개할 필요가 없을 때
  • 관련된 코드를 가까운 곳에 두고 싶을 때

여기서 캡슐화라는 말도 연결된다.
이 단계에서는 캡슐화를 “관련 있는 기능을 안쪽에 묶어 두는 방식” 정도로 이해하면 충분하다.
즉, 내부 클래스는 관련된 기능이 바깥으로 불필요하게 퍼지지 않도록 안쪽에 모아 두는 한 가지 방법이다.

여기서 오해하지 말아야 할 점

  • 내부 클래스가 무조건 더 좋은 구조라는 뜻은 아니다.
  • 모든 클래스를 안쪽에 넣는 것이 좋은 것도 아니다.
  • 바깥 클래스와 강하게 연결된 경우에 자연스럽게 쓰는 구조다.

즉, 내부 클래스는 “무조건 써야 하는 문법”이 아니라, “안쪽에 두는 편이 더 잘 어울리는 상황”에서 쓰는 구조라고 보면 된다.


내부 클래스를 왜 따로 나누어 배우는가

내부 클래스는 전부 비슷해 보이지만 실제로는 성격이 다르다.
초보자가 이 단원에서 많이 헷갈리는 이유도 여기에 있다.
겉으로는 전부 클래스 안에 있거나, 메서드 안에 있거나, 이름 없이 갑자기 등장하는 형태라서 비슷해 보이기 쉽다.


하지만 실제로는 정의 위치와 사용 범위가 다르다.
즉, 내부 클래스 단원은 종류 이름을 외우는 단원이 아니라, 어디에 정의되었는지에 따라 성격이 어떻게 달라지는지 읽는 단원이다.

먼저 잡아야 하는 기준

  • 어디에 정의되었는가
  • 어디서 사용할 수 있는가
  • 바깥 객체가 필요한가
  • 이름이 있는가 없는가

이 네 가지 기준을 잡으면 멤버 클래스, 로컬 클래스, 익명 클래스가 왜 나뉘는지 훨씬 편하게 이해할 수 있다.


즉, 이 단원은 문법 모양부터 외우면 어렵다.
먼저 종류를 나누는 기준을 이해하고, 그다음 예제에서 “그래서 이 코드가 이렇게 생겼구나”로 연결해야 한다.


내부 클래스의 종류를 먼저 크게 보기

내부 클래스는 먼저 멤버 클래스와 로컬 클래스로 나눌 수 있다.
그리고 로컬 클래스 쪽에는 이름이 없는 특별한 형태인 익명 클래스가 있다.
즉, 이번 정리에서는 사실상 세 갈래로 이해하면 된다.

가장 먼저 이렇게 구분하면 된다

  • 멤버 클래스
    • 클래스 안에 있지만 메서드 바깥에 정의된다.

  • 로컬 클래스
    • 메서드 안에 정의된다.

  • 익명 클래스
    • 이름 없이 바로 정의하고 바로 객체를 만든다.

즉, 내부 클래스는 전부 같은 방식으로 쓰는 것이 아니다.
어디에 정의했는지에 따라 역할과 범위가 달라진다.

멤버 클래스는 클래스 내부에, 로컬 클래스는 메서드 내부에 정의된다.


이 그림에서 가장 먼저 봐야 하는 것은 정의 위치다.
멤버 클래스는 클래스의 구성 요소처럼 보이고, 로컬 클래스는 메서드 안에서만 잠깐 쓰는 구조처럼 보인다.
즉, 어디에 만들어졌는지가 곧 그 클래스의 성격을 결정한다고 이해하면 된다.


여기서 멤버 클래스는 다시 인스턴스 클래스와 스태틱 클래스로 나뉜다.
반면 로컬 클래스 쪽에서는 이름이 있는 일반 로컬 클래스와 이름이 없는 익명 클래스가 나온다.
이제부터는 이 큰 틀을 기준으로 하나씩 자세히 보면 된다.


여기까지 먼저 정리

내부 클래스는 클래스 안에 다시 정의하는 클래스다.
하지만 단순히 안에 넣었다는 사실보다, 바깥 클래스와 함께 움직이는 기능을 안쪽에 묶었다는 점이 더 중요하다.

지금 단계에서 꼭 기억할 것

  • 내부 클래스는 바깥 클래스와 연결된 기능을 안쪽에 둔 구조다.
  • 종류는 정의 위치에 따라 나뉜다.
  • 먼저 위치와 범위를 이해해야 예제 문법이 덜 헷갈린다.

즉, 이 첫 주제에서는 세부 문법보다 “왜 안쪽에 두는가”와 “어떻게 종류가 갈리는가”를 먼저 잡는 것이 목표다.
이 기준이 잡히면 다음의 멤버 클래스, 로컬 클래스, 익명 클래스 예제를 훨씬 편하게 읽을 수 있다.




멤버 클래스

멤버 클래스는 바깥 클래스 안에 들어 있는 내부 클래스다.
다만 메서드 안에 들어 있는 것이 아니라, 클래스 본문에 바로 들어 있는 내부 클래스라는 점이 중요하다.


쉽게 말하면 바깥 클래스의 한 구성 요소처럼 들어 있는 내부 클래스라고 보면 된다.
그래서 이름도 멤버 클래스다.
멤버 변수나 멤버 메서드처럼, 바깥 클래스에 소속된 구성 요소처럼 이해하면 훨씬 쉽다.


즉, 멤버 클래스는 메서드 안에서 잠깐 쓰고 끝나는 구조가 아니다.
바깥 클래스 안에 자리 잡고 있으면서, 바깥 클래스와 함께 이해해야 하는 내부 클래스다.


멤버 클래스는 왜 따로 구분해서 보는가

내부 클래스라고 해서 전부 같은 방식으로 만들어지는 것은 아니다.
그중 멤버 클래스는 클래스 안에 들어 있다는 점에서는 같지만, 바깥 객체와 어떤 관계를 가지느냐에 따라 다시 나뉜다.


바로 인스턴스 클래스와 스태틱 클래스다.
둘 다 바깥 클래스 안에 있다는 공통점은 있지만, 생성 방식이 다르다.
그래서 이 둘을 구분할 때는 이름보다 어떻게 만들어지는가를 먼저 보는 것이 더 중요하다.

두 종류를 나누는 가장 쉬운 기준

  • 바깥 객체가 먼저 필요하면 인스턴스 클래스
  • 바깥 객체 없이 클래스 이름으로 접근하면 스태틱 클래스

이 기준 하나만 먼저 잡아도 뒤의 코드가 훨씬 덜 낯설어진다.
즉, 둘 다 안쪽에 들어 있는 클래스이지만, 바깥 객체에 묶여 있느냐 아니냐가 핵심 차이다.


인스턴스 클래스

인스턴스 클래스는 바깥 객체가 있어야 만들 수 있는 멤버 클래스다.
쉽게 말하면 바깥 클래스의 객체 하나에 소속되어 만들어지는 내부 클래스라고 보면 된다.


그래서 이 클래스를 만들 때는 먼저 바깥 클래스의 객체를 생성해야 한다.
그다음 그 객체를 통해 내부 객체를 만든다.
이 때문에 문법도 일반 객체 생성과는 조금 다르게 보인다.

인스턴스 클래스의 핵심

  • 바깥 객체가 먼저 있어야 한다.
  • 내부 객체는 그 바깥 객체를 통해 만들어진다.
  • 바깥 객체와 연결된 상태로 이해해야 한다.

즉, 인스턴스 클래스는 혼자 따로 만들어지는 클래스가 아니라, 바깥 객체에 기대어 만들어지는 클래스다.


초보자 입장에서는 o.new Inner() 같은 문장이 가장 낯설 수 있다.
하지만 이 문장은 특별한 마법 문법이 아니다.
“바깥 객체가 있어야 한다”는 규칙이 코드로 나타난 것뿐이다.


스태틱 클래스

스태틱 클래스는 바깥 객체 없이 사용할 수 있는 멤버 클래스다.
즉, 객체 하나하나에 소속되어 움직이기보다 클래스 자체에 연결된 성격으로 이해하는 편이 자연스럽다.


그래서 바깥 객체를 먼저 만들지 않아도 된다.
외부클래스.내부클래스 형태로 바로 접근할 수 있다.
이 점이 바깥 클래스의 static 멤버와 비슷하다.

스태틱 클래스의 핵심

  • 바깥 객체가 없어도 된다.
  • 클래스 이름으로 바로 접근할 수 있다.
  • 인스턴스 클래스보다 상대적으로 독립적인 느낌이 강하다.

즉, 스태틱 클래스는 “바깥 클래스 안에 있긴 하지만, 객체를 먼저 만들 필요는 없는 구조”라고 이해하면 된다.


인스턴스 클래스와 스태틱 클래스 비교

둘 다 멤버 클래스이기 때문에 정의 위치는 비슷하다.
둘 다 바깥 클래스 안, 메서드 바깥에 정의된다.
하지만 실제 사용 방식은 꽤 다르다.

차이를 한 번에 보면

  • 인스턴스 클래스
    • 바깥 객체가 먼저 필요하다.
    • 객체를 통해 내부 객체를 만든다.
  • 스태틱 클래스
    • 바깥 객체가 필요 없다.
    • 클래스 이름으로 바로 접근한다.

즉, 두 클래스를 구분할 때 가장 먼저 볼 것은 “바깥 객체가 필요한가”다.
이 기준이 잡히면 생성 문법이 왜 달라지는지도 자연스럽게 이해된다.

인스턴스 클래스와 스태틱 클래스는 정의 위치는 같아도 사용 방식과 의미가 다르다.


그림의 class B는 인스턴스 클래스다.
그래서 A$B.class가 만들어진다.
static class C는 스태틱 클래스다.
그래서 A$C.class가 만들어진다.


즉, 둘 다 바깥 클래스 A 안에 있지만 static 유무에 따라 바깥 객체와의 관계가 달라진다.
이 차이가 실제 코드에서는 생성 방식 차이로 나타난다.


예제로 확인하기 - InnerTest1

이제 InnerTest1을 보면 방금 설명한 개념이 코드에 그대로 나온다.
이 예제의 핵심은 Outer 안에 일반 내부 클래스 Inner와 static 내부 클래스 Static_Inner가 함께 있다는 점이다.
즉, 멤버 클래스의 두 종류를 한 번에 비교할 수 있는 예제다.

먼저 코드 구조부터 보기

  • Outer
    • 바깥 클래스다.

  • Inner
    • 인스턴스 클래스다.

  • Static_Inner
    • 스태틱 클래스다.

이 구조를 먼저 알고 보면 각 줄이 왜 그렇게 쓰였는지 바로 연결된다.

// InnerTest1.java
package extraexam;

class Outer {
    void pr() {
        System.out.println("Outer's pr() Method !!"); // 바깥 클래스 메서드
    }

    class Inner {
        void pr() {
            System.out.println("Inner's pr() Method !!"); // 인스턴스 내부 클래스 메서드
        }
    }

    static class Static_Inner {
        static void pr() {
            System.out.println("Static_Inner's pr() Method !!"); // 스태틱 내부 클래스 메서드
        }
    }
}

public class InnerTest1 {
    public static void main(String args[]) {
        Outer.Static_Inner.pr(); // 바깥 객체 없이 호출
        Outer o = new Outer(); // 바깥 객체 생성
        o.pr();
        Outer.Inner i = o.new Inner(); // 바깥 객체를 통해 내부 객체 생성
        i.pr();
    }
}
// 출력결과
// Static_Inner's pr() Method !!
// Outer's pr() Method !!
// Inner's pr() Method !!

이 예제에서 꼭 짚어야 하는 줄

  • Outer.Static_Inner.pr();
    • 스태틱 클래스는 바깥 객체 없이 접근할 수 있다는 뜻이다.

  • Outer o = new Outer();
    • 인스턴스 클래스를 만들기 전에 바깥 객체를 준비하는 단계다.

  • Outer.Inner i = o.new Inner();
    • 인스턴스 클래스는 바깥 객체를 통해 만들어진다는 뜻이다.

즉, 앞에서 설명한 차이가 코드에서는 생성 문법 차이로 그대로 드러난다.


여기서 가장 중요한 연결은 이것이다.
앞에서 인스턴스 클래스는 바깥 객체가 필요하다고 했다.
그 설명이 코드에서는 Outer o = new Outer();와 o.new Inner()로 나타난다.


반대로 앞에서 스태틱 클래스는 바깥 객체 없이 사용할 수 있다고 했다.
그 설명이 코드에서는 Outer.Static_Inner.pr();로 나타난다.
즉, 이 예제는 새로운 개념을 더 추가하는 것이 아니라, 앞에서 설명한 개념을 코드로 확인시키는 역할을 한다.


초보자 입장에서는 Outer.Inner i = o.new Inner();가 가장 눈에 걸릴 수 있다.
하지만 이 문장을 따로 외우려고 하지 말고, “바깥 객체가 있어야 하는 내부 클래스라서 이렇게 쓴다”라고 이해하면 훨씬 쉽다.
문법이 먼저가 아니라 규칙이 먼저다.


출력 결과까지 연결해서 보기

출력 결과도 개념과 연결해서 보면 더 잘 보인다.

출력 흐름 해석

  • Static_Inner's pr() Method !!
    • 스태틱 클래스의 메서드를 먼저 호출한 결과다.

  • Outer's pr() Method !!
    • 바깥 객체 o의 메서드를 호출한 결과다.

  • Inner's pr() Method !!
    • o.new Inner()로 만든 인스턴스 클래스 객체의 메서드를 호출한 결과다.

즉, 출력 순서만 봐도 스태틱 클래스는 바로 접근되고, 인스턴스 클래스는 바깥 객체를 만든 뒤에 사용된다는 흐름이 드러난다.


멤버 클래스에서 꼭 기억할 점

멤버 클래스는 바깥 클래스 안, 메서드 바깥에 정의되는 내부 클래스다.
그래서 클래스의 구성 요소처럼 다뤄진다.


그리고 멤버 클래스는 다시 인스턴스 클래스와 스태틱 클래스로 나뉜다.
이 둘은 정의 위치가 비슷해 보여도 생성 기준이 다르다.

마지막으로 짧게 정리하면

  • 클래스 안, 메서드 바깥에 있으면 멤버 클래스
  • 바깥 객체가 필요하면 인스턴스 클래스
  • 클래스 이름으로 바로 접근하면 스태틱 클래스

즉, 멤버 클래스 파트에서는 종류 이름보다 바깥 객체가 필요한지 아닌지를 기준으로 보는 것이 가장 중요하다.
이 기준이 잡히면 InnerTest1의 코드도 훨씬 자연스럽게 읽힌다.




로컬 클래스

로컬 클래스는 메서드 안에서 정의하는 내부 클래스다.
쉽게 말하면 클래스 전체에서 계속 쓰려고 만드는 구조가 아니라, 특정 메서드 안에서만 잠깐 사용하는 지역용 클래스라고 이해하면 된다.


여기서 local은 지역이라는 뜻이다.
즉, 사용 범위가 넓은 클래스가 아니라, 메서드 안이라는 좁은 범위에서만 쓰는 클래스라는 의미가 들어 있다.


그래서 로컬 클래스는 바깥 클래스의 한 구성 요소처럼 계속 자리 잡고 있는 구조가 아니다.
특정 메서드의 흐름을 돕기 위해 안쪽에 잠깐 만들어 두는 보조 구조에 가깝다.
이 점이 앞에서 본 멤버 클래스와 가장 큰 차이다.


로컬 클래스는 왜 필요한가

처음 보면 “메서드 안에 굳이 클래스를 또 만들 필요가 있나?”라는 생각이 들 수 있다.
하지만 어떤 기능은 클래스 전체에서 공통으로 쓸 정도는 아니고, 특정 메서드 안에서만 잠깐 필요할 수 있다.
이럴 때 그 메서드 안에서만 쓸 보조 기능을 하나로 묶어 두면 흐름이 더 깔끔해진다.


즉, 로컬 클래스는 클래스 전체를 위한 구조라기보다, 특정 메서드의 작업을 정리하기 위한 작은 묶음이라고 보면 된다.
굳이 바깥까지 공개할 필요가 없는 기능을 안쪽에서만 다루고 싶을 때 잘 어울린다.

이런 경우에 로컬 클래스를 떠올리면 된다

  • 특정 메서드 안에서만 필요한 보조 기능이 있을 때
  • 클래스 전체에서 공통으로 쓸 구조는 아닐 때
  • 메서드 안의 값과 바깥 클래스 필드를 함께 써야 할 때

즉, 로컬 클래스는 범위가 짧고 목적이 분명한 구조다.


로컬 클래스의 가장 중요한 특징

로컬 클래스를 이해할 때 가장 중요한 기준은 두 가지다.
하나는 어디서 만들었는가, 다른 하나는 어디까지 사용할 수 있는가다.


로컬 클래스는 메서드 안에서 정의되므로, 사용할 수 있는 범위도 그 메서드 안으로 제한된다.
즉, 클래스 안에 들어 있긴 하지만 아무 데서나 사용할 수는 없다.
정의된 메서드 안에서만 생성하고, 그 안에서만 사용할 수 있다.

꼭 먼저 잡아야 하는 기준

  • 정의 위치 : 메서드 안
  • 사용 범위 : 그 메서드 안
  • 성격 : 메서드 내부용 보조 클래스

이 기준이 먼저 잡혀야 멤버 클래스와 섞이지 않는다.
멤버 클래스는 클래스 안에 자리 잡고 있지만, 로컬 클래스는 메서드 안에서만 잠깐 쓰인다.


그런데 여기서 한 가지 더 중요한 점이 나온다.
로컬 클래스는 활동하는 위치는 좁지만, 참조할 수 있는 값은 생각보다 다양하다.
바깥 클래스의 필드도 볼 수 있고, 메서드 안의 값도 함께 사용할 수 있다.

이 파트에서 꼭 기억할 한 줄

  • 참조할 수 있는 값은 넓다.
  • 하지만 활동할 수 있는 위치는 메서드 안으로 좁다.

이 대비가 로컬 클래스의 핵심이다.

로컬 클래스는 메서드 안에서만 사용할 수 있고, 바깥 클래스의 필드와 메서드 내부 값을 함께 참조할 수 있다.


그림의 class Y는 sam() 메서드 안에 정의되어 있으므로 로컬 클래스다.
그래서 sam() 메서드 안에서만 사용할 수 있다.
또 예시처럼 바깥 클래스의 필드와 메서드 안의 값도 함께 사용할 수 있다.


즉, 이 그림은 로컬 클래스가 사용 위치는 좁지만, 필요한 값은 바깥쪽에서도 가져와 쓸 수 있다는 점을 보여 준다.


예제로 확인하기 - InnerTest2

InnerTest2는 로컬 클래스의 특징을 아주 잘 보여 주는 예제다.
바깥 클래스 LocalTest에는 필드 name이 있다.
메서드 pr()에는 매개변수 s와 메서드 안의 값 su가 있다.
그리고 그 안에 Local이라는 로컬 클래스가 정의된다.


이 예제의 핵심은 Local 클래스 안에서 여러 위치의 값을 함께 사용한다는 점이다.
즉, 로컬 클래스는 메서드 안에 정의되지만, 바깥 클래스와 바깥 메서드의 문맥을 함께 참조할 수 있다.

먼저 값의 자리를 구분해서 보면 쉽다

  • name
    • 바깥 클래스 LocalTest의 필드다.

  • s
    • 바깥 메서드 pr()의 매개변수다.

  • su
    • 바깥 메서드 안에서 만든 값이다.

  • ls
    • Local.pr() 메서드의 매개변수다.

이 네 값을 먼저 구분해 두고 코드를 보면 훨씬 덜 헷갈린다.

// InnerTest2.java
package extraexam;

class LocalTest {
    String name = "Java"; // 바깥 클래스 필드

    void pr(final String s) {
        final int su = 100; // 메서드 안의 값
        System.out.println(s + " : " + su); // 바깥 메서드에서 먼저 출력
        class Local {
            void pr(String ls) {
                System.out.println("s : " + s); // 바깥 메서드의 매개변수 사용
                System.out.println("ls : " + ls); // 자신의 매개변수 사용
                System.out.println(name); // 바깥 클래스 필드 사용
                System.out.println(su); // 바깥 메서드 안의 값 사용
            }
        }
        Local lt = new Local(); // 로컬 클래스 객체 생성
        lt.pr("Local Test");
    }
}

public class InnerTest2 {
    public static void main(String args[]) {
        LocalTest l = new LocalTest();
        l.pr("Main Call");
    }
}
// 출력결과
// Main Call : 100
// s : Main Call
// ls : Local Test
// Java
// 100

출력 흐름을 줄마다 해석하기

  • Main Call : 100
    • 바깥 메서드 pr() 안에서 먼저 출력한 값이다.

  • s : Main Call
    • 로컬 클래스 안에서 바깥 메서드의 매개변수 s를 사용한 결과다.

  • ls : Local Test
    • 로컬 클래스 자신의 메서드 매개변수 ls를 사용한 결과다.

  • Java
    • 바깥 클래스 필드 name을 사용한 결과다.

  • 100
    • 바깥 메서드 안의 값 su를 사용한 결과다.

이렇게 보면 값이 섞여 보이지 않는다.
각 값이 어디서 왔는지를 자리에 따라 구분해서 읽을 수 있다.


초보자가 여기서 가장 헷갈리기 쉬운 부분은 s와 ls다.
둘 다 문자열이라 비슷해 보이지만 서로 같은 값이 아니다.
s는 바깥 메서드 pr()가 받은 값이고, ls는 Local.pr()가 받은 값이다.
즉, 이름이 비슷해 보여도 위치가 다르면 다른 값이다.


또 name은 바깥 클래스 필드이고, su는 바깥 메서드 안의 값이다.
이처럼 로컬 클래스는 메서드 안에 있으면서도 바깥쪽 문맥의 값을 함께 사용할 수 있다.
하지만 이 구조 전체가 사용할 수 있는 위치는 끝까지 메서드 안이다.

이 예제가 보여 주는 핵심

  • 로컬 클래스는 메서드 안에서만 정의된다.
  • 바깥 클래스 필드와 메서드 안의 값을 함께 사용할 수 있다.
  • 사용할 수 있는 값은 다양하지만, 사용할 수 있는 위치는 메서드 안으로 제한된다.

즉, 이 예제는 앞에서 설명한 개념을 코드로 그대로 보여 주는 확인용 예제다.


멤버 클래스와 비교해서 보면 더 잘 보인다

멤버 클래스는 클래스 안, 메서드 바깥에 정의되므로 클래스의 구성 요소처럼 보인다.
반면 로컬 클래스는 메서드 안에서만 정의되고, 그 메서드 안에서만 사용된다.

차이를 짧게 비교하면

  • 멤버 클래스
    • 클래스 안에 자리 잡는다.

  • 로컬 클래스
    • 메서드 안에서 잠깐 쓰인다.

즉, 멤버 클래스는 클래스 수준의 내부 구조이고, 로컬 클래스는 메서드 수준의 내부 구조라고 이해하면 된다.


로컬 클래스에서 꼭 기억할 점

로컬 클래스는 메서드 안에서만 쓰는 내부 클래스다.
그래서 클래스 전체를 위한 구조라기보다, 특정 메서드 흐름을 돕는 보조 구조에 가깝다.


이번 예제에서는 바깥 클래스 필드 name, 바깥 메서드의 값 s, su, 그리고 자기 자신의 매개변수 ls가 한곳에서 만나고 있다.
이 흐름을 이해하면 로컬 클래스가 왜 메서드 내부용 구조인지 자연스럽게 보인다.

마지막으로 짧게 정리하면

  • 메서드 안에 정의하면 로컬 클래스
  • 그 메서드 안에서만 사용할 수 있다.
  • 바깥 클래스 필드와 메서드 안의 값도 함께 사용할 수 있다.

즉, 로컬 클래스는 위치는 좁고, 연결되는 값은 넓은 구조라고 정리하면 된다.




익명 클래스

익명 클래스는 이름이 없는 내부 클래스다.
하지만 단순히 이름만 없는 것이 아니라, 필요한 자리에서 바로 정의하고, 그 자리에서 객체까지 바로 만드는 구조라는 점이 더 중요하다.


보통은 먼저 class로 클래스를 만들고, 그다음 new로 객체를 만든다.
그런데 익명 클래스는 이 두 단계를 한 번에 처리한다.
즉, “클래스를 따로 만들어 두고 나중에 쓰는 방식”이 아니라 “지금 여기서 바로 필요한 객체를 만드는 방식”이라고 이해하면 된다.


그래서 익명 클래스는 한 번 잠깐 쓰고 끝나는 경우가 많다.
굳이 클래스 이름까지 따로 만들 필요가 없을 때, 필요한 구현을 바로 적어서 객체를 넘기기에 잘 어울린다.
즉, 익명 클래스의 핵심은 이름이 없다는 점보다, 필요한 자리에서 바로 만들고 바로 쓴다는 점이다.


익명 클래스는 왜 사용하는가

처음 보면 익명 클래스 문법은 꽤 낯설다.
그래서 “왜 굳이 이런 방식으로 쓰지?”라는 생각이 들 수 있다.


하지만 어떤 경우에는 객체가 한 번만 필요하다.
그리고 그 객체를 특정 메서드에 바로 넘기기만 하면 된다.
이럴 때 굳이 새 클래스를 따로 만들고 이름까지 붙이는 것은 오히려 번거로울 수 있다.

이런 경우에 익명 클래스를 떠올리면 된다

  • 한 번만 잠깐 사용할 객체가 필요할 때
  • 메서드에 객체를 바로 전달하고 싶을 때
  • 굳이 클래스 이름을 따로 만들 필요가 없을 때

즉, 익명 클래스는 짧게 한 번 쓸 구현 객체를 그 자리에서 바로 만들기 위한 구조라고 보면 된다.


익명 클래스는 두 가지 형태로 많이 본다

익명 클래스는 겉모양이 비슷해서 처음 보면 다 같은 것처럼 느껴질 수 있다.
하지만 출발점이 다르다.
기존 클래스를 기준으로 만드는 경우가 있고, 인터페이스를 기준으로 만드는 경우가 있다.

두 형태를 나누는 가장 쉬운 기준

  • new 클래스명() { ... }
    • 기존 클래스의 자식 클래스를 바로 만든다.

  • new 인터페이스명() { ... }
    • 인터페이스를 구현한 클래스를 바로 만든다.

겉모양은 비슷하지만, 하나는 상속, 다른 하나는 구현이라는 점이 다르다.
이 기준이 먼저 잡혀야 뒤의 예제가 덜 헷갈린다.

익명 클래스는 이름 없이 바로 정의하고, 그 자리에서 객체까지 함께 만드는 1회용 구조로 많이 사용된다.


그림의 pr(new Test() { ... });는 Test를 상속하는 이름 없는 자식 클래스를 만들고, 그 객체를 바로 pr()에 전달하는 흐름이다.
즉, 따로 클래스 이름을 만든 뒤 다시 new한 것이 아니라, 지금 필요한 자리에서 바로 만들어 전달한 것이다.


추상 클래스를 상속하는 익명 클래스

먼저 추상 클래스를 기준으로 만드는 형태부터 보면 이해가 쉽다.
추상 클래스는 직접 객체를 만들 수 없는 틀이다.
안에 추상 메서드가 있어서, 자식 클래스가 그 내용을 완성해야 하기 때문이다.


그런데 익명 클래스를 사용하면, 그 자식 클래스를 이름 없이 바로 만들 수 있다.
그래서 new 추상클래스명() { ... } 형태가 나온다.

이 구조를 읽는 방법

  • new Test()
    • Test를 기준으로 객체를 만들겠다는 뜻이다.

  • { ... }
    • 그런데 Test는 추상 클래스이므로, 필요한 메서드 구현을 여기서 바로 채운다는 뜻이다.

즉, 이름 없는 자식 클래스를 그 자리에서 바로 하나 만든다고 이해하면 된다.


예제로 확인하기 - InnerTest3

InnerTest3는 추상 클래스 Test를 상속하는 익명 클래스 예제다.
여기서 Test는 생성자를 가지고 있고, output()이라는 추상 메서드를 가진다.
즉, 직접 객체를 만들 수는 없고 자식이 구현을 완성해야 하는 틀 역할을 한다.


그리고 pr(Test o)는 Test 타입의 객체를 받아서 o.output("Test");를 호출한다.
이제 main()에서는 new Test() { ... }를 사용해 이름 없는 자식 클래스를 바로 만들어 전달한다.

먼저 구조를 보면

  • Test
    • 추상 클래스다.

  • pr(Test o)
    • Test 타입 객체를 받는다.

  • new Test() { ... }
    • Test를 상속한 익명 클래스를 바로 만든다.

즉, 이 예제는 “추상 클래스의 자식 클래스를 이름 없이 즉석에서 만든다”는 흐름을 보여 준다.

// InnerTest3.java
package extraexam;

abstract class Test {
    Test() {
        System.out.println("No Argument sample"); // 부모 생성자 실행
    }

    abstract void output(String s); // 자식이 구현해야 하는 메서드
}

public class InnerTest3 {
    void pr(Test o) {
        o.output("Test"); // 전달받은 객체의 메서드 호출
    }

    public static void main(String args[]) {
        System.out.println("Main start !!");
        InnerTest3 n = new InnerTest3();
        n.pr(new Test() {
            int su = 100; // 익명 클래스 안의 멤버

            public void output(String s) {
                System.out.println("Anonymous Class : " + s); // 오버라이딩한 메서드 실행
                System.out.println("Anonymous Class : " + su);
            }
        });
    }
}
// 출력결과
// Main start !!
// No Argument sample
// Anonymous Class : Test
// Anonymous Class : 100

실행 순서를 따라가면 더 잘 보인다

  • Main start !!
    • main()이 시작되었음을 먼저 출력한다.

  • No Argument sample
    • new Test() { ... }로 익명 클래스 객체를 만드는 순간 부모 생성자가 먼저 실행된다.

  • Anonymous Class : Test
    • pr() 안에서 o.output("Test")가 호출되고, 익명 클래스가 구현한 output()이 실행된다.

  • Anonymous Class : 100
    • 익명 클래스 안에 추가한 멤버 su를 출력한다.

이 흐름에서 가장 중요한 점은 두 가지다.

이 예제에서 꼭 잡아야 하는 핵심

  • 익명 클래스도 결국 Test를 상속한 자식 클래스다.
  • 그래서 객체를 만들 때 부모 생성자도 함께 연결된다.

초보자는 “이름이 없으니까 그냥 특별한 문법인가?”라고 느낄 수 있다.
하지만 이 예제는 익명 클래스도 결국 자식 클래스라는 점을 보여 준다.
그래서 No Argument sample이 먼저 출력되는 것이다.


또 su를 보면, 익명 클래스도 안에 멤버를 가질 수 있다는 점이 보인다.
이름이 없을 뿐이지, 내용이 없는 껍데기 구조가 아니라는 뜻이다.


인터페이스를 구현하는 익명 클래스

이번에는 상속이 아니라 구현이다.
인터페이스는 메서드의 틀만 제공하는 구조다.
즉, 무엇을 해야 하는지는 정해 주지만, 실제 동작 내용은 구현하는 쪽에서 채워야 한다.


익명 클래스를 사용하면 이 구현도 그 자리에서 바로 만들 수 있다.
그래서 new 인터페이스명() { ... } 형태가 나온다.

이 구조를 읽는 방법

  • new Testable()
    • Testable 타입 객체가 필요하다는 뜻이다.

  • { ... }
    • 필요한 메서드 구현을 여기서 바로 작성한다는 뜻이다.

즉, 인터페이스를 구현한 이름 없는 클래스를 즉석에서 만드는 방식이다.


예제로 확인하기 - InnerTest4

InnerTest4는 인터페이스 Testable을 구현하는 익명 클래스 예제다.
Testable에는 output() 메서드가 있고, FS라는 상수도 있다.
pr(Testable o)는 Testable 타입 객체를 받아서 o.output("Test");를 호출하고, 이어서 Testable.FS를 출력한다.


그리고 main()에서는 new Testable() { ... } 형태로 이름 없는 구현 클래스를 바로 만들어 전달한다.
즉, 이번에는 추상 클래스를 상속한 것이 아니라 인터페이스를 구현한 익명 클래스다.

먼저 구조를 보면

  • Testable
    • 인터페이스다.

  • pr(Testable o)
    • Testable 타입 객체를 받는다.

  • new Testable() { ... }
    • Testable을 구현한 익명 클래스를 바로 만든다.

즉, 이 예제는 “인터페이스 구현 객체를 이름 없이 즉석에서 만든다”는 흐름을 보여 준다.

// InnerTest4.java
package extraexam;

interface Testable {
    final static String FS = "Interface Test"; // 인터페이스 상수

    abstract void output(String s); // 구현해야 하는 메서드
}

public class InnerTest4 {
    void pr(Testable o) {
        o.output("Test"); // 구현된 메서드 호출
        System.out.println(Testable.FS); // 인터페이스 상수 사용
        // System.out.println(o.su); // 추가 멤버는 Testable 타입으로 접근 불가
    }

    public static void main(String args[]) {
        System.out.println("Main start !!");
        InnerTest4 n = new InnerTest4();
        n.pr(new Testable() {
            int su = 100; // 익명 클래스 안에 추가한 멤버

            public void output(String s) {
                System.out.println("Anonymous Class : " + s);
                System.out.println("Anonymous Class : " + su);
            }
        });
    }
}
// 출력결과
// Main start !!
// Anonymous Class : Test
// Anonymous Class : 100
// Interface Test

이 예제에서 가장 중요한 포인트

  • o.output("Test")
    • 익명 클래스가 구현한 메서드가 실행된다.

  • Testable.FS
    • 인터페이스에 원래 선언된 상수이므로 사용할 수 있다.

  • o.su
    • 실제 객체 안에는 있어도 Testable 타입 기준으로는 접근할 수 없다.

여기서 초보자가 가장 헷갈리기 쉬운 부분은 su다.
객체 안에 su가 실제로 들어 있으니, 밖에서도 당연히 쓸 수 있을 것처럼 느껴질 수 있다.
하지만 바깥에서 보고 있는 기준은 Testable o다.
즉, 이 변수는 Testable 타입으로 객체를 보고 있다.


그래서 바깥에서는 Testable이 원래 알고 있는 멤버만 접근할 수 있다.
output()은 인터페이스에 선언된 메서드이므로 가능하다.
FS는 인터페이스 상수이므로 가능하다.
하지만 su는 익명 클래스 안에서 새로 추가한 멤버일 뿐, Testable 인터페이스의 원래 멤버는 아니다.
그래서 o.su는 사용할 수 없다.


즉, 실제 객체 안에 멤버가 있어도, 참조 타입이 모르면 그 멤버에는 접근할 수 없다.
이 기준은 뒤에서 상속과 다형성을 이해할 때도 계속 중요하게 쓰인다.

익명 클래스는 기존 클래스를 상속해서 만들 수도 있고, 인터페이스를 구현해서 만들 수도 있다.


왼쪽의 new 클래스명() {}는 그 클래스의 이름 없는 자식 클래스를 만든다고 이해하면 된다.
오른쪽의 new 인터페이스명() {}는 그 인터페이스를 구현한 이름 없는 클래스를 만든다고 이해하면 된다.


즉, 익명 클래스는 겉모양은 비슷하지만 출발점이 클래스인지 인터페이스인지에 따라 의미가 달라진다.


익명 클래스에서 꼭 기억할 점

익명 클래스는 이름이 없는 대신, 필요한 구현을 바로 그 자리에서 넣을 수 있다.
그래서 메서드 호출 인자로 객체를 넘겨야 할 때 자주 쓰인다.


이번 정리에서는 두 가지 형태를 확인했다.

이번에 확인한 두 가지

  • InnerTest3
    • 추상 클래스를 상속하는 익명 클래스

  • InnerTest4
    • 인터페이스를 구현하는 익명 클래스

결국 핵심은 같다.
클래스를 따로 이름 붙여 만들지 않고, 지금 필요한 자리에서 바로 정의하고 바로 객체를 만들어 사용한다는 점이다.

마지막으로 짧게 정리하면

  • 이름 없이 바로 정의한다.
  • 클래스 상속 방식도 가능하고, 인터페이스 구현 방식도 가능하다.
  • 한 번 잠깐 쓸 객체를 만들 때 특히 잘 어울린다.

즉, 익명 클래스는 이름 없는 특별한 문법이라기보다, 필요한 자리에서 바로 구현 객체를 만드는 방식이라고 이해하면 된다.




내부 클래스 한 번에 정리

이제 지금까지 본 내용을 한 번에 묶어 보면, 내부 클래스는 종류 이름을 따로따로 외우는 것보다 어떤 기준으로 나뉘는지를 먼저 잡는 것이 더 중요하다.
이 기준만 잡히면 멤버 클래스, 로컬 클래스, 익명 클래스가 서로 섞여 보이지 않는다.


즉, 이번 단원은 문법 모양을 외우는 단원이 아니라, 어디에 정의되었는지, 어떻게 객체가 만들어지는지, 어디까지 사용할 수 있는지를 구분하는 단원이라고 이해하면 된다.


어디에 정의되었는가로 먼저 구분하기

내부 클래스는 먼저 어디에 정의되었는지를 보면 큰 흐름이 잡힌다.
이 기준이 가장 기본이 된다.

위치 기준으로 보면 이렇게 나뉜다

  • 멤버 클래스
    • 바깥 클래스 안, 메서드 바깥에 정의된다.
    • 클래스의 구성 요소처럼 다뤄진다.

  • 로컬 클래스
    • 메서드 안에 정의된다.
    • 그 메서드 안에서만 사용할 수 있다.

  • 익명 클래스
    • 이름 없이 특정 자리에서 바로 정의된다.
    • 보통 객체를 곧바로 만들어 전달한다.

즉, 내부 클래스는 전부 “클래스 안에 있는 클래스”라는 공통점은 있지만, 정의된 위치가 다르면 역할도 달라진다.


여기서 가장 먼저 떠올려야 하는 기준은 이것이다.

위치만 봐도 먼저 감이 잡힌다

  • 클래스 안, 메서드 바깥이면 멤버 클래스
  • 메서드 안이면 로컬 클래스
  • 이름 없이 바로 만들면 익명 클래스

이 기준이 잡혀 있으면 뒤의 코드가 훨씬 덜 낯설어진다.


바깥 객체가 필요한가로 다시 구분하기

이번에는 생성 방식을 기준으로 보면 차이가 더 또렷해진다.
특히 멤버 클래스에서는 이 기준이 중요하다.

생성 방식으로 보면 이렇게 보인다

  • 인스턴스 클래스
    • 바깥 객체가 먼저 필요하다.
    • 바깥 객체를 통해 내부 객체를 만든다.

  • 스태틱 클래스
    • 바깥 객체가 필요 없다.
    • 클래스 이름으로 바로 접근한다.

  • 로컬 클래스
    • 메서드 안에서 정의하고, 그 안에서 객체를 만든다.

  • 익명 클래스
    • new 부모타입() { ... } 형태로 정의와 객체 생성을 동시에 한다.

즉, 내부 클래스를 볼 때는 “이름이 뭐지?”보다 만들 때 무엇이 먼저 필요한지를 보는 것이 더 중요하다.


특히 멤버 클래스는 이 차이가 분명하다.

멤버 클래스는 이렇게 나뉜다

  • 바깥 객체가 있어야 하면 인스턴스 클래스
  • 바깥 객체 없이 바로 쓰면 스태틱 클래스

그래서 o.new Inner() 같은 문법이 나오면 인스턴스 클래스를 떠올리면 되고, Outer.Static_Inner.pr() 같은 문법이 나오면 스태틱 클래스를 떠올리면 된다.


사용할 수 있는 범위로 보면 더 분명해진다

내부 클래스는 단순히 어디에 있느냐만 중요한 것이 아니다.
어디까지 사용할 수 있는지도 함께 봐야 한다.

사용 범위 기준으로 보면

  • 멤버 클래스
    • 클래스의 구성 요소처럼 이해한다.

  • 로컬 클래스
    • 정의된 메서드 안에서만 사용한다.

  • 익명 클래스
    • 보통 필요한 자리에서 한 번 바로 만들어 쓴다.

즉, 내부 클래스는 위치와 범위가 함께 움직인다.
어디에 정의되었는지가 곧 어디까지 사용할 수 있는가와 연결된다.


여기서 로컬 클래스는 특히 이 기준이 중요했다.

로컬 클래스는 이렇게 기억하면 쉽다

  • 참조할 수 있는 값은 넓다.
  • 하지만 활동할 수 있는 위치는 메서드 안으로 좁다.

즉, 바깥 클래스 필드나 메서드 안의 값을 함께 사용할 수 있어도, 사용할 수 있는 위치 자체는 끝까지 메서드 안이다.


예제 네 개를 한 줄씩 연결해서 보기

이번에 사용한 예제 네 개는 내부 클래스의 핵심 종류를 각각 정확히 보여 준다.
그래서 마지막에 한 번씩 연결해서 보면 전체 흐름이 정리된다.

예제별 핵심 역할

  • InnerTest1
    • 멤버 클래스를 보여 준다.
    • 특히 인스턴스 클래스와 스태틱 클래스의 차이를 확인할 수 있다.

  • InnerTest2
    • 로컬 클래스를 보여 준다.
    • 메서드 안에서만 정의되고, 바깥 필드와 메서드 안의 값을 함께 사용하는 흐름을 확인할 수 있다.

  • InnerTest3
    • 추상 클래스를 상속하는 익명 클래스를 보여 준다.
    • 부모 생성자 호출과 메서드 오버라이딩까지 함께 확인할 수 있다.

  • InnerTest4
    • 인터페이스를 구현하는 익명 클래스를 보여 준다.
    • 인터페이스 상수 사용과 참조 타입 기준 접근 범위까지 같이 확인할 수 있다.

즉, 이번 예제 네 개는 서로 따로 노는 예제가 아니다.
내부 클래스의 핵심 종류를 하나씩 정확하게 대응해서 보여 주는 예제 묶음이다.


익명 클래스는 왜 따로 기억해야 하는가

익명 클래스는 앞의 두 종류보다 더 낯설게 느껴질 수 있다.
이름이 없고, 그 자리에서 바로 만들어지기 때문이다.
그래서 처음에는 그냥 특이한 문법처럼 보이기 쉽다.


하지만 익명 클래스도 결국 기준은 같다.

익명 클래스도 이렇게 보면 된다

  • 이름 없이 바로 정의한다.
  • 바로 객체를 만든다.
  • 클래스 상속 방식도 가능하고, 인터페이스 구현 방식도 가능하다.

즉, 익명 클래스는 특별한 예외가 아니라, 필요한 자리에서 바로 구현 객체를 만드는 내부 클래스 방식이라고 이해하면 된다.


특히 InnerTest4에서는 중요한 기준 하나가 더 보였다.

꼭 같이 기억해야 하는 기준

  • 실제 객체 안에 멤버가 있어도
  • 참조 타입이 모르면 그 멤버에는 접근할 수 없다.

이 기준은 익명 클래스만의 내용이 아니라, 뒤에서 상속과 다형성을 이해할 때도 계속 중요하게 이어진다.


마지막 정리

내부 클래스는 클래스 안에 다시 정의하는 클래스다.
하지만 진짜 중요한 것은 “안에 있다”는 사실 자체가 아니라, 바깥 클래스와 어떤 관계를 가지는가, 어디서 쓰이는가, 어떻게 만들어지는가다.

이번 단원에서 가장 중요하게 남겨야 할 기준

  • 어디에 정의되었는가
  • 바깥 객체가 필요한가
  • 어디까지 사용할 수 있는가
  • 이름이 있는가 없는가

이 네 가지 기준만 확실히 잡히면 멤버 클래스, 로컬 클래스, 익명 클래스는 따로 외우지 않아도 자연스럽게 구분된다.


즉, 내부 클래스 단원은 문법을 기계적으로 외우는 단원이 아니다.
바깥 클래스와의 관계, 사용 범위, 객체 생성 방식을 읽는 단원이라고 이해하면 된다.
이 기준이 잡히면 InnerTest1부터 InnerTest4까지의 예제가 각각 무엇을 보여 주는지도 훨씬 편하게 보인다.

0개의 댓글