1. 클래스 안의 클래스

자바에서는 클래스 안에 클래스가 들어갈 수 있습니다. 이러한 클래스를 "Nested 클래스"라고 부릅니다. 이와 같은 클래스가 존재하는 가장 큰 이유는 코드를 간단하게 표현하기 위함입니다. Nested 클래스는 자바 기반의 UI 처리를 할 때 사용자의 입력이나, 외부의 이벤트에 대한 처리를 하는 곳에서 가장 많이 사용됩니다. 

 

Nested 클래스는 선언한 방법에 따라 "Static nested 클래스"와 "내부 클래스"로 구분됩니다. Static nested 클래스와 내부 클래스의 차이는 static으로 선언되었는지 여부입니다. 내부 클래스는 다시 두가지로 나뉩니다. 이름이 있는 내부 클래스는 "로컬(혹은 지역) 내부 클래스" 라고 하고, 이름이 없는 클래스를 "익명 내부 클래스"라고 부릅니다. 각각 간단하게 "내부 클래스"와 "익명 클래스"라고 부릅니다.

 

다음의 예제를 살펴봅시다.

package c.inner;

public class PublicClass {

}

class JustNotPublicClass{

}

PublicClass라는 클래스와 JustNotPublicClass가 하나의 .java파일에 선언되어있습니다. 이 파일의 이름은 반든시 public한 클래스의 이름을 따라서 PublicClass.java로 정해져야 합니다. 그렇지 않으면 컴파일 에러가 발생합니다. 여기서 JustNotPublicClass는 public한 클래스가 아닐 뿐이지 이 클래스는 내부 클래스가 아닙니다. Nested 클래스를 만드는 이유는 다음과 같습니다.

 

1. 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 때

 

2. 캡슐화가 필요할 때(예를 들어 A라는 클래스에 private 변수가 있다. 이 변수에 접근 하고 싶은 B라는 클래스를 선언하고, B 클래스를 외부에 노출시키고 싶지 않을 경우가 여기에 속합니다.)

 

3. 소스의 가독성과 유지보수성을 높이고 싶을 때

 

여기서 1번이 Static Nested 클래스를 사용하는 이유이고, 2번이 내부 클래스를 사용하는 이유 입니다.

 

2. 정적 중첩 클래스 (Static nested Class)의 특징

내부 클래스는 감싸고 있는 외부 클래스의 어떤 변수도 접근할 수 있습니다. 심지어 private로 선언된 변수까지도 접근 가능합니다. 하지만, Static Nested 클래스를 그렇게 사용하는 것은 불가능 합니다. 왜냐하면 이름 그대로 Static하기 때문입니다.  

package c.inner;

public class OuterOfStatic {

    static class StaticNested{

        private int value = 0;

        public int getValue() {

            return value;

        }

        public void setValue(int value) {

            this.value = value;

        }

    }

}

1. 내부클래스와 비슷하나, static으로 선업합니다. 

 

2. 밖에 있는 클래스의 변수와 static 메소드를 사용할 수  있습니다.

 

[내부 클래스와의 차이]

  • 내부클래스는 밖에있는 자원을 마음대로 사용할 수 있지만, 중첩 클래스는 static 키워드가 안붙었다면 사용할 수 없습니다.
  • Outer 클래스의 객체가 없어도 Inner 클래스의 객체 생성이 가능합니다.
Outer.Inner 객체 = new Outer.Inner();

이 클래스를 컴파일해봅시다. Nested 클래스는 별도로 컴파일할 필요가 없습니다. 왜냐하면 여기서 OuterOfStatic이라는 감싸고 있는 클래스를 컴파일하면 자동으로 컴파일 됩니다.

 

"OuterOfStatic.class"와 "OuterOfStatic$StaticNested.class" 2개의 클래스가 만들어집니다. 즉, Nested 클래스는 이처럼 감싸고 있는 클래스 이름 뒤에 $ 기호를 붙인 후 Nested 클래스의 이름이 나옵니다.

 

이 StaticNested 클래스의 객체는 다음과 같이 생성할 수 있습니다.

package c.inner;

public class NestedSample {



    public static void main(String[] args) {

        NestedSample sample = new NestedSample();

        sample.makeStaticNestedObject();

    }

    public void makeStaticNestedObject() {

        OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested(); 

        staticNested.setValue(3);

        System.out.println(staticNested.getValue());

    }

}

이렇게 객체를 생성한 이후에 사용하는 방법은 일반 클래스와 동일합니다. 그렇다면 왜 이리 귀찮게 Static nested 클래스를 만들까요? 앞 절의 끝 부분에서 말했듯이 일반적으로 Static Nested 클래스를 만드는 이유는 클래스를 묶기 위해서 입니다. 

 

