| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 클라우드아키텍처
- 헥사고날 아키텍처
- 회고
- HashMap
- 3계층 아키텍처
- 프로그래머스
- static-factory-method
- OpenSearch
- 다짐글
- design-pattern
- 글쓰기세미나
- object-creation
- constructor
- axios
- SpringBoot
- React
- UserLand
- 레벨1
- builder-pattern
- 코엑스그랜드볼룸
- DevOps
- Level2
- 코딩테스트
- 가용영역
- 글또10기
- 클린 아키텍처
- 글또
- 포트앤어댑터 아키텍처
- QueryDSL
- ReverseNested
- Today
- Total
oguri's garage
Inner Class에 Static을 붙여야 하는 이유 (메모리 관점) 본문
상황
코드를 작성하던 중 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;
}
}
내 코드를 다시 확인해보니 CategoryAnalyzer는 httpClient, 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으로 선언하는 것이 메모리 효율 측면에서 적절하다.
컴파일러의 경고를 무시하지 말고 확인해볼 필요가 있다.
'개발하다 > Java' 카테고리의 다른 글
| Java 문자열 조합, String.format()이 가독성에 좋은 이유 (0) | 2025.10.19 |
|---|---|
| (Java) Stream API에서 Checked Exception 처리하기 (0) | 2025.10.07 |
| (Java) Record - Lombok Builder vs Record 비교 (0) | 2025.10.05 |
| `null` 대신 `Optional.empty`를 return 해야 하는 이유 (0) | 2025.09.30 |
| Java 객체 생성 패턴 비교 및 선택 가이드 (0) | 2025.09.28 |