item 24에서 private static class
에 대해 말해준다. 언젠가 내부 클래스는 private static class로 만들어야한다
는 글을 본적이 있는데, 제목에서도 써있듯이 항상
그렇게 만들라는 것은 아니다. 이번 item을 통해 언제 그리고 왜 사용하는지에 대해 알 수 있을 것이다.
중첩 클래스는 다른 클래스안에 정의된 클래스를 말하며, 자신을 감싼 바깥 클래스에서만 사용되어야 한다. 중첩 클래스는 4가지로 나눌 수 있다.
정적 멤버 클래스는 public 도우미 클래스로 쓰인다. 이 예시로 item34를 언급하며 Operation 열거 타입이 Calculator클래스이 public 정적 멤버 클래스가 되어야한다고 한다. 열거타입은 뒷 item의 내용이긴 하지만 public 도우미 클래스
가 어떤 형태일지 이해를 돕기위해 간단하게 구현해보았다.
public class Calculator {
private int battery;
public Calculator() {
this.battery = 100;
}
public double calculate(Operation op, double x, double y) {
if(this.battery == 0) throw new RuntimeException("배터리 없음.");
this.battery -= 1;
return op.apply(x,y);
}
public static enum Operation {
PLUS,
MINUS,
TIMES,
DIVIDE,
;
public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
}
throw new AssertionError("알수없는 연산 : " + this);
}
}
}
이는 계산기 클래스이다. 간단히 사칙연산을 지원하는 계산기이다. calculate
함수는 Operation 도우미 클래스를 사용하여, 파라미터로 넘겨받은 Opertaion 상수에 따라 계산을 수행한다. 실제 Calculator 클라이언트에서는 다음처럼 사용할 것이다.
public class Main {
public static void main(String[] args) {
//계산기 수행.
Calculator calculator = new Calculator();
double calculate = calculator.calculate(Calculator.Operation.PLUS, 1.3, 1.7);
System.out.println("calculate = " + calculate);
//가능
double apply = Calculator.Operation.DIVIDE.apply(3, 2);
System.out.println("apply = " + apply);
}
}
계산기를 사용할 때, 어떤 operation에 따라 수행되어야하는 로직이 다를 것이다. 이런 경우 public static class(도우미 클래스)
로 enum클래스를 사용하여 원하는 연산을 쉽게 사용할 수 있을 것 같다.
즉, static이 아닌 멤버 클래스를 말한다. 이 클래스의 인스턴스는 바깥 클래스의 인스턴스로의 숨은 외부 참조를 갖는다
이 관계는 멤버 클래스의 인스턴스안에 만들어져, 메모리도 더 차지하고, 생성시간도 오래걸린다. 그리고 가장 중요하게 GC가 이를 수집하지 못하는 메모리 누수가 발생
할 수도 있다.
숨은 외부 참조? 메모리 누수?
public class Outer {
private int[] arr;
// 내부 클래스
class Inner_Class {
}
// 외부 클래스 생성자
public Outer(int size) {
arr = new int[size];
System.out.println("Outer 인스턴스 생성 : " +this);
}
public void print() {
System.out.println("Outer 인스턴스");
}
}
위 Outer클래스의 컴파일된 형태를 확인해보자. 그러면 다음과 같이 Inner_Class의 생성자에 매개변수로 Outer의 인스턴스가 넘어옴을 볼 수 있다.
Command Prompt를 열고, 해당 패키지로 경로이동한 뒤에 javac Outer.java -encoding UTF8
로 컴파일했다. encoding옵션은 내 intellij 설정을 잘못했었는지 한글 인식문제가 있어 UTF8로 지정한 것이다.
class Outer$Inner_Class {
Outer$Inner_Class(Outer var1) {
this.this$0 = var1;
}
}
body부분을 보면 this.this$0 = Outer인스턴스
형태로 참조를 가지게 된다. 이러한 방식은 코드 작성 시점에서는 눈으로 확인할 수 없으니 숨은 외부 참조
라고 말하는 듯 싶다.
그럼 메모리 누수
에 대해 확인해보자
public class Main {
public static void main(String[] args) {
Outer.Inner_Class inner_class = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class1 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class2 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class3 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class4 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class5 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class6 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class7 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class8 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class9 = new Outer(100_000_000).new Inner_Class();
Outer.Inner_Class inner_class10 = new Outer(100_000_000).new Inner_Class();
}
}
GC는 자바에서 사용되는 4가지의 참조관계에 따라 객체들을 수집한다.
이 코드만 보면 Outer인스턴스는 생성된 이후, 어떠한 참조도 갖지 못해 GC가 수집할 수 있을 것 처럼 보인다. 하지만 알고보면 위에서 말한 숨은 외부 참조
로 인해 즉, 내부 Inner_Class인스턴스가 Outer인스턴스를 참조하고 있으므로 강한 참조
관계를 가지게 되어 GC가 수집하지 않는것이다. 따라서, 어디에도 사용되지 않을 Outer인스턴스는 수집되는것이 더 좋겠지만, 그렇지 못해 메모리 누수
로 이어지게 되는 것이다.
그래서 글의 초반부에 말한 private static class
중 static에 대해 이유는 확실해졌다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 반드시 static을 붙여서 정적 멤버 클래스
로 만들어야한다.
이제는 private에 대해 생각해보자.
Map구현체
들을 생각해보면 이유를 찾을 수 있다. WeakHashMap을 생각해보자.
여러 구현체들과 마찬가지로 Entry라는 내부 클래스
로 key-value쌍을 표현하고 있다. 이 Entry클래스의 메소드들은 바깥클래스(WeakHashMap)의 정보에 직접 접근하지는 않는다. 그러니 당연히 static가 알맞겠다. 그리고 이전 item에서부터 계속 말해왔던 불변
에 있어 안전해야하므로 private이 알맞을 것이다. 만약 바깥 클래스의 구성요소로 쓰이는 멤버 클래스
가 public이라면 불변이 깨지는 문제가 발생한다. Outer클래스를 다음처럼 조금 수정해보았다.
public class Outer {
private int[] arr;
private 도우미클래스 helper[];
private int size;
public static class 도우미클래스 {
int a = 1;
private int b = 2;
final int c = 3;
static final int d = 4;
}
// 외부 클래스 생성자
public Outer(int size) {
arr = new int[size];
helper = new 도우미클래스[100];
size = 0;
System.out.println("Outer 인스턴스 생성 : " + this);
}
public 도우미클래스 push(도우미클래스 help) {
helper[size++] = help;
return help;
}
public 도우미클래스[] getHelper() {
return this.helper;
}
}
public static class인 도우미 클래스를 배열형태로 내부 구성요소로 사용하고 있다.
push메소드를 통해 삽입 과정이 진행된다. public이므로 외부에서 이 도우미클래스를 직접 생성할 수 있을 것이다. 그럼 데이터 조작도 충분히 가능하고, 이러한 행위가 Outer인스턴스에도 영향을 끼치게 되는데 이를 확인해보겠다.
Outer.도우미클래스 도우미클래스 = new Outer.도우미클래스();
Outer outer = new Outer(100);
outer.push(도우미클래스);
System.out.println(outer.getHelper()[0].a);
도우미클래스.a = -1;
System.out.println(outer.getHelper()[0].a);
우선 출력결과는 1
과 -1
이 출력된다. 도우미클래스
의 인스턴스를 조작한것이 Outer클래스까지 영향일 끼친 것이다. public으로 접근제한자를 지정하였기 때문에 외부에서 충분히 조작할 수 있게된다. 따라서 바깥 클래스의 한 구성요소로 사용하기 위해서는 private 정적 멤버 클래스
로 사용해야함을 알 수 있다.
effective-java스터디에서 공유하고 있는 전체 item에 대한 정리글