만약 학교를 관리하는 School이라는 클래스를 만들고, 대학을 관리하는 University라는 클래스를 만들었을 때를 생각해 봅시다. 이 때 Student라는 클래스를 만들면 School의 학생인지 University의 학생인지가 불분명해집니다. 하지만 만약 School 내에 static nested 클래스인 Student를 만든다면, 이 클래스의 용도가 보다 명확해집니다. 물론 SchoolStudent라는 식의 클래스를 만들면 되겠지만, 필요하다면 이렇게 만들 수도 있다는 말입니다. 게다가 School.Student 클래스는 School 클래스에 만들었기 때문에 University 클래스에서는 사용할 수가 없습니다.

 

3. 내부 클래스와 익명 클래스

앞 절에서 살펴본 Static Nested 클래스와 내부 클래스의 차이는 겉으로 보기에는  static을 쓰냐 아니냐 차이만 있습니다. 다음의 예를 봅시다.

package c.inner;

public class OuterOfInner {

    class Inner{

        private int value = 0;

        public int getValue() {

            return value;

        }

        public void setValue(int value) {

            this.value = value;

        }

    }

}


이제 이 Inner 클래스의 객체를 생성하는 방법을 살펴 봅시다. 

package c.inner;

public class NestedSample {

    public static void main(String[] args) {

        NestedSample sample = new NestedSample();

        sample.makeStaticNestedObject();

        sample.makeInnerObject();

    }

    public void makeInnerObject() {

        OuterOfInner outer = new OuterOfInner();

        OuterOfInner.Inner inner = outer.new Inner();

        inner.setValue(3);

        System.out.println(inner.getValue());

    }

    public void makeStaticNestedObject() {

        OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested(); 

        staticNested.setValue(3);

        System.out.println(staticNested.getValue());

    }

}

객체를 생성한 다음에 사용하는 방법은 차이가 없습니다. 하지만 객체를 생성하는 방법에 차이가 있습니다. Inner 클래스의 객체를 생성하기 전에는 먼저 Inner 클래스를 감싸고 있는 OuterOfInner라는 클래스의 객체를 만들어야만 합니다. 여기서는 outer라는 객체입니다. 그리고, 이 outer 객체를 통해서 Inner 클래스의 객체를 만들어 낼 수 있습니다. 

 

이렇게 내부 클래스를 만드는 이유는 캡슐화 때문입니다. 하나의 클래스에서 어떤 공통적인 작업을 수행하는 클래스가 필요한데, 다른 클래스에서는 그 클래스가 전혀 필요가 없을 때 이러한 내부 클래스를 만들어 사용합니다. 내부 클래스는 GUI 관련 프로그램을 개발할 때 가장 많이 사용합니다.

 

GUI에서 내부 클래스들이 많이 사용되는 부분은 Listener(리스너)라는 것을 처리할 때입니다. 이벤트가 발생할 때 어떤 버튼이 눌렸을 떄 해야 하는 작업을 정의하기 위해서 내부 클래스를 만들어 사용하게 됩니다. 그런데, 하나의 애플리케이션에서 어떤 버튼이 눌렸을 때 수행해야 하는 작업은 대부분 상이합니다. 그러니, 하나의 별도 클래스를 만들어 사용하는 것 보다는 내부 클래스를 만드는 것이 훨씬 편합니다. 그리고 내부 클래스를 만드는 것보다도 더 간단한 방법은 "익명 클래스"를 만드는 것입니다. 이름이 없는 클래스를 말합니다. 간단한 버튼을 처리하는 예제를 통해서 익명 클래스에 대해서 알아 보겠습니다. Gui는 제공하지 않으므로 코드의 흐름만 봅시다!

package c.inner;

public class MagicButton {

    public MagicButton() {

    }

    private EventListener listener;

    public void setListener(EventListener listener) {

        this.listener = listener;

    }

    public void onClickProcess() {

        if(listener!=null) {

            listener.onClick();

        }

    }

}

MagicButton 클래스는 이와 같이 간단하게 되어 있고, setListener() 메소드에서는 EventListener라는 것을 매개 변수로 받아 인스턴스 변수에 지정합니다. 그리고, 여기서 사용하는 EventListener는 다음과 같이 선언되어 있습니다.

package c.inner;

public interface EventListener {

    public void onClick();

}

이 클래스들을 적용하기 위해서, NestedSample 클래스에 다음과 같이 setButtonListener()라는 메소드를 만들어 봅시다. 또한 NestedSample 클래스 내에 다음과 같이 EventListener 인터페이스를 구현한 MagicButtonListener라는 클래스를 만들어봅시다. 실제로는 GUI에서는 이 예제처럼 별도로 onClickProcess() 메소드를 실행하지 않습니다. 화면이 클릭되었을 때 onClick() 메소드가 수행됩니다. 

package c.inner;

public class NestedSample {

    class MagicButtonListener implements EventListener{

        public void onClick() {

            System.out.println("Magic Button Clicked !!!");

        }

    }

    

    public static void main(String[] args) {

        NestedSample sample = new NestedSample();

        sample.makeStaticNestedObject();

        sample.makeInnerObject();

    }

    public void makeInnerObject() {

        OuterOfInner outer = new OuterOfInner();

        OuterOfInner.Inner inner = outer.new Inner();

        inner.setValue(3);

        System.out.println(inner.getValue());

    }
    
    public void makeStaticNestedObject() {

        OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested(); 

        staticNested.setValue(3);

        System.out.println(staticNested.getValue());

    }

    public void setButtonListener() {

        MagicButton button = new MagicButton();

        MagicButtonListener listener = new MagicButtonListener();

        button.setListener(listener);

        button.onClickProcess();

    }

}

방금 사용한 것과 같이 내부 클래스로 MagicButtonListener 클래스를 별도로 만들 수도 있습니다. 하지만, 다음과 같이 익명 클래스를 만들 수도 있습니다.

package c.inner;

public class NestedSample {

    class MagicButtonListener implements EventListener{

        public void onClick() {

            System.out.println("Magic Button Clicked !!!");

        }

    }

    public static void main(String[] args) {

        NestedSample sample = new NestedSample();

        sample.makeStaticNestedObject();

        sample.makeInnerObject();

        sample.setButtonListener();

    }

    public void makeInnerObject() {

        OuterOfInner outer = new OuterOfInner();

        OuterOfInner.Inner inner = outer.new Inner();

        inner.setValue(3);

        System.out.println(inner.getValue());

    }

    

    public void makeStaticNestedObject() {

        OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested(); 

        staticNested.setValue(3);

        System.out.println(staticNested.getValue());

    }

    

    public void setButtonListener() {

        MagicButton button = new MagicButton();

        MagicButtonListener listener = new MagicButtonListener();

        //button.setListener(listener);

        //button.onClickProcess();

        button.setListener(new EventListener() {

            public void onClick() {

                System.out.println("Magic Button Clicked !!!");

            }

        });

        button.onClickProcess();

    }

}

setListener() 메소드를 보면 new EventListener()로 생성자를 호출한 후 바로 중괄호를 열었습니다. 그리고, 그 중괄호 안에는 onClick() 메소드를 구현한 후 중괄호를 닫았습니다. 이렇게 구현한 것이 바로 "익명 클래스"입니다. 클래스 이름이 없지만 onClick()과 같은 메소드가 구현되어 있습니다. 따라서, setListener() 메소드가 호출되어 onClick() 메소드가 호출될 필요가 있을 때 그 안에 구현되어 있는 내용들이 실행됩니다. 그런데 이렇게 구현했을 때에는 클래스 이름도 없고, 객체 이름도 없기 때문에 다른 곳에서는 참조할 수가 없습니다. 그래서, 만약 객체를 해당 클래스 내에서 재사용하려면, 다음과 같이 객체를 생성한 후 사용하면 됩니다.

package c.inner;

public class NestedSample {

    class MagicButtonListener implements EventListener{

        public void onClick() {

            System.out.println("Magic Button Clicked !!!");

        }

    }

    public static void main(String[] args) {

        NestedSample sample = new NestedSample();

        sample.makeStaticNestedObject();

        sample.makeInnerObject();

        sample.setButtonListener();

    }

    public void makeInnerObject() {

        OuterOfInner outer = new OuterOfInner();

        OuterOfInner.Inner inner = outer.new Inner();

        inner.setValue(3);

        System.out.println(inner.getValue());

    }

    public void makeStaticNestedObject() {

        OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested(); 

        staticNested.setValue(3);

        System.out.println(staticNested.getValue());

    }

    public void setButtonListener() {

        MagicButton button = new MagicButton();

        //MagicButtonListener listener = new MagicButtonListener();

        //button.onClickProcess();

        EventListener listener = new EventListener(){

            public void onClick() {

                System.out.println("Magic Button Clicked !!!");

            }

        };

        button.setListener(listener);

        button.onClickProcess();

    }

}

그런데, 그냥 내부 클래스를 만들면 되는데 왜 자바에서는 이렇게 복잡하게 익명 클래스라는 것을 제공하는 것일까요? 익명 클래스를 만들었을 떄의 장점을 생각해봅시다. 클래스를 많이 만들면 만들수록 메모리는 많이 필요해지고, 애플리케이션을 시작 할 때 더 많은 시간이 소요됩니다. 그렇다고, 클래스 한 개 더 만든다고 해서 애플리케이션 시작 시간이 1초씩 더 걸리는 것은 아니지만, 줄일 수 있으면 줄이는 것이 좋습니다.

 

