2024. 4. 5. 10:43ㆍ백엔드/자바
내부 클래스 (Inner Class)
내부 클래스 (inner class)란 하나의 클래스 내부에 선언된 또 다른 클래스를 의미한다.
보통 사용자 클래스 자료형이 필요하면, 메인 클래스 외부에 선언하거나, 따로 독립적인 클래스 파일을 만들어 불러와 사용해 왔다. 내부 클래스는 기존에 사용했던 방법 대신 클래스 내부에 선언해 사용할 수 있다. 하지만 사실 내부 클래스는 내부에 정의된다는 점을 제외하고는 일반적인 클래스와 다르지 않다. 우리가 어느 클래스에 변수나 상수가 필요하다면 클래스 멤버로서 클래스 내에 선언하여 사용해 왔듯이, 클래스도 마찬가지로 변수처럼 사용해 주면 된다.
내부 클래스는 보통 두 클래스가 서로 긴밀한 관계에 있거나, 하나의 클래스 또는 메서드에서만 사용되는 클래스일 때 사용할 수 있다.

이 글에선 내부 클래스를 설명하기 위해 내부 클래스를 감싸고, 내부 클래스를 제외한 클래스를 외부 클래스라고 부르도록 하겠다.
// Creature 클래스는 내부 클래스들의 외부 클래스
class Creature {
int life;
//Animal 클래스는 Creature 클래스의 내부 클래스
class Animal { ... }
//Insect 클래스는 Creature 클래스의 내부 클래스
class Insect { ... }
public void method() {
Animal animal = new Animal();
Insect insect = new Insect();
// ...
}
}
내부 클래스의 종류
내부 클래스에는 아래와 같이 총 네 가지가 있는데, 중점적으로 봐야 할 클래스는 static 클래스와 익명 클래스이다.
나중에 배우지만 static 클래스는 디자인 패턴에서 싱글톤 패턴과 Holder 패턴에서 쓰이는 기법 중 하나이고, 익명 클래스는 자바8부터 나온 람다 표현식의 기본 골자이기 때문이다.
인스턴스 클래스(instance class)
외부 클래스의 멤버변수 선언 위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다뤄진다.
주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다.
스태틱 내부 클래스(nested class)
외부 클래스의 멤버변수 선언 위치에 선언하며, 외부 클래스의 static 멤버처럼 다뤄진다. 다만 주의할 점은 static이라고 해서 new 생성자 초기화를 못하는 것은 아니다. static class는 일반적인 static 필드 변수나 static 메서드와 달리 메모리 구조나 기능이 전혀 다르다. 중첩 클래스의 일종이다.
지역 클래스(local class)
외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 메서드 블록 영역 내부에서만 사용될 수 있다.
익명 클래스(anonymous class)
클래스의 선언과 객체의 생성을 동시에 하는 이름 없는 클래스이다. 주로 클래스를 일회용을 사용할 때 자주 이용된다.
인스턴스 클래스(instance class)
- 클래스의 멤버 변수 선언부에 위치하고 static 키워드가 없는 내부 클래스
- 외부 클래스의 멤버로 취급되기 때문에 외부 클래스의 객체 먼저 생성한 후 내부 클래스의 객체 생성이 가능하다.
- 인스턴스 클래스 내부에는 instance 멤버만 선언할 수 있다. (static 멤버는 선언 불가)
- 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다.
class PocketBall {
// 인스턴스 변수
int size = 100;
int price = 5000;
// 인스턴스 내부 클래스
class PockeMonster {
String name = "피카츄";
int level = 10;
// static int cost = 100; ERROR! 인스턴스 내부 클래스에는 static 변수를 선언할 수 없다.
static final int cost = 100; // final static은 상수이므로 허용
public void getPockemonMember() {
// 별다른 조치없이 외부 클래스 멤버 접근 가능
System.out.println(size);
System.out.println(price);
// 내부 클래스 멤버
System.out.println(name);
System.out.println(level);
System.out.println(cost);
}
}
}
public class Main {
public static void main(String[] args) {
PocketBall ball = new PocketBall(); //먼저 외부 클래스를 인스턴스화
//외부클래스.내부클래스 형식으로 초기화하여 사용
//내부클래스도 외부 클래스의 멤버라고 생각하면 쉽게 이해할 수 있다
PocketBall.PockMoster pokemon = ball.new PokeMoster();
pokemon.getPokemonMember();
// 위의 단계를 한 줄로 표현
PocketBall.PokeMonster pokemon2 = new PocketBall().new PokeMonster();
}
}
외부 클래스를 인스턴스화하면 외부 클래스의 코드가 메모리에 올라오게 되고 이때 내부 클래스의 코드도 메모리에 올라오게 된다. 이렇게 외부 클래스의 코드를 먼저 메모리에 올려줘야 내부 클래스의 인스턴스를 생성할 수 있다.
다만 내부 클래스는 다른 클래스에서 직접 사용하는 것보다 해당(외부) 클래스에서만 사용하는 것이 일반적이므로, 위의 메인 코드와 같이 내부 클래스의 인스턴스를 다른 클래스에서 만드는 경우는 드물다. 이렇게 할 수 있다는 것만 알아두자.
이름이 같은 외부 클래스 메서드 호출
만일 다음과 같이 내부 클래스에서 외부 클래스와 동일한 메서드명을 사용할 때 외부 클래스의 메서드를 호출하려면 어떻게 해야 할까?
// 외부 클래스
public class Main {
public print(String txt) {
System.out.println(txt);
}
// 내부 클래스
class Sub {
public print() {}
}
}
클래스가 상속관계에 있을 때는 super 예약어를 통해 부모 메서드를 호출할 수 있었다. 하지만 외부 - 내부 클래스 관계는 상속 관계가 아니기 때문에 다른 방법으로 외부 클래스의 멤버를 호출해야 한다.
이때 정규화된 this를 사용하면 외부 클래스 인스턴스의 메서드를 호출하거나 외부 클래스 인스턴스의 참조를 가져올 수 있다.
정규화된 this란 "클래스명.this"의 형태로 외부 클래스의 이름을 명시하는 용법을 말한다.
public class Main {
public void print(String txt) {
System.out.println(txt);
}
class Sub {
public void print() {
Main.this.print("외부 클래스 메서드 호출");
System.out.println("내부 클래스 메서드 호출");
}
}
}
public static void main(String[] args) {
Main.Sub s = new Main().new Sub();
s.print();
/*
외부 클래스 메서드 호출
외부 클래스 메서드 호출
*/
}
다음에 배울 static 내부 클래스에서는 정규화된 this 문법을 사용할 수 없다.
인스턴스 클래스 컴파일
- 내부 클래스는 클래스 안의 클래스 형태이기 때문에 컴파일 형태가 조금 다르다.
- 컴파일 시 생성되는 클래스 파일명은 "[외부클래스]$[내부클래스].class" 형태이다.

스태틱 클래스 (static nested class)
- static 키워드가 붙은 내부 클래스
- 단, 일반적인 static 필드 변수나 static 메서드와 똑같이 생각하면 안 된다.
- static 클래스 내부에는 instance 멤버와 static 멤버 모두 선언할 수 있다.
- 그러나 일반적인 static 메서드와 동일하게 외부 클래스의 인스턴스 멤버에는 접근이 불가하고, 정적(static) 멤버에만 접근할 수 있다.
class PocketBall {
int size = 100;
static int price = 5000;
// static 내부 클래스
static class PocketMonster {
static String name = "이상해씨";
int level = 10;
public static void getPokeMember() {
// 외부 클래스 인스턴스 멤버 접근 불가능
// System.out.println(size);
// 외부 클래스 스태틱 멤버 접근 가능
System.out.println(price);
// 내부 클래스 멤버도 스태틱 멤버만 접근 가능
System.out.println(name);
// System.out.println(level);
}
}
}
public class Main {
public static void main(String[] args) {
// 스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다.
PocketBall.PokeMonster pokemon = new PocketBall.PokeMonster();
System.out.println(pokemon.level); // 스태틱 내부 클래스도 인스턴스로는 멤버에 접근 가능
System.out.println(PocketBall.PokeMonster.name);
// 클래스. 정적내부클래스.정적메서드()
PocketBall.PokeMonster.getPokeMember();
}
}
static 클래스에 대한 오해
많은 사람들이 실수하는 오해가 static 클래스가 그 외의 static 필드 변수나 static 메서드와 같이, 'static 이니까 메모리에 한 번 올려놓고 두고두고 사용하는 거겠지?'라고 생각하는 것이다.
하지만 static 클래스는 키워드가 static이 들어갔을 뿐, 우리가 알던 static 멤버, 메서드와는 다른 녀석이다.
다음 코드를 보면 static 변수와 내부 인스턴스 클래스 그리고 내부 static 클래스가 있다.
public class Main {
// 스태틱 필드 변수
static Integer num = new Integer(0);
// 내부 인스턴스 클래스
class InnerClass { ... }
// 내부 스태틱 클래스
static class InnerStaticClass { ... }
public static void main(String[] args) {
// 스태틱 필드 변수는 유일해서 서로 같다
Integer num1 = Main.num;
Integer num2 = Main.num;
System.out.println(num1 == num2); // true
// 생성된 내부 클래스 인스턴스는 서로 다르다
Main.InnerClass inner1 = new Main().new InnerClass();
Main.InnerClass inner2 = new Main().new InnerClass();
System.out.println(inner1 == inner2); // false
// 생성된 내부 스태틱 클래스 인스턴스는 서로 다르다
Main.InnerStaticClass static1 = new InnerStaticClass();
Main.InnerStaticClass static2 = new InnerStaticClass();
System.out.println(static1 == static2); // false
}
}
이들을 각각 메인 메서드에서 변수로 받아 같은지 다른지 비교해 보면, static 변수는 예상대로 메모리에서 한 번만 생성되어 num1과 num2가 참조하는 데이터가 같다.
하지만 내부 static 클래스는 일반 클래스처럼 초기화할 때마다 다른 객체가 만들어지는 것을 볼 수 있다. 위 코드에서 보았듯이 static 클래스는 static이라고 해서 메모리에 한 번만 로드되는 객체의 개념이 아닌 것이다.
따라서 static 내부 클래스는 non-static 내부 클래스와 달리 외부 인스턴스를 먼저 선언하지 않아도 바로 내부 클래스의 인스턴스를 바로 생성할 수 있다는 차이점이 존재할 뿐이다.
다만 우리가 non-static 내부 클래스 대신 static 클래스를 사용해야 하는 이유는 따로 있는데...
내부 클래스는 static 클래스로 선언하자
만일 내부 클래스를 이용하는데, 내부 클래스에서 클래스 바깥에 인스턴스를 사용할 일이 없다면 static 클래스로 선언해주어야 한다.

