본문 바로가기
Development Tools/Java

[Java] Inner Class를 static으로 선언해야 하는 이유 (Feat. Enum)

by 수짱수짱 2026. 2. 11.

개요

개발을 진행할 때 외부나 DB에서 해당 값을 사용하지 않고 현재 클래스 로직 내에서만 사용하는 class 가 있다면 inner class 를 활용하곤 한다.

(Ex. helper 용도)

만약 현재 클래스 로직 내에서만 사용하고 도메인이나 DB 컬럼에 저장되지 않는 값인 이런 경우 응집도를 높이기 위해선 inner class를 활용하는 것이 좋다

이때, inner class 를 활용할 때 정적(static)으로 선언해야 할지, 비정적(non-static)으로 선언해야 할지에 대한 선택의 기준을 명확하게 세우고자 해당 글을 작성한다.

 

내부 클래스를 static으로 선언하지 않으면 일어나는 일

비정적 Inner Class는 Inner class 인스턴스를 만들기 위해 외부 클래스를 초기화한 후 내부 클래스를 초기화한다.

이 과정에서 내부 클래스는 외부 클래스에 대한 ‘외부 참조’를 가진다.

내부 클래스가 외부 클래스 멤버를 사용하지 않더라도 컴파일 과정에서 숨겨진 ‘외부 참조’가 생성된다.

즉, 비정적(non-static)멤버 클래스의 인스턴스는 외부 클래스의 인스턴스와 암묵적으로 연결된다.

이로인해 비정적 멤버 클래스는 인스턴스 메서드에서 정규화된 `this` 를 사용하여 외부 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있다.

public class OuterClass {
	int field = 10;
	int getField() {
		return this.field;
	}
	
	class InnerClass {
		int inner_field = 20;
		int getOuterField() {
				return OuterCalss.this.getField(); // 숨은 외부 참조가 있어서 정규화 this 사용 가능
			}
	}
}

 

비정적 내부 클래스와 외부 클래스의 인스턴스는 암묵적으로 연결되면서 '메모리 누수'가 발생할 수 있게 된다.

만약 외부 클래스가 더이상 필요없어지고 내부 클래스만 사용하는 경우, 정상적인 시스템 흐름에선 더이상 사용하지 않는 외부 클래스를 대상으로 GC가 처리해야 한다.

하지만, 비정적 내부 클래스로 선언했다면 외부 클래스와 비정적 내부 클래스가 연결되어 있기 때문에 사용하지 않는 외부 클래스를 GC 대상으로 삼을 수 없게된다.

이로인해 외부 클래스가 메모리에 계속해서 잔존하게 되고 '메모리 누수'가 발생할 수 있게 되는 것이다.

 

class OuterClass {
	private int[] data;
	
    // 비정적 내부 클래스
	class InnerClass {
	}
	
	public OuterClass(int size) {
		data = new int[size];
	}
	
	InnerClass getInnerClass() {
		return new InnerClass();
	}
}
public class Main {
	public static void main(String[] args) {
		ArrayList<Object> al = new ArrayList<>(); // InnerClass 객체를 저장할 리스트
		
		for(int counter = 0; counter < 50; counter++) {
			al.add(new OuterClass(100000000).getInnerClass());
			System.out.println(counter);
		}
	}
}

문제 상황에 대한 예시를 들어보자.

먼저, OuterClass에 int 형 배열 필드 data를 선언한다. 이는 객체 크기를 임의로 키우기 위한 데이터 저장소이다.

data 필드에 생성자를 통해 100000000 크기의 배열 사이즈를 넣어 생성한다.

이로써 4byte * 100000000 = 400MB 크기의 OuterClass 객체를 생성하게 되었다.

 

`al.add(new OuterClass(100000000).getInnerClass())` 

위 로직을 분석해 보자면, 비정적 Inner class 객체를 생성하기 위해 먼저 OuterClass의 생성자를 호출하여 OuterClass 인스턴스를 생성하고 있다.

이후 연달아서 getInnerClass 메소드를 호출하여 비정적 Inner class 객체의 생성자를 호출해 인스턴스를 생성하고 al 리스트에 담아주고 있다.

이때,  Inner Class 인스턴스를 생성하기 위해 생성된 Outer Class 인스턴스는 getInnerClass 메소드를 호출하는 용도로 딱 1번만 사용되고 버려지게 된다. 

(=추적할 수 있는 참조 값이 없음)

이경우 정상적인 루틴이라면 Outer Class 인스턴스는 GC의 대상이 되어야만 한다.

하지만, Inner Class가 Outer Class에 대한 '외부 참조'를 가지고 있기 때문에 Outer Class는 GC의 대상이 될 수 없다.

이로인해 Outer Class 생성자가 1번씩 호출될 때 마다 메모리에는 400MB 크기만큼의 데이터가 게속해서 쌓이게 된다.

Outer Class 인스턴스에 대한 GC가 이루어지지 않으니 결국 Out Of Memory Error가 발생하게 된다.

 

 

만약 정상적인 GC 흐름이었다면 메소드 호출로만 쓰인 Outer Class 인스턴스는 GC의 수거 대상이 되었을 것이다.

하지만, Inner Class에서 Outer Class를 참조하고 있는 관계가 되었기 때문에 Inner Class가 살아있는 한 계속해서 Outer Class도 살아있게 된다.

이로 인해 400MB 메모리가 계속해서 누적되어 메모리에 쌓이게 되고 OOM이 발생해 프로그램이 터지게 되는 불상사가 발생할 것이다.

 

static (정적) 내부 클래스로 선언하면 변경되는 특징들

그렇다면 static inner class로 내부 클래스를 선언하게 되면 어떻게 될까?

가장 먼저 Outer Class에 대한 '외부 참조'를 가지지 않게 된다는 변경점이 발생한다.

'외부 참조'를 가지지 않게 되는 사실은 컴파일 된 클래스 파일을 보고 확인할 수 있다.

// 컴파일 전
public class OuterClass {
	int field = 10;
	
	static class InnerClass {
		int innerField = 20;
	}
}

// 컴파일 후
class OuterClass$IneerClass {
	int ineerField = 20;
	
	OuterClass$IneerClass() {
	} // 외부 참조 존재 X
}

위 예제와 같이 static inner class 의 컴파일 결과는 외부 참조가 존재하지 않는 것을 확인할 수 있다.

동시에 외부 참조가 존재하지 않기에 `this` 키워드도 사용할 수 없다.

public class OuterClass {
	int field = 10;
	
	static class InnerClass {
		int innerField = 20;
		
		int getOuterField() {
			return OuterClass.this.field; // compile error 발생
		}
	}
}

 

 

static 내부 클래스 인스턴스는 외부 참조를 가지지 않기에 메모리 누수(OOM) 또한 발생하지 않는다.

class OuterClass {
    private int[] data;

	// 정적 Inner Class
    static class InnerClass {
    }

    public Outer_lass(int size) {
        data = new int[size];
    }

    InnerClass getInnerObject() {
        return new InnerClass();
    }
}

public class Main {
    public static void main(String[] args) {
        ArrayList<InnerClass> al = new ArrayList<>();
        
        for (int counter = 0; counter < 50; counter++) {
            al.add(new OuterClass(100000000).getInnerObject());
            System.out.println(counter);
        }
    }
}

해당 예제는 위에서 비정적 inner class의 OOM을 테스트 한 코드와 동일한 코드이다.

단지 차이점은 Inner Class가 static이냐 아니냐의 차이일 뿐이다.

해당 코드의 실행 결과는 OOM이 발생하지 않고 counter가 정상적으로 49까지 잘 출력되는 모습을 확인할 수 있다.

 

또한, static Inner Class의 인스턴스는 외부 인스턴스 없이 생성될 수 있다.

따라서 '외부 참조'가 존재하지 않기에 일회용 OuterClass 인스턴스는 GC 수거 대상이 되는 것이다.

'외부 참조'가 없으니 static Inner Class 인스턴스와 Outer Class의 인스턴스는 관계가 없는 것이다.

 

결론

내부 클래스가 외부 클래스의 참조 값을 사용해야 하는 경우라면 non-static inner class를 사용할 수 있다.

non-static inner class의 잘못된 사용은 OOM 및 결합도 증가를 유발하므로 사용을 지양하는 것이 좋다.

하지만, 무조건적인 사용 금지도 지양해야 한다. 따라서 상황에 따라서 사용해야 할 때는 반드시 주의해서 사용하자.

 

만약 내부 클래스가 외부 클래스의 참조 값을 사용하지 않는 경우라면 반드시 static 키워드를 붙여서 정적 Inner Class로 선언해주도록 하자.

 

번외) Enum 클래스

java의 Enum 또한 클래스이다. 

파일 내부에 Enum 을 선언하면 일반적으로 비정적(non-static) 클래스 처럼 보이겠지만 Enum은 내부 클래스 규칙이 다르게 적용된다는 것을 유의하자.

Enum 은 절대로 non-static inner class가 될 수 없다. Enum 은 항상 static 한 class 이다. (싱글톤 객체)

내부에 Enum 을 작성하고 static 키워드를 붙이지 않더라도 자동으로 컴파일 시점에 static 키워드가 붙게된다.

즉, 모든 Enum 타입은 암묵적으로 static이다. (싱글톤 객체22)

헷갈리지 말자!

 

 

 

 

Reference

☕ 내부 클래스(Inner Class) 장점 & 종류 총정리

☕ 내부 클래스는 static 으로 선언 안하면 큰일 난다