익명 클래스나 내부 클래스는 모두 다른 클래스에서 재사용할 일이 없을 때 만들어야 합니다. 어느 정도 익숙해지면, 오히려 익명 클래스를 사용함으로써 코드의 가독성이 높아질 수도 있지만, 그 반대의 경우도 생길 수 있으므로 너무 남용하지는 말아야 합니다.

 

4. Nested 클래스의 특징

Nested 클래스를 여러분이 사용하려면 알고 있어야 하는 사항 중 하나 설명해드리겠습니다. 바로 참조 가능한 변수들입니다. 다음의 예제를 보겠습니다.

package c.inner;

public class NestedValueReference {

    public int publicInt = 0;

    protected int protectedInt = 1;

    int justInt = 2;

    private int privateInt = 3;

    static int staticInt = 4;

    static class StaticNested{

        public void setValue() {

            staticInt = 14;

        }

    }

    class Inner{

        public void setValue() {

            publicInt = 20;

            protectedInt = 21;

            justInt = 22;

            privateInt = 23;

            staticInt = 24;

        }

    }

    public void setValue() {

        EventListener listener = new EventListener() {

            public void onClick() {

                publicInt = 30;

                protectedInt = 31;

                justInt = 32;

                privateInt = 33;

                staticInt = 34;

            }

        };

    }

}

예제에 있는 것과 같이 Static Nested 클래스에서는 감싸고 있는 클래스의 static 변수만 참조할 수 있습니다. 당연히 클래스가 static으로 선언되어 있으니, static하지 않은 변수를 참조할 수는 없습니다. 만약 참조하는 코드가 있다면, 컴파일시에 에러가 발생 하므로 어떻게라도 사용할 수 없습니다. 즉, 여기서 publicInt, protectedInt, justInt, privateInt 라는 변수는 StaticNest라는 클래스에서 참조가 불가능하다는 말입니다. static nested 클래스와는 다르게, 내부 클래스와 익명클래스는 감싸고 있는 클래스의 어떤 변수라도 참조할 수 있습니다.

 

그렇다면 반대로 감싸고 있는 클래스에서 Static Nested 클래스의 인스턴스 변수나 내부 클래스의 인스턴스 변수로의 접근은 가능할까요? 반대로의 참조도 물론 가능합니다. 다음과 같이 몇 줄을 추가합시다.

package c.inner;

public class NestedValueReference {

    public int publicInt = 0;

    protected int protectedInt = 1;

    int justInt = 2;

    private int privateInt = 3;

    static int staticInt = 4;

    static class StaticNested{

        private int staticNestedInt=99;

        public void setValue() {

            staticInt = 14;

        }

    }

    class Inner{

        private int innerValue = 100;

        public void setValue() {

            publicInt = 20;

            protectedInt = 21;

            justInt = 22;

            privateInt = 23;

            staticInt = 24;

        }

    }

    public void setValue() {

        EventListener listener = new EventListener() {

            public void onClick() {

                publicInt = 30;

                protectedInt = 31;

                justInt = 32;

                privateInt = 33;

                staticInt = 34;

            }

        };

    }

    public void setValue(int value) {

        StaticNested nested = new StaticNested();

        nested.staticNestedInt = value;

        Inner inner = new Inner();

        inner.innerValue = value;

    }

}

StaticNest 클래스와 Inner 클래스에 각각 한 줄이 추가되고, 감싸고 있는 NestedValueReference 클래스에 int 타입의 value를 매개 변수로 받는 setValue(int) 메소드가 추가되었습니다. 이렇게 각 클래스의 객체를 생성한 후 그 값을 참조하는 것은 가능합니다. 그 값이 private라고 할지라도 모두 접근할 수 있습니다.

 

마지막으로 자신이 만든 클래스의 내용을 숨기려고, 너무 많은 Nested 클래스를 선언하여 사용하는 것은 좋지 않습니다. Nested 클래스를 사용하는 것이 어떻게 보면 가독성이 증가할 수 있지만, 어떻게 보면 오히려 가독성이 감소될 수도 있습니다. 따라서, 꼭 필요한 경우에만 적절하게 사용하는 것이 좋습니다.

 

'Java' 카테고리의 다른 글

[Java] 20. 예외 처리(2)  (0) 2019.04.18
[Java] 20. 예외 처리(1)  (0) 2019.04.17
[Java] 18. enum 클래스  (0) 2019.04.09
[Java] 17. 상속과 final  (0) 2019.04.08
[Java] 16. 인터페이스(Interface)와 추상클래스(abstract 클래스)  (0) 2019.04.01
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기