왜냐하면 static이 아닌 내부 인스턴스 클래스는 외부와 연결이 되어 있어 '외부 참조'를 갖게 되어 메모리를 더 먹고, 느리며, 무엇보다 GC의 대상에서 벗어나는 문제점을 일으키기 때문이다.
이것은 아주 큰 문제이다 (####추가 예정####)
static 클래스 컴파일

- 위의 인스턴스 내부 클래스와 같다.
로컬 클래스
- 메서드 내부에 위치하는 클래스 (지역 변수와 같은 성질을 지님)
- 지역 변수처럼 해당 메서드 내부에서만 한정적으로 사용된다. (해당 메서드 실행 외에는 클래스 접근 및 사용 불가)
- 접근 제어자와 static을 붙일 수 없다. 메서드 내부에서만 사용되므로 접근을 제한할 필요가 없고, 원래 메서드 내에는 static 멤버를 선언할 수가 없기 때문이다.
class PocketBall {
int size = 100;
int price = 5000;
public void pocketMethod() {
int exp = 5000;
// 메서드 내에서 클래스를 선언
class PocketMonster {
String name = "이상해씨";
int level = 10;
public void getPoketLevel() {
System.out.println(level); // 인스턴스 변수 출력
System.out.println(exp); // 메서드 지역 상수 출력
}
}
// 메서드 내에서 클래스를 선언
class PocketMonster2 {
String name = "리자몽";
int level = 50;
}
new PocketMonster().getPoketLevel();
System.out.println("메서드 실행 완료");
}
}
메서드 내에서 잠시 사용하는 지역 변수 개념과 같이 지역 클래스 자료형을 사용하고 버리는 것으로 이해하면 된다.
메서드 내의 모든 내용은 Stack Area에 생성되었다가 메서드 종료 시 사라지게 되므로, 당연히 다른 곳에서는 접근 및 사용 자체가 불가능하다.
메서드 내에서 인스턴스를 생성한 후 사용하고 메서드 종료와 함께 레퍼런스가 사라지면서 힙 메모리 영역의 실제 데이터도 나중에 GC(가비지 컬렉터)에 의해 지워지게 된다.
로컬 클래스 지역 상수 접근
- 메서드 내의 로컬 클래스에서 지역 변수에 접근해서 값을 사용하려고 할 때는 final 지역변수만 사용 가능하다.
- 변수에 final을 안 붙여도 실행이 되는 이유는, JDK1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final을 생략할 수 있기 때문이다. (컴파일러가 자동으로 붙여준다)
- 그러나 지역 변수(상수)의 값이 바뀌는 문장이 있으면 이는 상수가 아닌 변수이니, 컴파일 에러가 발생한다.

로컬 클래스 컴파일
- 컴파일 시 생성되는 클래스 파일명: "외부클래스$1내부클래스.class"
- 지역 클래스는 여러 메서드 내에서 생성이 가능하니 같은 클래스명이 중복될 수 있어 앞에 숫자를 붙여 구분한다.

익명 클래스
- 익명 클래스는 클래스 이름이 존재하지 않는 내부 클래스이다.
- 단 하나의 객체만을 생성하는 일회용 클래스
- 클래스의 선언과 동시에 객체를 생성
- 익명 클래스는 생성자가 존재하지 않는다.
- 익명 클래스는 기존에 존재하는 클래스를 메서드 내에서 일회용으로 클래스 내부 구성을 선언하여 필요한 메서드를 재정의하여 사용하는 기법이다.
interface Anonymous {
String amIAnonymous();
}
public class InnerClassEx implements Anonymous{
public static void main(String[] args) {
// Anonymous 클래스를 일회성으로 익명 클래스로 선언하여 변수 o에 저장
Anonymous o = new Anonymous() {
@Override
public String amIAnonymous() {
return "아 제가 익명 클래스입니다.";
}
}; // 익명 클래스 뒤에는 반드시 세미콜론을 붙혀줘야 한다.
// 익명 클래스의 객체의 오버라이딩한 메서드를 사용
String txt = o.amIAnonymous();
System.out.println(txt); // 아 제가 익명 클래스입니다.
}
@Override
public String amIAnonymous() {
return null;
}
}
익명 클래스 컴파일

- 컴파일 시 생성되는 클래스 파일명 : "외부클래스$1.class"
- 익명 클래스이기 때문에 클래스명은 없고 오직 숫자로만 구분한다.
내부 클래스의 사용 이유
지금까지 네 가지의 내부 클래스를 배웠다. 그런데 정작 내부 클래스를 쓰는 이유에 대해선 알지 못한다. 마지막으로 내부 클래스를 사용하는 이유에 대해서 알아보기 전에 기억해야 할 키워드는 딱 두 가지이다. 간결함과 캡슐화.
1. 클래스를 논리적으로 그룹화
클래스가 여러 클래스와 관계를 맺지 않고 하나의 특정 클래스와만 관계를 맺는다면, 외부에 클래스를 새로 작성하는 것이 아닌 내부 클래스로 작성할 수 있다.
이런 경우 내부 클래스와 외부 클래스를 함께 관리하는 것이 가능해 유지보수 면에서나 코드 이해성 면에서 편리해진다.
또한 내부 클래스로 인해 새로운 클래스를 생성하지 않아도 되므로 패키지를 간소화할 수 있다.
코드를 예시로 들어보자.
평소대로라면 아래처럼 클래스의 바깥이나 다른 파일로 클래스를 선언해 놓고, 인스턴스화하여 사용했을 것이다.
class Creature {
int life;
public void method() {
Animal animal = new Animal(); // Animal 객체는 오로지 Creature 클래스의 메소드 내에서만 사용된다고 가정
// ...
}
}
// 외부에 선언된 클래스
class Animal {
}
그런데 만일 Animal 클래스 자료형이 오로지 Creature 클래스의 메서드 내에서만 선언되어 사용된다면, Animal 클래스를 Creature 클래스 내부로 넣어줌으로써 보다 코드 자원의 이해도가 높아진다.
class Creature {
int life;
// 클래스 멤버 같이 Creature 클래스 안에다 넣어 선언한다.
class Animal {
}
public void method() {
Animal animal = new Animal(); // Animal 객체는 오로지 Creature 클래스의 메소드 내에서만 사용된다고 가정
// ...
}
}
2. 더욱 타이트한 캡슐화의 적용
내부 클래스에 `private` 제어자를 적용해 줌으로써, 캡슐화를 통해 클래스를 내부로 숨길 수 있다.
즉, 캡슐화를 통해 외부에서의 접근을 차단하면서도, 내부 클래스에서 외부 클래스의 멤버들을 제약 없이 쉽게 접근할 수 있어 구조적인 프로그래밍이 가능해진다. 그리고 클래스 구조를 숨김으로써 코드의 복잡성도 줄일 수 있다.
class Creature {
private int life = 50;
// private class 로, 오로지 Creature 외부 클래스에서만 접근 가능한 내부 클래스로 설정
private class Animal {
private String name = "호랑이";
int getOuter() {
return life; // 외부 클래스의 private 멤버를 제약 없이 접근 가능
}
}
public void method() {
Animal animal = new Animal();
// Getter 없이 내부 클래스의 private 멤버에 접근이 가능
System.out.println(animal.name); // 호랑이
// 내부 클래스에서 외부 클래스이 private 멤버를 출력
System.out.println(animal.getOuter()); // 50
}
}
3. 가독성이 좋고 유지 관리가 쉬운 코드
결국은 내부 클래스를 작성하는 경우 클래스를 따로 외부에 작성하는 경우보다, 물리적으로도 논리적으로도 외부 클래스에 더 가깝게 위치하게 된다. 따라서 시각적으로 읽기가 편해질 뿐 아니라 유지보수에 있어 이점을 가지게 된다.
한 클래스를 다른 클래스의 내부 클래스로 선언하면 두 클래스 멤버들 간에 서로 자유로이 접근할 수 있고, 외부에선 불필요한 클래스를 감춰 클래스 간의 연관관계를 따지는 것과 같은 코드의 복잡성을 줄일 수 있다는 장점이 있다.
요는 어차피 A 클래스 안에서만 사용하기 위한 클래스이니 굳이 연관관계를 따질 필요 없이 내부에서 선언해 직관적으로 사용하자는 것이다.
지금까지 내부 클래스를 공부해 봤다. 사실 내부 클래스부터는 이해하기가 조금 어려웠고, 지금도 내부 클래스의 개념은 이해가 되지만 실제로 어디서 쓰이는 지를 알 수가 없어서 나중에 실제로 코드를 짜게 되면 위 내용을 내가 경험한 것과 조금씩 연결시켜 나가야겠다.
원문
☕ 내부 클래스(Inner Class) 장점 & 종류 총정리
내부 클래스 (Inner Class) 내부 클래스(inner class)란 하나의 클래스 내부에 선언된 또 다른 클래스를 의미한다. 보통 사용자 클래스 자료형이 필요하면, 메인 클래스 외부에 선언하거나, 따로 독립적
inpa.tistory.com
'백엔드 > 자바' 카테고리의 다른 글
| 자바 - 업캐스팅(UpCasting) & 다운캐스팅(DownCasting) (0) | 2024.05.28 |
|---|---|
| 자바 - 익명 클래스(Anonymous Class) (0) | 2024.05.03 |
| 자바 - 추상 클래스 vs 인터페이스 (0) | 2024.03.28 |
| 자바 - 인터페이스(Interface) (0) | 2024.03.27 |
| 자바 - 추상 클래스(abstract class) (0) | 2024.03.26 |