Effective Java : 아이템24. 멤버 클래스는 되도록 static으로 만들라.

    들어가기 전

    이 글은 인프런 백기선님의 이펙티브 자바 강의를 복습하며 작성한 글입니다. 


    이 글의 요약

    • 중첩 클래스 중 일부는 멤버 클래스다. 멤버 클래스가 아닌 중첩 클래스는 익명 클래스, 로컬 클래스가 있다. 
    • 중첩 클래스가 바깥 클래스의 인스턴스를 참조하지 않는다면 정적 멤버 클래스로 구성해서 메모리를 효율화 할 수 있다. 
    • 중첩 클래스가 바깥 클래스의 인스턴스를 참조한다면 비정적 멤버 클래스로 구성한다. 
    • 비정적 멤버 클래스는 바깥 클래스의 묵시적 참조를 가지며, Outerclass.this로 접근 가능하다.
    • 비정적 멤버 클래스는 주로 어댑터 패턴에 자주 사용된다. 
    • 익명 클래스는 메서드 scope에서 생성된 클래스다. 최근에는 람다 / 메서드 레퍼런스로 점차 사라지는 중이다.
    • 로컬 클래스는 쓰지 않는다. 

    핵심 정리 : 네 종류의 중첩 클래스와 각각의 쓰임

    • 정적 멤버 클래스
      • 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스. 예) Calculator.Operation.PLUS
      • 바깥 클래스 인스턴스 참조가 필요없는 내부 클래스라면, 반드시 정적 멤버 클래스로 사용
    • 비정적 멤버 클래스
      • 바깥 클래스의 인스턴스와 암묵적으로 연결된다.
      • 어댑터를 정의할 때 자주 쓰인다.
      • 멤버 클래스에서 바깥 인스턴스를 참조할 필요가 없다면 무조건 정적 멤버 클래스로 만들자.
    • 익명 클래스
      • 바깥 클래스의 멤버가 아니며, 쓰이는 시점과 동시에 인스턴스가 만들어진다.
      • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다. 
      • 자바에서 람다를 지원하기 전에 즉석에서 작은 함수 객체나 처리 객체를 만들 때 사용했다. 
      • 주로 메서드 Scope 내에서 선언되고 생성된다. 
      • 정적 팩터리 메서드를 만들 때 사용할 수도 있다. 
    • 지역 클래스
      • 가장 드물게 사용된다.
      • 지역 변수를 선언하는 곳이면 어디든 지역 클래스를 정의해 사용할 수 있다.
      • 가독성을 위해 짧게 작성해야한다.

     


    멤버 클래스와 중첩 클래스는 다른 것일까? 

    멤버 클래스와 중첩 클래스는 다른 것이다. 우선 중첩 클래스의 정의부터 해보자.

    중첩 클래스는 클래스 내부에서 선언된 클래스다. 

    클래스 내부에 선언된 중첩 클래스는 멤버 클래스는 아닌건가? 엄밀히 말하면 멤버 클래스도 있고, 멤버 클래스가 아닐 수도 있다. 이것은 멤버 변수 / 로컬 변수와 동일한 형태로 볼 수 있다. 

    예를 들어 변수가 클래스 Scope에 선언되어 있으면 멤버 변수라고 부른다. 반면 변수가 메서드 Scope에 선언되어 있으면 로컬변수라고 부른다. 마찬가지로 클래스가 클래스 Scope에 선언되어 있으면 멤버 클래스라고 부르고, 메서드 Scope에 선언되어 있으면 로컬 클래스라고 부른다.

    따라서 정리해보면 다음과 같다. 

    • 중첩 클래스 중 일부는 멤버 클래스가 된다. (정적 멤버 클래스 / 비정적 멤버 클래스)
    • 중첩 클래스 중 일부는 멤버 클래스가 아니다. (로컬 클래스 / 익명 클래스)

     


    정적 멤버 클래스

    아래 코드를 살펴보자. OuterClass 내부에는 InnerClass가 중첩 클래스로 선언되어있다. InnerClass는 클래스 Scope에 선언되어있고, static으로 선언되어있기 때문에 '정적 멤버 클래스'가 된다. 

    // ITEM24 : 정적 멤버 클래스
    public class OuterClass {
    
        private static int number = 10;
        
        // 정적 멤버 클래스
        static private class InnerClass {
            void doSomething() {
                System.out.println(number);
            }
        }
    
        public static void main(String[] args) {
            InnerClass innerClass = new InnerClass();
            innerClass.doSomething();
        }
    }
    

    정적 멤버 클래스의 특징은 무엇이 있을까?

    • 정적 필드에 접근할 수 있음. 
    • 바깥 클래스의 인스턴스를 필요로 하지 않는다. (묵시적 참조가 없음) 

    정적 멤버 클래스는 독립적으로 사용되기 보다는 Outter 클래스와 함께 사용될 때 유용하다. 그렇다면 정적 멤버 클래스는 언제 만드는 것이 좋을까? 우선은 비정적 멤버 클래스를 만들어본다. 비정적 멤버 클래스가 Outer 클래스의 인스턴스를 참조하지 않는다면, 그 때 정적 멤버 클랙스로 바꾸는 것이 좋다. 

     


    비정적 멤버 클래스

    비정적 멤버 클래스는 앞서 공부했던 것처럼 Outer 인스턴스에 대한 묵시적 참조가 생긴다. 이것은 Outer 인스턴스가 생성되어야지만, Outer 인스턴스를 통해 Inner 클래스를 생성할 수 있기 때문이다. 만약, 비정적 멤버 클래스의 인스턴스를 생성한다면 다음과 같이 생성해야한다.

    // Outer 클래스 먼저 생성 → inner 클래스 생성. 
    InnerClass innerClass = new OuterClass().new InnerClass();

    Inner 클래스가 비정적 멤버 클래스라면 생성할 때는 반드시 Outer 인스턴스를 생성한 후, 생성해야 된다. 따라서 Inner 클래스는 암묵적으로 생긴 Outter 인스턴스를 OuterClass.this로 참조할 수 있다. 아래에서 살펴볼 수 있듯이, 비정적 멤버 클래스는 Outer 인스턴스를 열심히 참조한다. 

    // ITEM24 : 비정적 멤버 클래스
    public class OuterClass {
    
        private int number = 10;
    
        void prinNumber() {
            // 주로 이런 형태로 OuterClass 내부에서 생성한 후 작업한다.
            InnerClass innerClass = new InnerClass();
        }
    
        // 비정적 멤버 클래스
        private class InnerClass {
            void doSomething() {
                System.out.println(number);
                // 비정적 멤버 클래스는 바깥 클래스 생성 후, 생성되기 때문에 묵시적 참조 가능
                OuterClass.this.prinNumber();
            }
        }
    
        public static void main(String[] args) {
            // 주로 이렇게는 사용하지 않음.
            InnerClass innerClass = new OuterClass().new InnerClass();
            innerClass.doSomething();
        }
    }

    비정적 멤버 클래스가 적합한 경우는 내부 클래스가 바깥 클래스의 인스턴스를 직접 참조하는 경우가 많을 때다. 이것은 비정적 멤버 클래스가 바깥 클래스의 인스턴스를 참조하지 않는다면, 비정적 멤버 클래스를 사용할 필요가 없다는 것이다. 왜냐하면 비정적 멤버 클래스는 그 특성상 불필요한 Outer 인스턴스를 생성하고, 내부적으로 참조하기 때문이다. 

     

    비정적 멤버 클래스의 주요 용도 → 어댑터로 사용

    비정적 멤버 클래스는 Adatper 패턴에 유용하다. 우리가 이미 구현한 클래스가 있고, 클라이언트가 원하는 클래스가 있다고 가정해보자. 이 때, 구현한 클래스와 클라이언트가 원하는 클래스가 타입 캐스팅이 되지 않는 경우가 있다. 이 때, 구현 클래스에 비정적 멤버 클래스를 하나 생성해두고 Adapting 하는 기능을 넣을 수 있다. 

    public class MySet<E> extends AbstractSet<E> {
        @Override
        public Iterator<E> iterator() {
            // MySet은 Iterator가 없다. --> 사용자는 Iterator를 사용하고 싶어함. 
            // Adapter 패턴을 이용해서 처리할 수 있음.
            return new MyIterator();
        }
        
        @Override
        public int size() {
            return 0;
        }
    
        private class MyIterator implements Iterator<E> {
            @Override
            public boolean hasNext() {
                return false;
            }
    
            @Override
            public E next() {
                return null;
            }
        }
    }

    자세한 내용은 다음 완벽공략에서 정리한다. (링크 추가) 


    익명 클래스

    익명 클래스는 메서드 Scope에 선언 및 생성되는 클래스다. 익명 클래스는 다음 특징을 가진다.

    • 정의함과 동시에 인스턴스가 생성됨. 
    • 생성된 클래스의 이름은 없다. 

    익명 클래스는 자바 8에서 람다가 추가되기 전에는 굉장히 많이 사용되었다. 그렇지만 람다 / 메서드 레퍼런스가 등장하면서 익명 클래스 대신 좀 더 간결한 람다 / 메서드 레퍼런스로 대체되고 있다. 

    public class IntArrays {
        static List<Integer> intArrayAsList(int[] a) {
            Objects.requireNonNull(a);
            
            
            // 익명 클래스
            // intArrayAsList() 메서드 Scope에 정의되고, 동시에 인스턴스가 생성됨. 
            return new AbstractList<>() {
                @Override
                public Integer get(int index) {
                    return a[index];
                }
    
                @Override
                public int size() {
                    return a.length;
                }
            };
        }
    }

    로컬 클래스

    로컬 클래스는 사실상 사용되지 않는다고 보면 된다. 아래와 같이 메서드 Scope에서 클래스가 선언되고, 그 클래스를 구현한다. 사용하지 않는 이유는 재사용성도 떨어지고, 메서드의 가독성 역시 나빠지기 때문이다. 

    public class MyClass {
    
        private int number = 10;
        
        void doSomething() {
            
            // 로컬 클래스 선언 
            class LocalClass {
                private void printNumber() {
                    System.out.println(number);
                }
            }
    
            LocalClass localClass = new LocalClass();
            localClass.printNumber();
        }
    
        public static void main(String[] args) {
            MyClass myClass = new MyClass();
            myClass.doSomething();
        }
    }

     

     

     

    정리

    멤버 클래스가 바깥 클래스의 인스턴스를 참조한다면 비정적 멤버 클래스로 작성한다. 만약 멤버 클래스가 바깥 클래스의 인스턴스를 참조하지 않는다면 비정적 멤버 클래스로 만들어서 좀 더 효율적으로 사용한다. 

     

     

    2

    댓글

    Designed by JB FACTORY