Java & Spring

Nested Class

리차드 2023. 3. 26. 20:58

 

학습 계기


최근 업무중에 만난 코드에서 궁금증이 생겼었습니다.

Static Nested Class에 @Service 애너테이션이 적용되어 있고,
해당 클래스 내부에 @Transactional이 적용되어 있어서, 
왜 중첩클래스? 왜 static 선언? 이게 프록시가 적용되나? 등의 의문이 있었는데
마침 토비의 스프링 채널에 업로드된 Nested Class 관련 영상이 있어서 학습하고 정리해봤습니다.

 

 

 

 

용어 정리


  • Top-level Class vs Nested Class
  • Static Nested Class vs Non-static Nested Class (inner class)
  • Local Class
  • Anonymous Class

이러한 용어들에 대해 아직 정확히 구분하지 못하고 있었다는 걸 배웠네요.
관련하여 쉐도잉에 대해 표현만 들어보고 무슨 개념인지 모르고 있었는데 이번에 정확히 이해했습니다.

토비님이 추천해주신 오라클 튜토리얼이 확실히 도움이 된다는 걸 느꼈습니다.

주절주절 글로 나열하기 보다 코드로 보는 게 나을 것 같아 코드와 주석으로 내용을 남깁니다.

 

/**
 * Top-level Class
 * - 다른 클래스나 인터페이스 내부에 존재하지 않는다면 Top-level 클래스이다
 * - public 또는 default 접근제한자만 가능하다
 * - 자바 소스 파일 당 하나의 public top-level 클래스만 선언 가능하다
 * - default top-level 클래스는 여러 개 선언 가능하다
 * - static 선언이 불가하다
 */

/**
 * protected, private 접근제한자는 top-level 클래스의 접근제한자로 사용될 수 없다
 */
//protected class ProtectedTopLevelClass{}
//private class ProtectedTopLevelClass{}

/**
 자바 소스파일과 이름이 다른 public top-level 클래스는 컴파일 에러
 */
//public class SecondPublicTopLevelCalss {}

/**
접근제한자가 default인 top-level 클래스는 다수 선언 가능
*/
class DefaultTopLevelClass{}
class SecondDefaultTopLevelClass{}

/**
 inside TopLevelClass.java
 public Top-level class는 자바 소스파일 당 1개만 선언 가능
 */
public class TopLevelClass {
    private final AtomicLong id = new AtomicLong(0);
    private final String city = "seoul";

    /**
     Nested Class
     - 다른 클래스 내부에 선언되는 클래스를 Nested Class, 중첩 클래스 라고 한다
     - Nested 클래스는 이를 감싸는 바깥쪽 클래스의 멤버이다
     - Nested 클래스는 static nested class와 non-static nested class로 나뉜다
     - non-static nested class 는 inner class 라고도 부른다
     - top-level 클래스와 달리, protected, private 접근제한자도 선언 가능하다
     - 가독성, 유지보수성, 캡슐화, 한 곳에서만 사용되는 클래스들의 논리적 그룹화 등의 목적으로 사용할 수 있다
     */
    protected static class StaticNestedClass {
        /**
         Static Nested Class
         - static nested class는 바깥쪽 클래스의 멤버에 접근권한이 없다
         - 패키징 편의를 위해 다른 클래스 내부에 선언되었을 뿐, Top-level 클래스의 동일하게 인스턴스를 생성할 수 있다
         - 즉, non-static nested class와 달리, Top-level 클래스 인스턴스를 통하지 않고 직접 인스턴스를 생성할 수 있다
         - final var staticNestedClassInstance = new StaticNestedClass();
         - 바깥쪽 클래스의 non-static 필드에 static 참조는 불가하지만, object 참조는 가능하다.
         */
        // void printIdByStaticReference() {
        // System.out.println(id.getAndIncrement());
        // }

        void printIdByObjectReference(final TopLevelClass topLevelClass) {
            System.out.println("topLevelClass.id.getAndIncrement() = " + topLevelClass.id.getAndIncrement());
        }
    }
    private class NonStaticNestedClassWhichIsInnerClass{
        /**
         Non-Static Nested Class ( == Inner Class )
         - non-static nested class(inner class)는 바깥쪽 클래스의 멤버에 접근권한이 있다
         - JDK 16부터 non-static nested class에 static 멤버를 선언할 수 있게 됐다
         - Non-Static Nested Class 인스턴스를 생성하려면, 바깥쪽 클래스 인스턴스를 통해야 한다
         - final var innerClassInstance = new TopLevelClass().new NonStaticNestedClassWhichIsInnerClass();
         - non-static nested class(inner class)에는 Serialization을 사용하지 않는 게 강력히 권고된다
         */

