Skip to content

Item 24. 멤버 클래스는 되도록 static으로 만들라 #23

@youngkimi

Description

@youngkimi

Chapter : 4. 클래스와 인터페이스

Item : 24. 멤버 클래스는 되도록 static으로 만들라

Assignee : youngkimi


🍑 서론

중첩 클래스(nested class)

정의

  • 다른 클래스 안에 정의된 클래스를 말한다.
  • 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
public class OuterClass {
    public class InnerClass {
    }
}

종류

  • 정적 멤버 클래스
  • (비정적) 멤버 클래스
  • 익명 클래스
  • 지역 클래스

이 중 첫 번째를 제외한 나머지는 내부 클래스 (inner class)에 해당한다.
각 중첩 클래스를 언제, 사용해야 하는지 알아보자.

🍑 본론

정적 멤버 클래스

1. 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다.

public class OuterClass {
    private String val = "나는 Outer 의 Private Value";

    private static class StaticClass {
        void printOuterVal() {
            OuterClass outerClass = new OuterClass();
            System.out.println(outerClass.val);
        }
    }

    public static void main(String[] args) {
        StaticClass staticClass = new StaticClass();
        staticClass.printOuterVal();
    }
}

나는 Outer 의 Private Value 라고 출력된다.

2. 다른 정적 멤버와 같은 접근 규칙을 적용받는다.

private으로 선언하면 바깥 클래스에서만 접근 가능

3. 보통 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다.

Operation 열거 타입이 Calculator 클래스의 public 정적 멤버 클래스 (enum)로 사용

public class Calculator {
    public static enum Operation {
        PLUS, MINUS, MULTIPLY, DIVIDE;
    }

    public int execute(Operation op, int first, int second) {
        switch( op ) {
            case PLUS :
                return first + second;
            case MINUS :
                return first - second;
            case MULTIPLY :
                return first * second;
            case DIVIDE :
                return first / second;
            default:
                return 0;
        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        /**
        Calculator의 클라이언트에서 원하는 연산을 참조할 수 있다. 
        */ 
        System.out.println(calculator.execute(Calculator.Operation.PLUS, 5, 2));
        System.out.println(calculator.execute(Calculator.Operation.MINUS, 5, 2));
        System.out.println(calculator.execute(Calculator.Operation.MULTIPLY, 5, 2));
        System.out.println(calculator.execute(Calculator.Operation.DIVIDE, 5, 2));
    }
}

비정적 멤버 클래스

1. 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 없다면 정적 멤버 클래스로 만들어야 한다. 비정적 멤버 클래스는 바깥 인스턴스 없이 생성할 수 없다.

public class OuterClass {
    private static class StaticClass {
    }

    private class NonStaticClass {
    }

    public static void main(String[] args) {
        StaticClass staticClass = new StaticClass();
        NonStaticClass nonStaticClass = new NonStaticClass(); // 에러 발생
    }
}

라고 작성시,
'effective_java.item24.OuterClass.this' cannot be referenced from a static context 라는 경고가 출력된다.

public class OuterClass {
    private String val = "나는 Outer 의 Private Value";

    class NonStaticClass {
        void printOuterVal() {
            // 정규화된 this (클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법) 
            System.out.println(OuterClass.this.val);
        }
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        outerClass.new NonStaticClass().printOuterVal();
    }
}

나는 Outer 의 Private Value 라고 출력된다.

  • 위에서 말했듯, OuterClass와 별개로 생성될 수 없다. 바깥 클래스와 비정적 멤버 클래스는 암묵적으로 연결되기 때문이다.
  • 둘 사이의 관계는 비정적 멤버 클래스가 인스턴스화 될 때 확립되며 더 이상 변경 불가하다.
  • 이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 만들어지는 것이 일반적이지만, 위처럼 수동으로 만들 수도 있다.
  • 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.

2. 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자

비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용한다.

  • 예를 들어, Map의 인터페이스 구현체들은 보통 (keySet, EntrySet, values 메서드가 반환하는) 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.
  • Set, List와 같은 다른 컬렉션 인터페이스 구현들도 자신의 반복자(iterator) 구현 시 비정적 멤버 클래스를 주로 사용한다.
public class MySet<E> extends AbstractSet<E> {
    @Override
    public Iterator<E> iterator() {
        return new MyIterator();
    }
    // 비정적 멤버 클래스
    private class MyIterator implements Iterator<E> {
        // ...
    }
}
  • 이렇게 사용하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. (위에서 말했듯) 이 참조를 저장하려면 추가적인 시간, 공간이 필요하다. 더 심각한 문제는, GC가 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 발생할 수 있다. (item 7 참조) 참조가 눈에 보이지 않아 문제의 원인을 파악하기도 어렵다.
  • 많은 맵(Map) 구현체는 key-value 쌍을 표현하는 엔트리(Entry) 객체를 가지고 있다. 모든 엔트리가 맵과 연관되어 있지만 엔트리 메서드들(getKey, getValue, setValue)는 맵을 직접 사용하지 않는다. 따라서 엔트리를 비정적 멤버 클래스로 표현하는 것은 낭비고, Private 정적 멤버 클래스가 가장 알맞다. (풀어 설명하면, 맵을 직접 사용하지 않으므로 맵의 인스턴스에 대한 참조를 유지할 필요가 없는데, 비정적 멤버 클래스로 표현 시 인스턴스에 대한 참조를 유지해 오버헤드가 발생하기 때문.) static을 빼먹어도 맵은 여전히 작동하겠지만, 외부 클래스(여기서는 맵)에 대한 참조를 갖게 되어 오버헤드가 발생할 것이다.

익명 클래스

말 그대로, 이름이 없는 클래스. 일시적으로 한번만 사용되고 버려지는 객체 (일회용 클래스)

  • 선언한 지점에서만 인스턴스를 만들 수 있다.
  • instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.
  • 여러 인터페이스를 구현할 수도 없고, 구현하는 동시에 다른 클래스를 상속할 수도 없다.
  • 익명 클래스의 클라이언트는 해당 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.
  • 익명 클래스는 표현식 중간에 등장하므로, 짧지 않으면 가독성이 떨어진다.
  • 자바가 람다를 지원하기 이전에, 작은 함수 객체나 처리 객체를 만드는데 사용했다. (요새는 람다 씀. item 42)
  • 정적 팩토리 메서드를 구현할 때 사용한다. (item 20의 intArrayAsList 참조)

지역 클래스

메서드 내부에 위치하는 클래스 (지역 변수와 같은 성질), 지역 변수처럼 해당 메서드 내부에서만 한정적으로 사용. 접근 제한자와 static을 붙일 수 없다.

  • 가장 드물게 사용. 지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같다.
  • 다른 세 중첩 클래스와의 공통점도 하나씩 가지고 있다.
  • 멤버 클래스처럼 이름이 있고, 반복해서 사용할 수 있다.
  • 익명 클래스처럼 비 정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다.
  • 정적 멤버는 가질 수 없고, 가독성을 위해 짧게 작성해야 한다.

🍑 결론

  • 바깥 클래스 외부에서도 사용할거면 애초에 톱레벨 클래스로 만들어라.
  • 메서드 밖에서도 사용해야 하거나, 메서드 안에서 정의하기에 너무 길다면 멤버 클래스로 만든다.
  • 멤버 클래스의 인스턴스가 바깥 클래스의 인스턴스를 참조한다면 비정적 멤버 클래스, 그렇지 않다면 정적 멤버 클래스로 만들자.
  • 중첩 클래스가 한 메서드 안에서만 쓰이면서, 인스턴트를 생성하는 지점이 단 한 곳이고, 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.

Referenced by

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions