Nested 클래스

de_sj_awa·2021년 4월 25일
0

1. Nested 클래스

자바에서는 클래스 안에 클래스가 들어갈 수 있다. 이러한 클래스를 "Nested 클래스"라고 부른다. 이와 같은 Nested 클래스가 존재하는 이유는 여러 가지가 있겠지만, 가장 큰 이유는 코드를 간단하게 표현하기 위함이다. Nested 클래스는 자바 기반의 UI 처리를 할 때 사용자의 입력이나, 외부의 이벤트에 대한 처리를 하는 곳에서 가장 많이 사용된다. 이러한 부분에서 Nested 클래스가 없으면 코드가 매우 복잡해진다. 그럼 먼저 어떤 것들이 있는지 알아보자.

Nested 클래스는 선언한 방법에 따라 "Static nested 클래스"와 "내부(inner) 클래스"로 구분된다. Static nested 클래스와 내부 클래스의 차이는 static으로 선언되었는지 여부다. 만약 클래스를 선언할 때 static으로 선언이 되었다면, 그 클래스는 static nested 클래스가 되고, static이 없으면 그냥 내부 클래스라고 한다.

내부 클래스는 다시 두 가지로 나뉘는데, 이름이 있는 내부 클래스는 "로컬(혹은 지역) 내부 클래스(local inner class)"라고 하고, 이름이 없는 클래스를 "익명 내부 클래스(anonymous inner class)"라고 부른다. 하지만, 일반적으로는 간단하게 줄여서 각각 "내부 클래스"와 "익명 클래스"로 부른다.

자바에서 Nested 클래스를 쓰는 이유는 다음과 같다.

  • 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 때
  • 캡슐화가 필요할 때(예를 들어 A라는 클래스에서 private 변수가 있다. 이 변수에 접근하고 싶은 B라는 클래스를 선언하고, B 클래스를 외부에 노출시키고 싶지 않을 경우가 여기에 속한다.) 즉, 내부 구현을 감추고 싶을 때를 말한다.
  • 소스의 가독성과 유지보수성을 높이고 싶을 때

여기서, 가장 첫 번째 이유가 Static Nested 클래스를 사용하는 이유고, 두 번째 이유가 내부 inner 클래스를 사용하는 이유다.

2. Static nested 클래스의 특징

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

예제 코드

package nested;

public class OuterOfStatic {
    static class StaticNested{
        private int value = 0;
        public int getValue(){
            return value;
        }
        public void setValue(int value){
            this.value = value;
        }
    }
}

컴파일하면 다음과 같이 두 개의 클래스가 만들어진다.

OutOfStatic.class
OutOfStatic$StaticNested.calss

Nested 클래스는 이처럼 감싸고 있는 클래스 이름 뒤에 $ 기호를 붙인 후 Nested 클래스의 이름이 나온다. 그리고 보는 것과 같이 별도의 클래스 파일로 만들어진다.

그러면 이 StaticNested 클래스의 객체는 어떻게 생성할 수 있을까?

예제 코드

package nested;

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 클래스를 만들었을까? 일반적으로 Static nested 클래스를 만드는 이유는 클래스를 묶기 위해서이다.

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

University 클래스의 예

public class University{
   static class Student{
   }
}

School 클래스의 예

public class School{
    static class Student{
    }
}

그래서 겉으로 보기에는 유사하지만, 내부적으로 구현이 달라야 할 때 이와 같이 static nested 클래스를 사용한다.

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

예제 코드

package nested;

public class OuterOfInner {
    class Inner {
        private int value = 0;
        public int getValue(){
            return value;
        }
        public void setValue(int value){
            this.value = value;
        }
    }
}

앞의 예제에서 선언햇던 StaticNested와 Inner 클래스의 내용은 동일하다. 하지만, Inner 클래스의 선언부에는 static 선언이 없다. 그러므로, 이 Inner 클래스의 객체를 생성하는 방법도 다르다.

package nested;

public class InnerSample {
    public static void main(String[] args){
        InnerSample sample = new InnerSample();
        sample.makeInnerObject();
    }
    public void makeInnerObject(){
        OuterOfInner outer = new OuterOfInner();
        OuterOfInner.Inner inner = outer.new Inner();
        inner.setValue(3);
        System.out.println(inner.getValue());
    }
}

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

그런데 이와 같이 Inner 클래스의 객체를 만드는 방법이 복잡한데도 불구하고 왜 만들어졌을까?

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

GUI에서 내부 클래스들이 많이 사용되는 부분은 리스너(Listener)라는 것을 처리할 때 사용한다. 사용자가 버튼을 클릭하거나, 키보드를 입력할 때에는 모두 이벤트(Event)라는 것이 발생하게 된다. 어떤 버튼이 눌렸을 때 해야 하는 작업을 정의하기 위해서 내부 클래스를 만들어 사용하게 된다.

