[프로그래밍/자바/코틀린] 중첩 클래스

자바의 중첩 클래스

자바에서 클래스 내에 클래스를 정의할 수 있다. 클래스 내에 정의된 클래스를 중첩 클래스(nested class)라고 한다. 중첩 클래스는 비정적 중첩 클래스(non-static nested class)와 정적 중첩 클래스(static nested class)로 구분된다. 비정적 중첩 클래스를 내부 클래스(inner class)라고도 한다. static 키워드를 통해 선언된 중첩된 클래스를 정적 중첩 클래스라고 한다.

// 최상위 클래스
public class OuterClass {
  // 비정적 중첩 클래스 (내부 클래스)
  class InnerClass {
    ...
  }
  
  // 정적 중첩 클래스
  static class StaticNestedClass {
    ...
  }
}


비정적 중첩 클래스인 내부 클래스는 외부 클래스의 인스턴스에접근할 수 있는 반면 정적 중첩 클래스는 외부 클래스의 인스턴스에 접근할 수 없다.

내부 클래스를 인스턴스화하려면, 먼저 외부 클래스를 인스턴스화해야 한다. 외부 클래스를 인스턴스화한 후 외부 객체 참조를 사용하여 내부 클래스를 인스턴스화한다.

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();


정적 중첩 클래스는 최상위 클래스와 같은 방식으로 인스턴스화한다. 내부 클래스와 다르게 외부 클래스를 인스턴스화할 필요가 없다.

StaticNestedClass staticNestedObject = new StaticNestedClass();


내부 클래스의 경우 내부 클래스의 인스턴스와 외부 클래스의 인스턴스 사이의 관계는 내부 클래스가 인스턴스화될 때 정해지며 이후에 변경할 수 없다. 이 참조 관계는 외부 클래스의 인스턴스 메서드 내에서 내부 클래스의 생성자를 호출할 때 자동으로 생성되거나, 외부클래스명.new 내부클래스명()와 같이 외부 클래스 바깥에서 내부 클래스를 인스턴스화하는 코드를 직접 호출하는 경우 수동으로 생성된다. 이러한 참조는 내부 클래스의 인스턴스 안에 생성되어 메모리 공간을 차지하며, 시간도 소요된다. 내부 클래스가 외부 클래스의 인스턴스에 대한 숨은 외부 참조를 갖게 되므로 가비지 컬렉션이 외부 클래스의 인스턴스를 적절하게 컬렉션하지 못할 경우 메모리 누수가 발생할 가능성이 있다.

중첩 클래스를 정적으로 정의할 것인가, 비정적으로 정의할 것인가는 중첩 클래스에서 외부 클래스의 인스턴스에 접근할 필요가 있는가에 달려있다. 중첩 클래스에서 외부 클래스의 인스턴스를 참조하여 해당 인스턴스의 멤버를 직접 사용할 일이 없다면 정적 중첩 클래스로 만들어서 외부 인스턴스에 대한 참조가 생기지 않도록 하여 시간적, 공간적 낭비를 줄일 수 있다.

중첩 클래스를 사용하는 이유와 목적은 다음과 같다.

  1. 서로 상호작용하는 클래스를 논리적으로 그룹화하고, 읽기 쉽고 유지보수하기 쉬운 코드 생성: 클래스 A가 클래스 B와만 상호작용한다면 클래스 A와 클래스 B를 하나의 클래스 파일에 중첩하여 정의할 수 있다(하나의 클래스 파일 상에서 최상위 레벨에 여러 클래스를 정의하는 것은 권장되지 않는다). 서로 관련이 있는 클래스를 하나의 클래스 파일에 정의함으로써 코드의 응집도를 높이고, 읽고 수정하기 쉽게 만들 수 있다. 단, 두 클래스의 멤버를 한 클래스로 합치지 않아야 하는 경우에만 해당된다.
  2. 캡슐화 증가: 클래스 A의 멤버가 클래스 B에 의해서만 사용될 때, 클래스 A의 멤버를 선택적으로 클래스 B만 접근 가능하도록 정의하는 것이 불가능하다. 대신 중첩 클래스 구조를 사용하면 클래스 A 내에 클래스 B를 정의하고, 클래스 A의 멤버를 private로 선언하여 클래스 B만 접근 가능하도록 하고 외부로부터 캡슐화할 수 있다.
     public class OuterClassA {
       private String outerClassMemberA = "Outer Class Member A";
    
       class InnerClassB {
         public getData() {
           System.out.println(outerClassMemberA);
         }
       }
     }
    


내부 클래스는 어댑터(adapter)를 정의할 때 사용할 수 있다. 어댑터란 클라이언트와 호환되지 않는 인터페이스를 가진 객체가 클라이언트와 협업할 수 있도록 하거나, 복잡한 인터페이스를 가진 객체가 클라이언트와 단순한 인터페이스로 협업할 수 있도록 별도의 인터페이스를 구현하고 기존 객체를 래핑(wrapping)함으로써 인터페이스를 변환하는 역할을 수행하는 객체를 말한다. 어댑터를 사용하여 기존 객체의 인터페이스나 기능을 수정할 필요 없이 래핑된 객체로 기존 객체의 기능을 사용할 수 있다. 어댑터 클래스만 상속하는 경우 내부 클래스 사용은 필요 없지만 다른 클래스도 상속해야 하는 경우 자바는 다중 상속을 지원하지 않으므로 내부 클래스를 통해 어댑터를 사용할 수 있다. 인터페이스에 대해서도 동일하다.

public class Adaptee {
  public NewData getNewData() {
    return new NewData();
  }
}
public class Client implements AnotherTargetService {
  public Adapter getAdapter() {
    return new Adapter();
  }
  
  private class Adapter implements TargetService {
    private Adaptee adaptee;
    
    public Adapter(Adaptee adaptee) {
      this.adaptee = adaptee
    }
   
    @Override
    public Data getData() {
      NewData newData = adaptee.getNewData();
      return new Data(newData);
    }
  }
}

...

Client client = new Client();
Data dataFromAdaptee = client.new Adapter().getData();


어댑터는 클라이언트 인터페이스를 구현하는 동시에 서비스 객체를 래핑한다. 어댑터는 클라이언트 인터페이스를 통해 클라이언트로부터 호출을 받아 이를 래핑된 서비스 객체에 대한 호출로 변환한다.

public 정척 중첩 클래스의 경우 외부 클래스는 네임스페이스를 제공하고 정적 중첩 클래스는 외부 클래스의 정적 멤버로서 외부에 공개된다. public 정척 중첩 클래스는 주로 외부 클래스를 위한 헬퍼(helper) 클래스를 정의할 때 사용된다. 헬퍼 클래스란 클래스의 기능 제공을 돕는 클래스로, 열거 상수 제공, 기능 메서드 제공 등의 역할을 담당한다. 헬퍼 클래스를 정의하는 방법에는 여러 가지가 있다. 중첩 클래스를 사용하면 헬퍼 클래스 객체를 의존성 주입하는 합성(composition)을 이용하는 대신 클래스 내에 정의하여 헬퍼 클래스와 외부 클래스를 논리적으로 그룹화할 수 있다. 헬퍼 클래스는 외부 클래스의 public 정적 멤버가 되며 클라이언트는 외부 클래스를 참조하여 헬퍼 클래스를 사용할 수 있다.

public class MyClass {
  static class MyHelper {
    public static Data getData() {
      return new Data();
    }
  }
}
public class Client {
  private MyClass myClass;

  public getData() {
    Data data = myclass.MyHelper.getData();
  }
}


외부에 공개되지 않는 private 정적 중첩 클래스는 주로 클래스가 갖는 비공개 요소를 정의하기 위해 사용된다. 클래스의 요소를 외부에 공개할 필요가 없다면 private로 선언하고, 클래스의 요소가 외부 클래스에 대한 참조를 갖고 있을 필요가 없다면 중첩 클래스를 정적으로 선언하여 불필요한 참조가 생기지 않도록 한다.

public class MyClass {
  private MyObject myObject;

  private static class MyObject {
    public static Data getData() {
      return new Data();
    }
  }
}


중첩 인터페이스

인터페이스는 멤버 타입 선언을 포함할 수 있으며 인터페이스에 선언한 모든 멤버는 암시적으로 정적(static)이고 공개적(public)인 특성을 갖는다. 따라서 인터페이스는 정적 중첩 클래스를 포함할 수 있지만 비정적 중첩 클래스인 내부 클래스를 포함할 수는 없다. static 키워드를 생략하여 중첩 클래스를 선언하더라도 해당 클래스는 비정적 클래스가 아닌 정적 클래스가 된다. 인터페이스는 본질적으로 정적인 특성을 가진다.

interface OuterInterface {
  // 내부 클래스가 아닌 정적 중첩 클래스가 된다.
  class StaticNestedClass {
    ...
  }
  
  // 명시적으로 static 키워드를 사용하여 클래스를 선언할 수도 있다.
  static class StaticNestedClass {
    ...
  }
}


인터페이스 내에 인터페이스를 정의하는 중첩 인터페이스 정의도 가능하다.

interface OuterInterface {
  interface NestedInterface {
    ...
  }


코틀린의 중첩 클래스

코틀린의 경우 비정적 중첩 클래스인 내부 클래스를 정의하기 위해 명시적으로 inner 키워드를 사용하여 클래스를 선언하며, 정적 중첩 클래스를 정의하기 위해서는 별도의 키워드 없이 클래스 내부에 클래스를 정의하면 된다.

class OuterClass {
  inner class InnerClass {
    ...
  }
  
  class StaticNestedClass {
    ...
  }
}

자바와 마찬가지로 중첩된 인터페이스 정의도 가능하며, 클래스와 인터페이스의 조합으로 중첩 구조 정의가 가능하다.


참고

Comments