Programming/Java

[Effective Java] item24 - 멤버 클래스는 되도록 static으로 구성하라!

VSFe 2021. 3. 16. 00:52
멤버 클래스는 되도록 static으로 구성하라!

가끔 다른 코드를 보면 클래스 내에 정의된 중첩 클래스 (Nested Class)가 있을때가 많다. 이 경우 중첩 클래스는 반드시 자신을 감싼 바로 바깥 클래스에서만 사용할 수 있다.

이 책에서는 중첩 클래스의 종류를 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스로 분류하고 있는데, 이번 아이템에서는 각각의 중첩 클래스에 대해 맛 볼 것이다.

정적 멤버 클래스는 다른 클래스 내부에 선언되고, 바깥 클래스의 private에 접근할 수 있는 점만 빼면 일반 클래스와 동일한 역할을 하고, 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미로 사용된다. public으로 사용하기 때문에 Calculator.Operation.PLUS 같이 직접적으로 명시할 수 있다.

비정적 멤버 클래스는 대충 보기엔 static만 빠진 걸로 보이지만, 많은 차이가 있다.

우선 정적 내부 클래스는 다음과 같이 객체를 생성할 수 있다.

void foo() {
	A.B b = new B();
}

static이 붙어있기 때문에 A 생성 여부와 상관없이 독립적인 생성이 가능한데, 비정적 멤버 클래스의 경우 static이 없기 때문에 A를 먼저 생성하고, B를 생성해야 한다.

void foo() {
	A a = new A();
	A.B b = new a.new B();
}

반드시 상위 클래스가 생성되어야 사용할 수 있기 때문인지, 정규화된 this를 사용해 바깥 인스턴스의 메소드를 호출하거나 참조를 가져올 수 있다. (정규화된 this란 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 것을 말한다.)

성능 측면에서, 비정적 내부 클래스의 경우 바깥 클래스에 대한 참조를 가지고 있기 때문에 GC가 발생하지 않아 메모리 누수가 발생할 수 있다. 또한, 두 관계는 인스턴스가 생성될 때 확립되고, 수정 불가능하다.

어댑터 디자인 패턴을 사용할 일이 있을 때 비정적 멤버 클래스가 자주 쓰인다. 어댑터 패턴이란 클라이언트에서 요구하는 형태를 인터페이스에 적용해주는 중간 매개체같은 클래스인데, 비정적 멤버 클래스의 경우엔 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 만들어주는 것이다.

public class MySet<E> extends AbstractSet<E> {
    ...

    @Override
    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        
    }
}

물론 위에서 설명했던 메모리 문제 때문에, 바깥 인스턴스에 접근할 일이 없다면 static을 붙여서 정적 멤버 클래스로 만드는게 좋다.

위에선 public 정적 멤버 클래스를 설명했는데, private 정적 멤버 클래스는 어떨까? 일반적으로 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 쓴다.

Map을 떠올리면, 각각의 키와 값을 매칭해 놓고, 이것을 표현하는 엔트리 객체들을 내부에 보관하고 있는데, 이것을 활용하면 Map을 직접 거치지 않고 사용할 수 있다. 그런데 이런상황에서 비정적으로 선언하게 된다면, 모든 엔트리가 바깥에 대한 참조를 갖게 되면서 효율이 다소 떨어질 것이다.

익명 클래스는 이름이 없고, 바깥 클래스의 멤버도 아니다. 단지 파이썬의 람다 마냥 선언과 동시에 인스턴스가 생기고 비정적 문맥에서만 사용할 수 있다. 사실 자바에서도 람다가 들어오기 전에는 익명 클래스를 주로 사용했는데, 현재는 람다가 왕위를 계승했다. 요즘은 익명 클래스를 통해 팩토리 메소드를 구현할 수 있는 점에서 착안해 그쪽에서 많이 사용된다.

지역 클래스는 거의 등장하지 않는 개념인데, 이 친구는 지역변수를 선언할 수 있는 곳이면 선언할 수 있고, 유효 범위도 지역변수와 동일하다. 멤버 클래스 마냥 이름이 있고 반복해서 사용할 수 있지만, 익명 클래스 마냥 비정적 문맥에서 사용될 때만 바깥을 참조할 수 있고, 정적 멤버는 갖지 못한다.