티스토리 뷰
인텔리제이에서 내부 클래스를 선언할 때, static
으로 선언하지 않으면 경고 메세지가 표시된다.
그냥 그런가 보다 하고 있었는데, 어느날 궁금해서 찾아보다가 간과하기에는 큰 문제점이 있다.
내부 클래스는 외부 클래스를 참조
비정적(non-static) 내부 클래스는 외부 클래스와 강하게 연결되어 있기 때문에 외부 클래스의 맴버 변수나 메서드에 접근할 수 있다. 특히 비정적(non-static) 맴버 클래스의 인스턴스는 외부 클래스의 인스턴스 없이는 독립적으로 존재할 수 없기 때문에 암묵적으로 연결된다.
public class OuterClass {
private int outerNum = 10;
public void func1() {
InnerClass innerClass = new InnerClass();
innerClass.func2();
}
class InnerClass {
private int num = 20;
public void func2() {
System.out.println("outerNum = " + outerNum);
System.out.println("num = " + num);
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.func1();
}
}
위 예시처럼 OuterClass
의 outerNum
멤버 변수가 private
로 선언되어 있지만, 내부 클래스에서는 접근할 수 있다. 이는 내부 클래스가 외부 클래스의 인스턴스를 참조하고 있기 때문에 가능한 일이다. 컴파일 시점에, 내부 클래스는 외부 클래스의 인스턴스를 참조하는 필드와 그 필드를 통해 접근할 수 있는 코드를 추가한다.
InnerClass.java
가 컴파일된 OuterClass$InnerClass.class
파일을 인텔리제이에서 디컴파일 시켜준 코드를 보자.
먼저 OuterClass$InnerClass(OuterClass this$0)
라는 생성자를 추가시켜 줬다. 여기서 this$0
는 외부 클래스를 참조하고 있는 숨겨진 필드로, 내부 클래스가 외부 클래스를 참조하기 위해 컴파일러가 추가한 코드다. 이를 사용하고 싶어도 컴파일이 되기 전에는 사용할 수 없다.
그럼 만약에 필드 명이 같을 경우에는 어떻게 접근할 수 있을까? 이 때는 정규화된 this
문법을 사용하면 된다.
정규화된 this란 클래스명.this 형태로 외부 클래스의 이름을 명시하는 용법
public class OuterClass {
private int num = 100;
public void func1() {
InnerClass innerClass = new InnerClass();
innerClass.func2();
}
class InnerClass {
private int num = 20;
public void func2() {
System.out.println("Inner class num = " + num);
System.out.println("Outer class num = " + OuterClass.this.num);
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.func1();
}
}
OuterClass.this.num
으로 외부 클래스의 필드를 내부 클래스에서 접근할 수 있는 방법을 제공해 준다. 내부 클래스가 외부 클래스를 참조할 수 있게 해주는 이 숨겨진 필드 때문에 역설적이게도 내부 클래스를 static
으로 선언하는 것이 유리하다. 왜 그럴까?
비정적(non-static) 내부 클래스의 메모리 누수
먼저 다음 예시를 보자.
import java.util.ArrayList;
import java.util.List;
public class OuterClass {
private int[] arr;
OuterClass(int size) {
arr = new int[size];
}
InnerClass getInnerObject() {
return new InnerClass();
}
class InnerClass {
}
public static void main (String[]args){
List<InnerClass> innerList = new ArrayList<>();
int iter = 100;
for (int i = 0; i < iter; i++) {
System.out.println("i = " + i);
innerList.add(new OuterClass(100_000_000).getInnerObject());
}
}
}
위 코드의 main
메서드에서 예상되는 실행 흐름은 다음과 같다.
new OuterClass(100_000_000)
로OuterClass
인스턴스 생성하기 위해 생성자 호출size
= 100_000_000인int
타입 배열arr
필드를 생성 (4 byte * 100_000_000 = 약 400MB)- 생성된 임시 인스턴스의
getInnerObject()
메서드 호출 InnerClass
인스턴스를 생성 후 반환innerList
에 생성된InnerClass
인스턴스를 추가- 위 과정을 100회 반복
결과를 보면 11번의 순회만으로 힙 메모리 영역에서 OutOfMemoryError
예외가 발생한다. 단순히 400MB 크기의 배열을 담은 OuterClass
를 계속 생성하니까 그렇다고 생각할 수 있다. 하지만 위 코드에서 InnerClass
에 static
을 붙여 정적 내부 클래스로 만들어 주면 아무 무리 없이 동작한다.
자바를 제대로 공부하고 있는 사람이라면 위 예시에서 ‘GC(가비지 컬렉션)가 제대로 동작하지 않았나?’라는 의심을 먼저 해봐야 한다. 실행 흐름의 3번에서 임시로 생성된 OuterClass
의 인스턴스는 getInnerObject()
메서드를 호출한 뒤 역할이 끝났다. 따로 변수에 저장된 것도 아니기 때문에 사실 자바의 GC에 의해 정리돼야 한다. 문제는 여기서 발생한다.
InnerClass
가 OuterClass
의 참조 값을 히든 필드로 가지고 있다는 걸 우리는 이전에 직접 확인했다. 따라서 GC 입장에서는 OuterClass
의 인스턴스가 innerList
에 담긴 InnerClass
의 인스턴스의 필드 값으로 참조되고 있기 때문에 정리하지 못하는 것이다. 결과적으로 힙 메모리 영역에 불필요한 OuterClass
의 인스턴스 메모리들이 쌓이게 되고 OutOfMemoryError
가 발생한 것이다.
내부 클래스는 웬만하면 static으로 선언하자
내부 클래스를 static
으로 선언하면 위와 같은 불상사를 막을 수 있다. static
클래스라는 것은 외부 클래스의 인스턴스가 존재하지 않더라도 컴파일 시 단독으로 메모리에 올라갈 수 있다는 의미다. 따라서 외부 클래스의 인스턴스를 참조하는 히든 필드도 필요 없다.
확인해 보면 위처럼 정규화된 this
를 사용할 수 없고, 사용하고 싶다면 static
을 빼라고 조언한다. 즉, 위 예시에서 임시로 생성된 OuterClass
의 인스턴스를 어떤 인스턴스도 참조하고 있지 않아 GC의 정리 대상이 된다.
힙 메모리 관련 장애는 프로그램 실행에 있어 치명적이므로 웬만하면 내부 클래스는 static
으로 선언하는 것이 바람직하다. 사실 내부 클래스를 많이 사용하는 경우도 없겠지만 말이다.
그리고 내부 클래스를 사용한다는 것은 외부 클래스와 의미적으로 깊은 연관이 있을 때 사용해야 한다. 내부 클래스를 static
으로 선언하다고 해서 만능이 아니다. static
영역은 GC의 관리 영역 밖에 존재하므로, 무분별하게 사용하면 프로그램 종료시까지 남아있어 전체 시스템의 퍼포먼스에 악영향을 줄 수 있다.
또한, static
으로 선언된 내부 클래스는 실제로 외부 클래스 도움 없이 외부에서 인스턴스를 생성할 수 있다.
// OuterClass.java
public class OuterClass {
static class InnerClass {
private int num = 100;
public int getNum() {
return num;
}
}
}
// Main.java
public class Main {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = new OuterClass.InnerClass();
System.out.println(inner.getNum());
}
}
이와 같은 코드도 사실 설계가 잘못된 것이다. 이럴거면 내부 클래스를 굳이 외부 클래스의 정적 멤버 클래스로 작성하는게 아니라 단독으로 설계하는 것이 맞다. 만약 이럴 의도가 아니라면 내부 클래스를 외부에서 접근하지 못하도록 private
접근 제어자를 붙여주는 것도 다른 방법이 된다.
좋은 코드는 최대한 명확하게 의미가 보여야 한다. 애매하거나 모호한 코드는 후에 치명적인 결함을 만들 수 있다. 제한을 많이 둘 수록 명확해지는데 반해 유연함은 떨어질 수 있다. 언제나 이 중간지점을 찾는게 참 어려운 것 같다.
References
'Java' 카테고리의 다른 글
[Java] 빌드와 빌드 자동화 툴 Maven vs Gradle (0) | 2024.08.15 |
---|---|
[Java] 람다식과 함수형 인터페이스, java.util.function (0) | 2024.07.31 |
[Java] 객체 지향의 4대 개념 추상화/상속/다형성/캡슐화 (0) | 2024.07.13 |
[Java] 지네릭스(Generics) (3) | 2023.03.27 |
[Java] 객체 지향 설계 5원칙 - SOLID (0) | 2023.02.07 |
- Total
- Today
- Yesterday
- 스프링 컨테이너
- 생활코딩 javascript
- Spring Boot
- 쉘 코드
- 패킷 스위칭
- git merge
- Spring
- 스프링 테스트
- jsp
- 프로그래머스
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- Spring Data JPA
- 지옥에서 온 git
- 운영체제 반효경
- 스프링
- 파이썬 for Beginner 솔루션
- 김영환
- Python Cookbook
- 방명록 프로젝트
- 선형 회귀
- shell code
- JPA
- git
- 쉽게 배우는 운영체제
- Gradle
- spring mvc
- Computer_Networking_A_Top-Down_Approach
- Thymeleaf
- 파이썬 for Beginner 연습문제
- git branch
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |