JAVA & Spring & JPA

[JAVA] 중첩 클래스, 중첩 클래스의 문단속

멈재 2023. 4. 29. 21:16
728x90

간단한 요청 응답 객체를 만든다거나, 여러 가지 이유로 중첩 클래스를 쓸 때가 있다. 
자바의 중첩 클래스로 여러 종류의 중첩 클래스가 존재하지만, 여기에서는 정적 중첩 클래스와 내부 클래스를 중점적으로 알아보려 한다.
 
 

중첩 클래스란

들어가기에 앞서,
중첩 클래스??
내부 클래스??
이름 그대로 뭔가 겹쳐서 사용하는 클래스인 것 같다.
 
중첩 클래스는 어떤 클래스의 내부에 존재하는 클래스를 의미한다.
중첩 클래스는 컴파일되면 바깥클래스$안쪽클래스의 형태로 컴파일된 클래스 파일이 생성되게 된다.

class Outer {     // 바깥 클래스
    class Inner { // 안쪽 클래스
        ...
    }
}
🖐️ 
외부 클래스와 내부 클래스로 이름 짓는것이 명확하지만 중첩 클래스의 종류로 내부 클래스가 존재하기 때문에 앞으로 바깥 클래스와 안쪽 클래스를 구분 지어야 하는 경우에는 바깥 클래스와 안쪽 클래스라는 용어를 사용할 예정입니다.

 
 

정적 중첩 클래스와 내부 클래스는 어떤 형태일까

내부 클래스는 비정적 중첩 클래스라고도 불리우는데 정적(static)이라는 이름에서 알 수 있듯 안쪽 클래스에 static이 붙어 있냐, 붙어있지 않냐에 따라 정적 중첩 클래스와 비정적 중첩 클래스로 구분된다.
 
정적 중첩 클래스(static nested class)

class Outer {
    static class StaticInner {
    }
}

 
비정적 중첩 클래스(non-static nested class)

class Outer {
    class NonStaticInner {
    }
}

이 둘의 차이는 명확하기 때문에 큰 어려움은 없을 것이다.
 
 

중첩 클래스는 언제 쓰이고, 왜 쓰이는걸까

일반적으로 중첩 클래스는 다른 클래스와 협력할 일이 적고, 바깥 클래스와 밀접한 관련이 있는 경우에 쓰인다. 이렇게 되면 하나의 논리적인 그룹으로 나타낼 수 있기 때문에 하나의 클래스에서 두 개의 클래스를 한 번에 관리할 수 있게 된다.
 
예를 들어, 안쪽 클래스인 B가 오직 A 클래스에서만 사용되는 경우가 중첩 클래스를 쓰기 적절한 예시가 된다.

class A {     // 바깥 클래스
    class B { // 안쪽 클래스
        ...
    }
}

 
따라서 중첩 클래스를 사용함으로써 다음과 같은 이점을 얻게 된다.

  1. 외부 클래스와 내부 클래스가 서로의 멤버에 쉽게 접근할 수 있게 된다.
  2. 서로 관련있는 클래스를 논리적으로 묶어서 표현하기 때문에 캡슐화를 향상 시킬 수 있다.
  3. 연관있는 클래스가 하나의 클래스로 묶이기 때문에 가독성이 좋아지고, 유지보수하기 쉬워진다.

 
 

정적 중첩 클래스와 비정적 중첩 클래스 인스턴스 생성

두 중첩 클래스는 static의 존재 여부에 따라 달라지는데 서로 인스턴스 생성 방식이 다르다.
 
정적 중첩 클래스
static 키워드가 있음으로써 바깥 클래스를 참조하지 않고도 독립적으로 생성할 수 있다.

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

 
비정적 중첩 클래스
반면 비정적 중첩 클래스를 생성하기 위해서는 먼저 바깥 클래스를 초기화한 뒤 바깥 클래스의 참조를 이용해서 안쪽 클래스 인스턴스를 생성해야 한다.

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

즉 비정적 중첩 클래스의 인스턴스를 생성하기 위해서는 반드시 바깥 클래스의 참조가 필요하다.
 
얼핏보기에 인스턴스 생성의 차이만 있을 뿐, 별 다른 문제가 없는 것처럼 보인다.
사실은 비정적 중첩 클래스(내부 클래스)는 문제를 가지고 있다.
 
 
인텔리제이와 같은 IDE에서 (외부 클래스의 멤버를 사용하지 않고) 비정적 중첩 클래스를 선언하면 다음과 같이 경고 메시지를 보여준다. 그와 동시에 내부 클래스를 static으로 설정하라는 수정 제안을 보여준다.

왜 이런 경고 메시지를 보내는 것일까
 
 

내부 클래스는 메모리 누수를 발생시킨다.

그 이유는 비정적 중첩 클래스가 외부 참조를 하기 때문이다.
 
비정적 중첩 클래스의 인스턴스를 생성할 때 반드시 바깥 클래스의 참조가 필요했다.
이 과정으로 인해 비정적 중첩 클래스는 자신을 만들어 준 바깥 클래스에 대한 외부 참조를 가져야 한다.