그런데, 하나의 애플리케이션에서 어떤 버튼이 눌렸을 때 수행해야 하는 작업은 대부분 상이하다. 그러나, 하나의 별도 클래스를 만들어 사용하는 것보다는 내부 클래스를 만드는 것이 훨씬 편하다. 그리고, 내부 클래스를 만드는 것보다도 더 간단한 방법은 "익명 클래스"를 만드는 것이다.

익명이라는 것은 영어로 Anonymous라고 하고, 자바에서 익명 클래스는 말 그대로 이름이 없는 클래스다.

예제 코드

package nested;

public class MagicButton {
    public MagicButton(){

    }
    private EventListener listener;
    public void setListener(EventListener listener){
        this.listener = listener;
    }
    public void onClickProcess(){
        if(listener!=null){
            listener.onClick();
        }
    }
}
package nested;

public class AnonymousSample {
    public static void main(String[] args){
        AnonymousSample sample = new AnonymousSample();
        sample.setButtonListenerAnonymous();
    }
    public void setButtonListenerAnonymous(){
        MagicButton button = new MagicButton();
        button.setListener(new EventListener() {
            @Override
            public void onClick() {
                System.out.println("Magic Button Clicked !!!");
            }
        });
    }
}

setListener() 메소드를 보면 new EventListener()로 생성자를 호출한 후 바로 중괄호를 열었다. 그리고, 그 중괄호 안에는 onClick() 메소드를 구현한 후 중괄호를 닫았다. 이렇게 구현한 것이 바로 "익명 클래스"이다. 클래스에는 이름이 없지만, onClick()과 같은 메소드가 구현되어 있다. 이렇게 익명 클래스를 구현할 때 조심해야 하는 것은 괄호를 닫는 것이다. 중괄호를 닫고, 소괄호를 닫고 세미콜론을 했다. 겉으로 보기에는 클래스를 선언한 것이지만, 실제로는 setListener() 메소드를 호출하는 과정 내에 익명 클래스가 있는 것이기 때문에 이와 같이 소괄호를 닫고 세미콜론을 해 줘야만 한다.

setListener() 메소드가 호출되어 onClick() 메소드가 호출될 필요가 있을 때 그 안에 구현되어 있는 내용들이 실행된다. 그런데, 이렇게 구현했을 때에는 클래스 이름도 없고, 객체 이름도 없기 때문에 다른 클래스나 메소드에서는 참조할 수가 없다. 그래서, 만약 객체를 해당 클래스 내에서 재사용하려면, 다음과 같이 객체를 생성한 후 사용하면 된다.

package nested;

public class AnonymousSample2 {
    public static void main(String[] args){
        AnonymousSample2 sample = new AnonymousSample2();
        sample.setButtonListenerAnonymous();
    }
    public void setButtonListenerAnonymous(){
        MagicButton button = new MagicButton();
        EventListener listener = new EventListener() {
            @Override
            public void onClick() {
                System.out.println("Magic Button Clicked !!!");
            }
        };
        button.setListener(listener);
        button.onClickProcess();
    }
}

그런데, 그냥 내부 클래스를 만들면 편할 것 같은데 왜 자바에서는 이렇게 복잡하게 익명 클래스라는 것을 제공하는 것일까? 익명 클래스를 만들었을 때의 장점을 생각해보자. 여러분들이 클래스를 만들고, 그 클래스를 호출하면 그 정보는 메모리에 올라간다. 즉, 클래스를 많이 만들면 만들수록 메모리는 많이 필요해지고, 애플리케이션을 시작할 때 더 많은 시간이 소요된다. 따라서, 자바에서는 이렇게 간단한 방법으로 객체를 생성할 수 있도록 해놓았다.

4. Nested 클래스의 특징 : 참조 가능한 변수

Nested 클래스를 사용하려면 알고 있어야 하는 사항은 참조 가능한 변수들이다.

package nested;

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() {
            @Override
            public void onClick() {
                publicInt = 30;
                protectedInt = 31;
                justInt = 32;
                privateInt = 33;
                staticInt = 34;
            }
        };
    }
}

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

그렇다면, 반대로 감싸고 있는 클래스에서 Static Nested 클래스의 인스턴스 변수나 내부 클래스의 인스턴스 변수로의 접근하는 것은 가능할까?

반대로의 참조도 물론 가능하다.

예제 코드

package nested;

public class ReferenceAtNested {
    static class StaticNested{
        private int staticNestedInt = 99;
    }
    class Inner{
        private int innerValue = 100;
    }
    public void setValue(int value){
        StaticNested nested = new StaticNested();
        nested.staticNestedInt = value;
        Inner inner = new Inner();
        inner.innerValue = value;
    }
}

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

profile
이것저것 관심많은 개발자.

0개의 댓글