중첩, 내부 클래스 / 메모리 누수의 위험성?

maketheworldwise·2022년 1월 23일
0


이 글의 목적?

그 동안 프로젝트를 진행할 때 내부 클래스를 이용해 데이터를 전달해왔는데, IDE에서 Warning을 띄워주기도 했고 메모리 누수의 위험이 있다는 글을 보고 이 부분에 대해서는 간단하게라도 이해하고 넘어가야 한다고 생각했다.

이번 글에서는 간단하게 위험한 부분에 대해서만 정리해보고 추후에 더 자세히 정리해보도록하자.

중첩 클래스

중첩 클래스는 쉽게 말하자면 클래스 내부에 정의된 클래스를 의미한다. 중첩 클래스는 특정 클래스가 한 곳에서만 사용될 때 논리적으로 군집화하기 위해 사용한다. 불필요한 노출을 줄이면서 캡슐화를 할 수 있고 가독성과 유지 보수하기 좋은 코드를 작성하는데 장점을 가지고 있다.

출처: https://velog.io/@agugu95/%EC%99%9C-Inner-class%EC%97%90-Static%EC%9D%84-%EB%B6%99%EC%9D%B4%EB%8A%94%EA%B1%B0%EC%A7%80

정적 클래스인지 비정적 클래스인지를 나누는 기준은 static 예약어가 붙어있는지 안붙어있는지로 판단할 수 있다.

비정적(내부) 클래스

  • 외부 클래스의 인스턴스와 암묵적으로 연결된다.
  • this 예약어로 외부 클래스의 인스턴스를 호출할 수 있다.
  • 외부 인스턴스 없이는 생성할 수 없다.
  • 어댑터를 정의할 때 자주 사용한다.

(가장 중요한 점은 비정적 클래스는 외부 인스턴스에 대한 참조가 유지되는 것이다! 👊)

public class Outer {
	private int a;
    
	class Inner {
    	private int b;
    }
}
// 1
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

// 2
Outer.Inner inner = new Outer().new Inner();

지역 클래스

  • 지역 변수를 선언할 수 있는 곳이면 어디든 선언이 가능하다.
  • 스코프도 지역 변수와 동일하다.
  • 이름이 있다.
  • static 멤버는 가질 수 없다.
  • 익명 클래스가 여러 번 쓰일 경우 지역 클래스로 만드는 편이 좋다.
public class Outer {
	void outerPrint() {
    	class Local {
        	void localPrint() {
            	System.out.println("Local");
            }
        }
    }
}

익명 클래스

  • 외부 클래스의 멤버가 아니다.
  • 선언과 동시에 인스턴스가 만들어진다.
  • instanceof나 클래스의 이름이 필요한 작업은 수행할 수 없다.
  • 여러 인터페이스를 구현할 수 있다.
  • 인터페이스 구현과 상속을 동시에 할 수 없다.
  • 10줄 이하가 권장된다.
  • 정적 팩터리 메서드를 구현할 때 사용된다.
interface AnonymousInterface {
	void print();
}

public class Outer {
	int a = 10;

	void anonymous {
    	new AnonymousInterface() {
          @Override
          public void print() {
          	System.out.println(a);
          }
        }
    }
}

정적 클래스

  • 다른 클래스 내부에 선언되고, 외부 클래스의 private 멤버에도 접근할 수 있다.
  • 외부 인스턴스와 독립적으로 존재할 수 있다.
  • 외부 인스턴스 맴버의 직접 참조가 불가능하다.
public class Outer {
	private int a;
    
	static class Inner {
    	private int b;
    }
}
Outer.Inner inner = new Inner();

직접 확인해보자

// Outer.class
public class Outer {
    private int out;
    
    public static class StaticInner {
        private int in;
    }
    
    public class NonStaticInner {
        private int in;
    }
}

먼저 정적 내부 클래스부터 컴파일해서 class 파일로 변환한 뒤 javap -p 명령어로 Disassembler 결과를 확인해보자. 결과로는 멤버 변수와 기본 생성자에 대한 참조를 가지고 있는 것을 확인할 수 있다.

$ javap -p out.production.study-java-test.Outer\$StaticInner
Compiled from "Outer.java"
public class Outer$StaticInner {
  private int in;
  public Outer$StaticInner();
}

반대로 비정적 내부 클래스를 컴파일해서 확인해보면, 멤버변수와 기본 생성자를 비롯한 외부 클래스까지 참조하고 있는 것을 확인할 수 있다.

$ javap -p out.production.study-java-test.Outer\$NonStaticInner      
Compiled from "Outer.java"
public class Outer$NonStaticInner {
  private int in;
  final Outer this$0; // 여기!!
  public Outer$NonStaticInner(Outer);
}

정리해보자

자바에서 객체가 삭제되는 시점은 객체가 더 이상 사용되지 않을 때다. 하지만 위에서 확인했듯이, 내부 클래스는 외부 클래스를 항상 참조하고 있다. 따라서 외부 클래스가 삭제되더라도 내부 클래스가 살아있게 되어 메모리 누수가 발생하게 된다.

결국, 정적 내부 클래스로 선언하게 된다면 메모리 누수의 원인을 예방하고 클래스의 각 인스턴스당 더 적은 메모리를 사용하기 때문에 외부 인스턴스에 대한 참조가 필요하지 않다면, 정적 중첩 클래스로 만드는 것이 좋다. 반대로 비정적 클래스는 어댑터 패턴을 이용하여 외부 클래스를 다른 클래스로 제공할 때 사용하면 좋다.

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글