public class InnerExample {
    public static void main(String[] args) {
        Outer.NonStaticInner nonStaticInner = new Outer().new NonStaticInner();
    }
}

class Outer {
    private int out = 10;

    class NonStaticInner { // 내부 클래스
        private int in = 20;
    }
}

 
컴파일되어 생성된 비정적 중첩 클래스인 Outer$NonStaticInner 파일을 인텔리제이의 디컴파일러를 이용해서 확인해보면 다음과 같은 결과를 볼 수 있다.

class Outer$NonStaticInner {
    private int in;

    Outer$NonStaticInner(Outer this$0) {
        this.this$0 = this$0;
        this.in = 20;
    }
}

여기서 우리가 흔히 아는 this와는 다른 this$0이 존재하는 것을 볼 수 있는데, this$0은 바이트 코드에서만 보여지는 바깥 클래스의 숨겨진 참조 변수이다. 그리고, this$0는 '정규화된 this'라고 불리우는데 클래스명.this 형태로 클래스의 이름을 명시하는 문법을 의미한다.
 
분명 예시 코드에서 비정적 중첩 클래스에 별도의 생성자를 생성해주지 않았다.
그런데, 비정적 중첩 클래스인 NonStaticInner 클래스의 생성자가 바깥 클래스의 참조를 매개변수로 받아서 인스턴스 변수(this)로 저장하는 것을 볼 수 있다.
따라서 내부 클래스는 별도의 설정없이 외부 클래스의 참조를 가진다는 것을 알 수 있다.
 
그렇다면
이 정규화된 this(this$0)로 외부 클래스에 접근이 가능할까? 
가능하다.
 

public class InnerExample {
    public static void main(String[] args) {
        Outer.NonStaticInner nonStaticInner = new Outer().new NonStaticInner();
        nonStaticInner.printOuterField();
    }
}

class Outer {
    private int out = 10;

    class NonStaticInner { // 비정적 중첩 클래스
        private int in = 20;

        void printOuterField() {
            System.out.println("Outer.out = " + Outer.this.out);
        }
    }
}

 
이 코드를 실행시키게 되면 컴파일 에러없이 다음의 실행 결과를 확인할 수 있다.

Outer.out = 10

이처럼 내부 클래스의 인스턴스는 외부 클래스의 인스턴스와 연결되어 있는 것을 알 수 있다.
 
하지만,
내부 클래스는 위와 같은 기능적인 장점이 존재하지만, '외부 참조'로 인해 메모리 누수가 발생한다는 문제가 존재한다.
 
만약, 외부 클래스는 더 이상 사용되지 않고 내부 클래스만 남아있다면 외부 클래스는 가비지 컬렉션(GC, Garbage Collection)의 대상이 되어 메모리에서 제거되어야 마땅하다.
그러나 내부 클래스는 바깥 클래스와 외부 참조로 이어져있기 때문에 메모리에서 제거되지 않고 남아있게 된다.
 
이는 곧 메모리 누수로 이어지게 되어 성능 저하를 일으키고, 심각한 경우에는 OutOfMemory Error로 프로그램이 종료될 수도 있다.

메모리 누수란 더 이상 사용되지 않는 객체들이 GC(가비지 컬렉션)에 의해 소멸되지 않고, 누적되는 현상을 의미한다.

 
JVM을 실시간으로 모니터링 가능하고, Heap Dump도 뜰 수 있는 GUI 툴인 VisualVM으로 알아보자.

VisualVM: Features

Features VisualVM monitors and troubleshoots applications running on Java 1.4+ from many vendors using various technologies including jvmstat, JMX, Serviceability Agent (SA) and Attach API. VisualVM perfectly fits all the requirements of application develo

visualvm.github.io

 
1) 5개의 비정적 중첩 클래스의 인스턴스를 생성하고,
2) 직접 GC를 실행시킨 다음,
3) System.in.read에서 멈춰있을 때 VisualVM으로 Heap Dump를 떠서
메모리 상태를 확인하는 과정으로 진행된다.
 
실행할 코드는 다음과 같다.

import java.io.IOException;

public class InnerExample {
    public static void main(String[] args) throws IOException {
        Outer.NonStaticInner nonStaticInner1 = createNonStaticInner();
        Outer.NonStaticInner nonStaticInner2 = createNonStaticInner();
        Outer.NonStaticInner nonStaticInner3 = createNonStaticInner();
        Outer.NonStaticInner nonStaticInner4 = createNonStaticInner();
        Outer.NonStaticInner nonStaticInner5 = createNonStaticInner();

        System.gc();

        System.out.println("GC 동작 완료");
        System.in.read(); // VisualVm HeapDump 시점

        System.out.println("nonStaticInner1 = " + nonStaticInner1);
        System.out.println("nonStaticInner2 = " + nonStaticInner2);
        System.out.println("nonStaticInner3 = " + nonStaticInner3);
        System.out.println("nonStaticInner4 = " + nonStaticInner4);
        System.out.println("nonStaticInner5 = " + nonStaticInner5);
    }

