[item 24] 멤버 클래스 되도록 static으로 만들라

김동훈·2023년 6월 11일
1

Effective-java

목록 보기
8/14
post-thumbnail

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에 대한 정리글

profile
董訓은 영어로 mentor

0개의 댓글