내부 클래스는 왜 static 으로 선언할까?

Patrick YOO·2022년 1월 2일
1
post-thumbnail

img

이너클래스 선언시 위와같이 static 으로 선언해 달라는 IDEA의 잔소리를 한번쯤 봤을것입니다.
Nested class (내부 클래스) 에 대해 간략히 알아보는 시간을 가지며 왜 IDEA 는 내부 클래스 선언시
위와같은 잔소리를 하는것인지 알아보겠습니다.

내부 클래스의 장점

내부 클래스를 사용하는 이유는 ORACLE-java Tutorial 에 아주 간결하게 명시 되어있습니다.
From the Java Tutorial:


번역기 돌리실 여러분들을 위해 제가 좀 거들어 드리자면

  1. A와 B클래스 2개의 클래스가 존재할때 B클래스를 의존하는 다른 클래스가 존재 하지 않고 오로지 A 클래스 에서만 의존하고 있다면 서로 같은 클래스에 선언하여 패키지를 간소화 하는 장점이 있다.
    의존한다 라는말이 익숙하지 않으신 분들을 위해 -> A 가 B 를 의존한다 = A 클래스가 B 의 자원,기능 을 필요로 한다.

// A 가 B 를 의존하고있음. 이말은 즉슨 A 라는 클래스는 B라는 클래스 없이 작동하지 않음.
public class A{

  private B b;
  private String useB;
  
  public A(B b){
  this.b=b;
  }
  
  public void useB(){
     this.useB = b.whatever();
  }
}
  1. A와 B클래스 2개의 클래스가 존재할때 B 클래스 가 A 클래스의 private 멤버변수에 접근 할 경우
    B 클래스를 private class 로 선언하여 B를 캡슐화 할 수 있으며 A 클래스와 유동적인 자원 교환이 가능해집니다.

  2. 가독성과 유지보수성이 향상된다.... 클린코드의 치트키같은 말이니까 넘어가도 좋을것같습니다.

내부 클래스는 어떤종류가 있을까?

1.인스턴스 클래스

public class Outer{
	// 인스턴스 클래스	
    public class Inner{
    }
}

2.스테틱 클래스

public class Outer{
	// 스테틱 클래스	
    static public class Inner{
    }
}
  1. 로컬 클래스
public class A{
	// 메소드 내부에 생성된 클래스
    public void localClass(){
    	    class B{
              public String abc;
            }
        B b = new B();
        b.abc = "localClass";
    }
    
}
  1. 익명 클래스
public interface A{
    void anonymous();
}

public class Main{
  
  public satic void main(String[]args){
    // 익명클래스
  	A a = new A(){
	 @override
	 void anonymous(){
           System.out.println("익명클래스");
        };
     }
  }
}

위의 4가지 Innerclass 종류 중 이번 포스팅의 뜨거운 감자인 인스턴스 클래스와 스테틱 클래스는 어떤것이 다른지 자세히 살펴보겠습니다.

  1. 인스턴스 클래스는 외부 클래스의 인스턴스화 없이 내부 클래스에 접근이 불가능하다.
public class Outer{
	public class Inner{
    }
}

public 

2. 인스턴스 내부 클래스는 외부 클래스를 참조하는 변수를 선언하지 않아도 바이트 코드 변환시 자동으로 외부를 참조하는 변수를 만든다.

오늘 이 포스팅을 작성한 핵심 내용이기 때문에 색깔을 넣어보았습니다.

스텝바이 스텝으로 따라가 보겠습니다.

public class OuterClass {
    int o = 10;
    class Inner {
        int i = 20;
    }
}

현재 Inner 라는 클래스는 인스턴스 클래스로 선언이 되어있습니다. 이를 클래스 파일로 컴파일 후 바이트 코드를 확인 해보면
위 사진처럼 InnerClass 가 OuterClass 를 참조하고 있음을 알 수 있습니다. (this$0 -> OuterClass 를 참조하고있는 바이트 코드에서만 보여 Hidden 변수)

위 차이점 덕에 IDE 애플리케이션에서 내부에서 외부를 참조하지 않을경우 static 클래스로 선언 하라는 잔소를
했음을 알 수 있습니다. 이또한 차근차근 짚어나가 보겠습니다.

static 으로 선언 해야 하는 이유.

  1. GC 의 수거대상을 벗어날 수 있습니다.
  • 아래 코드를 살펴보겠습니다.
    @Test
    void leak(){
        ArrayList<EnclosedClass> al = new ArrayList<>();
        int counter = 0;
        while (20>counter)
        {
            al.add(new EnclosingClass(100000000).getEnclosedClassObject());
            System.out.println(counter++);
        }
    }
    
    public class EnclosingClass {

    private int[] data;

    public EnclosingClass(int size)
    {data = new int[size];}

    class EnclosedClass{}
    
    EnclosedClass getEnclosedClassObject()
    {
        return new EnclosedClass();
    }
}
    

<<<실행 결과>>>

펑~🎇

위같은 결과가 나온 이유는 GC 가 정상적으로 Unreachable 한 데이터를 수거해가지 못했기때문에 생긴 문제 입니다.

위 코드에서 ArrayList 는 InnerClass 에 대한 데이터만 저장하기 때문에 외부 OuterClass 즉
new int[100000000] 의 데이터를 갖고 있는 클래스는 힙영역에서 GC 의 대상이 되어 수거가 되었어야 합니다.
하지만 내부클래스에서 외부클래스를 참조하고 있는 관계가 형성되어 GC 는 엄청난 데이터를 머금고 있는 외부 클래스를 GC의 대상으로 보지 않고 힙영역에 계속 쌓게 됩니다.

결론-> Heap 영역 퍼버벙~

정상적으로 외부 클래스가 더이상 참조 되지 않았을때 GC 의 소거 대상이 되기 위해선 둘의 참조관계를 끊어야합니다.

  • 방법은 위에 명시되어있듯이 static 만 선언해주면 됩니다.
public class OuterClass {
    int o = 10;
    static class Inner {
        int i = 20;
    }
}

위 코드를 컴파일 했을경우 아래 .class 바이트 코드는 아래와 같습니다.

static 키워드만 붙였을 뿐인데 this$0 의 히든변수가 사라졌고 더이상 외부 클래스를 참조하지 않습니다.

위에 메모리 Leak 문제를 야기 시켰던 코드 내부 클래스에 static 클래스로 선언한후 다시 돌려보면

    @Test
    void leak(){
        ArrayList<EnclosedClass> al = new ArrayList<>();
        int counter = 0;
        while (20>counter)
        {
            al.add(new EnclosingClass(100000000).getEnclosedClassObject());
            System.out.println(counter++);
        }
    }
    
    public class EnclosingClass {

    private int[] data;

    public EnclosingClass(int size)
    {data = new int[size];}

    static class EnclosedClass{}
    
    EnclosedClass getEnclosedClassObject()
    {
        return new EnclosedClass();
    }
}
    

더이상 메모리 Leak 문제를 야기시키지 않습니다. 외부에 어마어마한 데이터를 머금고 있는 인스턴스가 GC 의 대상이 되었기 때문입니다.

결론.

내부클래스가 외부클래스를 참조하지 않는다면 인스턴스 클래스보다는 static 클래스로 선언하자.

reference:
https://www.infoworld.com/article/3526554/avoid-memory-leaks-in-inner-classes.html
https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html
https://tecoble.techcourse.co.kr/post/2020-11-05-nested-class/



profile
자유인을 꿈꾸는 개발자

0개의 댓글