    private static Outer.NonStaticInner createNonStaticInner() {
        return new Outer().new NonStaticInner();
    }
}

class Outer {
    private int out = 10;

    class NonStaticInner { // 내부 클래스
        private int in = 20;
    }
}

 
System.in.read에 의해 멈춰있는 동안 메모리의 상태를 확인해보면 다음과 같다.

 
createNonStaticInner 메서드에서 바깥 클래스의 인스턴스가 내부 클래스 인스턴스를 생성하고 나서 더 이상 사용되지 않기 때문에 가비지 컬렉션의 대상이 되어 메모리를 반환 받길 기대했다. 그러나, GC를 했음에도 해제되지 않음을 알 수 있다.
왜냐하면, 내부 클래스의 경우 외부 클래스의 참조 값을 가지고 있기 때문에 GC가 Unreachable한 데이터를 수거해가지 못했기 때문에 정상적인 메모리 관리가 되지 않아서 메모리 누수가 발생한 것이다.
 
이처럼 가비지 컬렉터에 의해 회수되지 않고 메모리에 계속해서 누적되게 되면 Major GC가 빈번하게 발생하게 되면서 성능 저하가 발생하게 된다.
 
 
반면 정적 중첩 클래스의 경우에는 어떨까
 

정적 중첩 클래스는 메모리 누수가 발생하지 않는다.

정적 중첩 클래스는 비정적 중첩 클래스와 달리 바깥 클래스의 참조없이도 인스턴스 생성이 가능했다.

public class InnerExample {
    public static void main(String[] args) throws IOException {
        Outer.StaticInner staticInner = new Outer.StaticInner();
    }
}

class Outer {
    private int out = 10;

    static class StaticInner { // 정적 중첩 클래스
        private int in = 30;
    }
}

 
그렇다면 컴파일되어 생성된 중첩 클래스인 Outer$StaticInner 파일은 어떻게 컴파일 되었을까

class Outer$StaticInner {
    private int in = 30;

    Outer$StaticInner() {
    }
}

비정적 중첩 클래스와 달리 외부 참조를 하지 않는 것을 볼 수 있다.
또한, 외부 참조를 갖지 않기 때문에 '정규화된 this' 기능도 사용하지 못한다.

 
마찬가지로 인스턴스 생성만 바꿔서 정적 중첩 클래스를 생성하면 어떤 결과가 나올까

import java.io.IOException;

public class InnerExample {
    public static void main(String[] args) throws IOException {
        Outer.StaticInner staticInner1 = createStaticInner();
        Outer.StaticInner staticInner2 = createStaticInner();
        Outer.StaticInner staticInner3 = createStaticInner();
        Outer.StaticInner staticInner4 = createStaticInner();
        Outer.StaticInner staticInner5 = createStaticInner();

        System.gc();

        System.out.println("GC 동작 완료");
        System.in.read(); // VisualVm HeapDump 시점

        System.out.println("staticInner1 = " + staticInner1);
        System.out.println("staticInner2 = " + staticInner2);
        System.out.println("staticInner3 = " + staticInner3);
        System.out.println("staticInner4 = " + staticInner4);
        System.out.println("staticInner5 = " + staticInner5);
    }

    private static Outer.StaticInner createStaticInner() {
        return new Outer.StaticInner();
    }
}

class Outer {
    private int out = 10;

    static class StaticInner {
        private int in = 30;
    }
}

따라서 정적 중첩 클래스는 외부 참조가 필요하지 않기 때문에 바깥 클래스의 외부 참조를 가지지 않았고, 정적 중첩 클래스의 생성을 위해 사용된 바깥 클래스의 인스턴스는 아무곳에서도 사용되지 않아서 GC에 의해 정상적으로 수거되어 메모리가 할당 해제된 것을 확인할 수 있다.
 
결론
되도록이면 중첩 클래스는 static을 붙여 메모리 누수가 발생하지 않도록 문단속(?)하는 것을 권장한다.
 
 
 
스프링까지 이어가고 싶으시다면 토비님 영상도 추천드립니다!

https://youtu.be/2G41JMLh05U

 
 
최근에 이펙티브 자바 07. 다 쓴 객체 참조를 해제하라에서도 메모리 누수를 다뤘기 때문에 나로서는 크게 다가온 내용이었다. 다만, JVM 메모리 구조나 가비지 컬렉션 등에 대한 지식이 부족하다보니 딥한 원리까지는 이해하지 못했다.
 

지난번 싱글턴 패턴을 정리하고 나서 같은 코어 내 글또분이 달아주신 댓글에 대댓글로, 글또 활동이 끝나기전에 다루겠다고 나름의 선언(?)을 해두었는데, 이 포스팅을 발판삼아 틈틈이 시간내서 꼭 정리해야겠다는 생각이 들었다.
 
 
참고