공부내용공유

java inner 클래스 static? non-static? (이펙티브 자바 item 24) 본문

Server/Java

java inner 클래스 static? non-static? (이펙티브 자바 item 24)

forfun 2024. 7. 14. 21:57

서론


 

아주 예전에 자바 공부를 할 때 'inner 클래스는 비정적으로 사용하면 메모리 누수가 일어나니 조심하자' 라는 내용의 블로그 글을 본적이 있었고 어렴풋하게 내 머리에 남아있었다.

 

그러던 도중 이펙티브 자바 item 24. 멤버 클래스는 되도록 static으로 만들어라 라는 주제에 대해 스터디를 하면서 비정적 멤버 클래스(non-static) , 정적 멤버 클래스(static)의 차이와 어떤 때 사용하게 되는지 알게 되었고 해당 글에서 간단히 정리할 예정이다.

 

 

본론


이 글은 자바의 멤버 클래스 (inner class)와 static에 대한 기본적인 이해가 있다는 가정하에 작성되어졌다.

 

목차는

  • 비정적 멤버 클래스를 사용했을 때 생길 수 있는 문제점
  • HashMap은 이걸 사용한다.
  • 문제가 있을 수 있나?

로 이루어질 예정이다.

 

 

비정적 멤버 클래스를 사용했을 때 생길 수 있는 문제점

 

일단 이전에 블로그 글에서 봤던 메모리 누수 문제는 어떻게 발생하는 것일까?

public class Member {
    private int age;
    private String name;
    private String id;
    private String [] items;

    public Member(int age, String name, String id, int size) {
        this.age = age;
        this.name = name;
        this.id = id;
        this.items = new String [size];
    }

    public class Job {
        private String jobName;
        private int rank;

        public Job(String name, int rank) {
            this.name = name;
            this.rank = rank;
        }
    }

    pulbic Job getJob() {
        return new Job(RandomJob.getJob(), RankUtil.getRankByAge(this.age));
    }
}

이런식으로 외부 클래스와 멤버 클래스가 있다고 할 때 현재 코드에서는 확인할 수 없지만 컴파일된 코드에서는 내부 클래스 생성자에 외부 클래스가 변수로 들어가게된다.

 

 

즉, 내부 클래스가 외부 클래스를 참조한다는 것이다. 이게 어떤 문제를 유발할 수 있을까?? 해당 실험은 내가 예전에 봤던 블로그에서 실험을 굉장히 잘 정리해 주셨는데 내 예시로 똑같이 실험을 한다고 하면

 

ArrayList<Object> list = new ArrayList<>();
for(int i = 0; i < 100; i++) {
    list.add(new Member(14,"name","id",1000000).);
}

 

대략 이런식으로 멤버의 job 배열을 만든다고 해보자, 우리는 현재 job 정보만 필요하기에 job 정보만 list에 넣고있고 큰 데이터를 가지고 있는 멤버 데이터는 gc에 의해 삭제될거라 생각한다.

 

 

하지만 아까 위에서 언급한 것처럼 Job은 Member를 참조하고 있기 때문에 Member 인스턴스들은 힙 메모리에 누적되고 (메모리 누수) oom이 터질 수 있다.

 

 

그러면 비정적 멤버 클래스는 절대 사용하면 안되는걸까??

 

 

HashMap은 이걸 사용한다.

final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

        public Object[] toArray() {
            return keysToArray(new Object[size]);
        }

        public <T> T[] toArray(T[] a) {
            return keysToArray(prepareArray(a));
        }

        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

 

HashMap 클래스에서 지원하는 KeySet이라는 멤버 클래스이다, 그리고 코드에서 확인할 수 있듯이 비정적 클래스이다.

 

웬만하면 멤버 클래스는 비정적으로 만들라고 했는데 왜 이렇게 작성했을까? 언제 이렇게 사용해야할까? 이펙티브 자바에서는 어댑터 패턴, 즉 어떠한 클래스에서 다른 타입의 인스턴스를 리턴하기 위해 사용한다고 나와있다.

 

HashMap에서도 key에 대해 Set 클래스의 인터페이스를 제공하기 위해 해당 클래스를 비정적으로 선언해 놓은것이다.

 

코드에서 보면 알 수 있듯이 실제로 Set이라는 자료구조를 만들지 않는다. HashMap에 존재하는 내부 자료구조를 통해 인터페이스만 지원하고 remove, size, clear 등은 있지만 add와 같은 인터페이스는 존재하지 않음을 볼 수 있다.

 

문제가 있을 수 있나?

 

결국 keySet은 HashMap을 사용하면서 좀 더 편한 사용을 위해 제공하는 인터페이스이기에 keySet만 필요한 상황이 드물것이다, 하지만 만약 hashMap을 다 사용하고 keySet만 필요하다.

 

그리고 HashMap의 value 데이터 크기가 큰편이라고 하면 단순히 KeySet으로 Set만 사용하니 GC가 처리해주겠지가 아니라 key 데이터만 실제 Set에 옮겨 닮으면 좀 더 메모리를 아낄 수 있지 않을까라고 생각한다.

 

 

결론


스터디를 통해 애매하게 알던 개념을 잘 정리하게 되었다. 또 실무에서 잘 안사용하는 문법이라 생각했는데 이렇게 자주 사용하는 라이브러리를 보니 중요성을 새삼 느끼게 되었다.