oguri's garage

Inner Class에 Static을 붙여야 하는 이유 (메모리 관점) 본문

개발하다/Java

Inner Class에 Static을 붙여야 하는 이유 (메모리 관점)

oguri 2025. 10. 29. 17:58

상황

코드를 작성하던 중 IntelliJ에서 경고가 표시됐다.

 

"Class '~~~Processor' may be 'static'"

처음엔 무시했는데 반복되는 경고가 신경 쓰여서 찾아보기로 했다.



예시 코드

public class ProductStatisticsService {
    private final HttpClient httpClient;
    private final String apiEndpoint;
    private final Integer DEFAULT_LIMIT = 500;

    private class CategoryAnalyzer implements DataAnalyzer<CategoryStats> {
        private final Integer BATCH_SIZE = 100;

        @Override
        public String getAnalyzerName() {
            return "category_analyzer";
        }

        @Override
        public QuerySpec createQuery() {
            return QuerySpec.builder()
                .dimension("category")
                .limit(BATCH_SIZE)
                .threshold(1)
                .build();
        }

        @Override
        public List<CategoryStats> analyze(Map<String, DataPoint> dataPoints) {
            return null;
        }
    }
}


얼핏 보면 문제없어 보인다.
CategoryAnalyzer는 인터페이스를 구현하고, 자기 일만 하고 있다.



검색 결과

Non-Static Inner Class는 컴파일 시점에 Outer Class의 참조를 자동으로 포함한다는 것을 알게 됐다.

// 내가 작성한 코드
private class CategoryAnalyzer { ... }

// 실제로 컴파일되면
private class CategoryAnalyzer {
    private final ProductStatisticsService this$0;  // 컴파일러가 자동으로 추가

    CategoryAnalyzer(ProductStatisticsService outer) {
        this.this$0 = outer;
    }
}

 

내 코드를 다시 확인해보니 CategoryAnalyzerhttpClient, apiEndpoint, DEFAULT_LIMIT 중 어느 것도 사용하지 않는다.
그런데 컴파일러는 ProductStatisticsService 전체에 대한 참조를 자동으로 추가하고 있었다.



메모리 관점에서 확인

이 구조가 메모리에 미치는 영향을 정리했다.

ProductStatisticsService service = new ProductStatisticsService(...);
// service: httpClient, apiEndpoint, 기타 필드 포함

CategoryAnalyzer analyzer = new CategoryAnalyzer();
// analyzer가 service 전체를 참조하고 있음

 

analyzer가 메모리에 남아있는 동안 service도 GC 대상이 되지 않는다.
CategoryAnalyzer가 Outer의 어떤 멤버도 사용하지 않는데도 말이다.



Static 적용

private static class CategoryAnalyzer implements DataAnalyzer<CategoryStats> {
    private final Integer BATCH_SIZE = 100;

    @Override
    public String getAnalyzerName() {
        return "category_analyzer";
    }

    @Override
    public QuerySpec createQuery() {
        return QuerySpec.builder()
            .dimension("category")
            .limit(BATCH_SIZE)
            .threshold(1)
            .build();
    }

    @Override
    public List<CategoryStats> analyze(Map<String, DataPoint> dataPoints) {
        return null;
    }
}

 

static을 추가하니 Outer Class에 대한 참조가 제거됐다.
불필요한 메모리 점유가 사라지고, 코드의 의도도 명확해졌다.



그럼 언제 Non-Static을 써야 할까?

Outer의 멤버를 실제로 써야 할 때만 Non-Static을 쓰면 된다.

private class RemoteDataFetcher implements DataAnalyzer<ProductInfo> {
    @Override
    public QuerySpec createQuery() {
        return QuerySpec.builder()
            .endpoint(apiEndpoint)  // 여기서 outer의 apiEndpoint를 씀
            .maxCount(DEFAULT_LIMIT)  // 여기서 outer의 DEFAULT_LIMIT를 씀
            .build();
    }

    @Override
    public List<ProductInfo> analyze(Map<String, DataPoint> dataPoints) {
        return httpClient.fetch(dataPoints);  // outer의 httpClient 사용
    }
}

 

이런 경우는 Outer 참조가 필요하니까 그냥 Non-Static으로 두면 된다.



일부 값만 필요한 경우

만약 Outer의 값 중 일부만 필요하다면? 생성자로 받는 게 더 깔끔하다.

private static class CategoryAnalyzer implements DataAnalyzer<CategoryStats> {
    private final Integer batchSize;

    public CategoryAnalyzer(Integer batchSize) {
        this.batchSize = batchSize;
    }

    @Override
    public QuerySpec createQuery() {
        return QuerySpec.builder()
            .dimension("category")
            .limit(batchSize)
            .build();
    }
}

// 사용
CategoryAnalyzer analyzer = new CategoryAnalyzer(DEFAULT_LIMIT);


필요한 값만 받고, Outer 전체를 참조하지 않는다.
의존성도 명확하다.



판단 기준은 간단했다

Inner Class를 만들 때 이것만 확인하면 된다:

"이 클래스가 Outer의 인스턴스 변수나 메서드를 쓰나?"

// 이런 것들을 사용하는가?
httpClient.fetch(...)    // outer 인스턴스 변수
apiEndpoint             // outer 인스턴스 변수
DEFAULT_LIMIT          // outer 인스턴스 변수
someMethod()           // outer 인스턴스 메서드
  • 사용함 → private class (Non-Static)
  • 사용 안 함 → private static class (Static)



추가 확인 사항

Static과 클래스 로딩 시점

Nested Class는 사용 시점에 생성된다.
Static은 메모리 로딩 시점이 아니라 Outer 참조 보유 여부를 의미한다.


Static과 접근 제어

private static과 같이 접근 제어자와 함께 사용할 수 있다.
접근 제어와 static은 독립적인 개념이다.


Nested Class에서 Static의 의미

Outer Class의 인스턴스 참조를 포함하지 않는다는 의미다.



결론

  • public/private: 접근 제어
  • static: Outer 인스턴스 참조 여부

Outer의 인스턴스 멤버를 사용하지 않는 Inner Class는 static으로 선언하는 것이 메모리 효율 측면에서 적절하다.
컴파일러의 경고를 무시하지 말고 확인해볼 필요가 있다.