static inner vs non-static inner class

이주오·2022년 2월 3일
0

java

목록 보기
6/6

프로젝트를 진행하며 리턴하는 api의 요청 혹은 응답의 DTO 클래스들은 데이터의 형식에 따라 많은 수가 필요하곤 했는데 재사용성을 줄이는 대신 관리적인 측면을 위해서 dto 클래스를 매번 만들지 않고 static inner 클래스로 사용함으로써 하나의 dto만으로 완전히 관리될 수 있도록 사용하도록 컨벤션을 정하였다. 프로젝트가 끝난 후 조금 더 깊이 정리하기 위해 중첩 클래스(Nested Class)는 무엇이고 4가지 종류 중 정적 내부 클래스비정적 내부 클래스에 대해 다뤄보고자 한다.


중첩 클래스란(Nested Class)

먼저 중첩 클래스에 대해 설명을 해야하는데, 중첩 클래스란 말 그대로 다른 클래스의 내부에 존재하는 클래스를 의미한다. 중첩클래스를 포함하는 외부 클래스를 Outer class라고 하며 내부에 포함된 클래스를 nested class 또는 Inner class라고 한다.

중첩 클래스는 4가지 종류가 존재한다.

  • 정적 멤버 클래스(static inner class) : static 키워드를 이용해서 클래스가 정의된 경우
  • 비정적 멤버 클래스(non-static inner class) : Outer 클래스의 멤버변수나 메소드처럼 클래스가 정의된 경우
  • 익명 클래스(anonymous class) : 익명 클래스를 이용해서 클래스가 정의된 경우
  • 지역 클래스(local class) : Outer 클래스의 특정 메소드 또는 초기화 블록에서 클래스가 정의된 경우
class Outer {
		class InstanceInner {} // non static inner class
		static class StaticInner {}  // static inner class

		void myMethod() {      // local class
				class LocalInner {}
		}

		// anonymous class
    Ex ex = new Ex() { 
      public void getEx() {
        System.out.println("anonymous class");
      }
    };
  }
}

interface Ex {
  public void getEx();
}

중첩 클래스는 특정 클래스를 자신의 클래스 내부적인 용도로만 사용하고자 할때 효율적이기 때문에 불필요한 노출을 줄이면서 캡슐화를 통해 유지 보수하기 좋은 코드를 작성하게 된다.

이펙티브 자바에서는 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱 레벨 클래스로 만들어야 한다고 권장하고 있으며 특히 static 으로 선언하도록 권장한다. (아이템 24)

그렇다면 왜인지 알아보자!!


정적 멤버 클래스

  • static이 붙는 중첩 클래스
  • 동일한 static 멤버들을 사용 가능
  • static의 특징에 따라 외부 인스턴스 멤버의 직접참조가 불가능

static 예약어가 있음으로 인해 독립적으로 생성할 수 있다.

정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고 일반 클래스와 쓰임새는 동일하다.

class Outer {
    static int x = 10;
    int y = 20;
    private static int z = 30;

    static class StaticInner {  // static inner class
        void get() {
            System.out.println("x: " + x);
            System.out.println("z: " + z);
        }
    }
}
  
Outer.StaticInner staticIneer = new Outer.StaticInner();
staticIneer.get();


비정적 멤버 클래스

  • Inner class라고 하며 외부 인스턴스에 대한 참조가 유지된다.
  • 외부 인스턴스는 내부 클래스를 new를 통한 인스턴스 할당으로 멤버변수처럼 사용할 수 있다.
  • 외부에 대한 참조가 유지되므로 내부 클래스도 외부 클래스의 자원을 사용할 수 있다.
class Outer {

    static int x = 10;
    int y = 20;
    public int z = 30;
    
    class InstanceInner {
        void get() {
            System.out.println("x: " + x);
            System.out.println("y: " + y);
            System.out.println("z: " + z);
        }
    }
}

Outer outer = new Outer();
Outer.InstanceInner insttanceInner = outer.new InstanceInner();
insttanceInner.get();

비정적 내부 클래스를 생성하는 경우에는 반드시 Outer 객체를 생성한 뒤 객체를 이용해서 생성해야 한다. 즉, 비정적 내부 클래스는 Outer 클래스에 대한 참조가 필요하다는 것이다.


그렇다면 왜 멤버 클래스는 static으로 선언하기를 권장할까?

바로 Outer 객체에 대한 참조 때문이다.

위의 예제처럼 InstanceInner 와 같은 중첩 클래스를 선언하면 인스펙터가 다음과 같이 경고를 해준다.

경고 주제는 메모리 누수 가능성이 있기 때문이다.


인용: 이펙티브 자바

정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결되기 때문에 바깥 클래스는 더 이상 사용되지 않지만 내부 클래스의 참조로 인해 GC가 수거하지 못해서 바깥 클래스의 메모리 해제를 하지 못하는 경우가 발생할 수 있기 때문이다. 이 문제는 IDE에서 경고해주기 때문에 흔하게 볼 수 있으며, 조슈아 블로흐가 이펙티브 자바에서도 강조하고 있는 내용이기도 하다.

비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경할 수 없다. 이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는 게 보통이지만, 드물게는 직접 바깥 인스턴스의 클래스.new MemberClass(args)를 호출해 수동으로 만들기도 한다. 예상할 수 있듯, 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. 앞서도 얘기했듯 이 참조를 저장하려면 시간과 공간이 소비된다. 더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다(아이템 7). 참조가 눈에 보이지 않으니 문제의 원인을 찾기 어려워 때때로 심각한 상황을 초래하기도 한다.


결론

  • static이 아닌 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.
  • 왜냐하면 static이 아닌 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문이다.
  • 두 클래스의 관계는 멤버 클래스의 인스턴스 안에 만들어지며, 메모리를 차지한다. 생성도 느리다.
  • 바깥 클래스 인스턴스의 참조를 멤버 클래스가 갖고 있으므로, 바깥 클래스 인스턴스가 쓰레기 수거 대상에서 빠지게 된다.
  • 이는 메모리 누수를 불러일으킬 수 있는 치명적인 위험요소

결국 외부 인스턴스에 대한 참조가 필요하지 않고 내부 클래스가 독립적으로 사용된다면 static nested class로 만드는 것이 낫다.


참고 출처

profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글