        static String NAME = "Richard"; // static member in inner class is available since JDK 16
        private final String city = "busan";

        void printId() {
            System.out.println(id.getAndIncrement()); // inner class has access to its outer class' member
        }

        /**
         shadowing
         - 위에서 아래로 순서대로 각각 전달된 파라미터, inner class의 필드, enclosing class의 필드를 출력한다
         - 불필요한 혼란을 야기할 수 있으니 피하자
         */
        void printCity(final String city) {
            System.out.println("city = " + city);
            System.out.println("city = " + this.city);
            System.out.println("city = " + TopLevelClass.this.city);
        }
    }
}

 

 

 

 

스프링 빈 등록 여부


public class Outer {
    @Service
    public static class PublicStaticNestedClass { // Case 1
        @Service
        public class InnerClassInPublicStaticNestedClass {} // Case 2
    }
    @Service
    static class DefaultStaticNestedClass { // Case 3
        @Service
        public class InnerClassInDefaultStaticNestedClass {} // Case4
    }

    @Service
    public class PublicInnerClass{} // Case 5

    @Service
    class DefaultInnerClass {} // Case 6
}

@Service
class DefaultTopLevelClass {} // Case7

@Service
class PublicTopLevelClass {} // Case8

 

이하는 Outer 클래스가 스프링 빈으로 등록되지 않는 상황을 전제했습니다.

  • Case 1 : Public Static Nested Class : 스프링 빈으로 등록됩니다.
  • Case 2 : Case 1  내부에 선언된 Non-Static Nested Class, 즉 Inner Class 입니다. 스프링 빈으로 등록됩니다.
  • Case 3,4 : Case 1에서 public 선언만 default로 변경된 케이스입니다. 둘 다 스프링 빈으로 등록됩니다.
  • Case 5,6 : Inner class, 즉 non-static nested class 입니다. 스프링 빈으로 등록되지 않습니다.
    • Outer Class의 접근제한자가 public임에도, 스프링 빈이 아니므로 등록되지 않습니다.
  • Case 7,8 : 자바 소스파일과 이름이 다른 Top-level class입니다. 스프링 빈으로 등록됩니다.

 

사실 지금도 위 내용이 명쾌하게 해석이되진 않습니다만, 나름대로 해석해보자면 아래와 같습니다.

  • Case 1과 3은 static nested class 이므로, 별도의 자바 소스파일에 선언한 것과 동일하다
    • 따라서 그 자신도 스프링 빈으로 등록되고, 뒤이어 Case 2,4도 스프링 빈으로 등록된다.
  • Case 5,6은 Outer Class가 스프링 빈으로 등록되지 않았기에 스프링 빈으로 등록되지 않는다.
    • Outer가 public이기에 외부에서 Case 5,6 모두 접근 및 인스턴스 생성이 가능하다.
    • 그러나 Outer가 스프링 빈으로 등록되지 않는다면 Case 5,6도 등록되지 않는다.
    • Outer에 @Service 선언 등을 사용해 스프링 빈으로 등록한다면, Case 5,6도 등록된다.
  • Case 7,8은 외부 자바 소스파일에 선언한 것과 동일하다. 스프링 빈으로 등록된다.

컴파일 된 결과를 보면, 자바 소스파일과 이름이 다른 Top-level 클래스는
별도의 class 파일로 생성되었음을 알 수 있습니다.

 

 

 

 

정리


  • 자바 클래스는 Top-level 클래스와 Nested 클래스로 나뉜다
  • Nested 클래스에는 static nested class와 non-static nested class, local class, anonymouse class가 있다.
  • non-static nested class를 inner class라고도 한다
  • nested 클래스를 사용하는 이유는 캡슐화, 논리적 그룹화, 유지보수성, 가독성 등의 이유가 있다.
  • nested 클래스를 사용하면 변수명이 일치할 경우 쉐도잉이 발생하므로 유의해야 한다.
  • static nested 클래스, 자바 소스파일과 이름이 다른 top-level 클래스는 별도의 파일에 선언한 것과 거의 동일하다.
    • 따라서 @Service 등의 애너테이션 사용시, Outer 클래스나 자바 소스파일과 이름이 같은 top-level 클래스와 무관하게 스프링 빈으로 등록된다.
  • inner class, 즉 non-static nested class는 바깥쪽 클래스가 스프링 빈으로 등록되지 않으면 스프링 빈으로 등록되지 않는다.

 

 

 

 

학습 출처


자바의 내부 클래스는 스프링 빈이 될 수 있을까? by 토비의 스프링

Nested Classes by Oracle Java Tutorial

 

 

틀렸거나 보완할 내용이 있다면 알려주세요! 감사합니다.