티스토리 뷰

https://www.geeksforgeeks.org/inner-class-java/

이번 포스트에서는 자바의 이너클래스에 대해 살펴보겠습니다. 이너클래스란 다른 클래스의 멤버로 정의된 클래스를 말합니다. 자바는 두개 이상의 데이터를 묶어서 사용하려도(C/C++의 struct) 클래스가 필요하고 함수를 다른 곳에 전달하려도(C/C++의 함수 포인터) 클래스가 필요합니다. 그렇다고 따로 .java 파일을 만들 필요까지는 없고 이 클래스에서만 사용할 꺼라면 이너클래스(Inner Class, 내부클래스)가 좋은 선택입니다.

class Hand {
    Card[] hand;
    Hand(String shapes, String nums) {
        hand = new Card[shapes.length()];
        Card c = null;
        for (int i = 0; i < shapes.length(); i++) {
            c = new Card(shapes.charAt(i), nums.charAt(i));
            hand[i] = c;
        }
    }          
    Card getNewCard() {
        System.out.print("=>");
        token = keyin.next();
        return new Card(token.charAt(0), token.charAt(1));
    }
    public String toString() {
        StringBuilder result = new StringBuilder();
        for (Card c : hand) 
            result.append(c + " ");
        return result.toString();
    }
    class Card {						// 내부 클래스 Card 선언
        char shape;
        char num;
        Card(char c, char n) {
            shape = c;
            num = n;
        }            
        public String toString() {
            return ""+shape+num;
        }
    }
}
        

위의 코드에서 외부에서 Hand를 new 할 수 있고 getNewCard를 통해 새로운 Hand.Card를 받을 수도 있습니다. 그러나 Card 클래스는 기본적으로 Hand 클래스 안에서 사용하는 것이 목적이고 따로 쓰일 일은 없을 때(카드를 낱장으로 다루게 하지 않겠다는 뜻?) 이런 식으로 이너클래스로 정의하게 됩니다. (만약 외부에서 카드로 쓸 필요가 생기면 Card.java를 분리해서 만드는 것이 더 바람직하겠지요.)

이너 클래스도 public으로 선언해서 외부에서 사용도 가능합니다. HashMap 클래스의 Entry 클래스가 그런 예입니다. 외부에서 해시맵의 단위인 키 밸류 쌍을 Entry 타입 객체로 받을 수 있습니다. 그러나 많은 경우 클래스 내부에서 접근하는 용도로 사용하고 그런 경우에는 클래스를 default나 private으로 선언해서 외부에서 보이지 않게 하는 경우가 많습니다. 

그럼 이런 이너클래스에서 사용하는 이름의 범위와 바인딩은 어떨까요? 이너클래스를 둘러싼 바깥 클래스에서 정의된 이름은 기본적으로 이너클래스에서도 보입니다. private 이름도 사용할 수 있습니다. 즉 바깥 범위의 private 필드를 이용할 수 있고 private 메소드도 호출할 수 있습니다. 같은 이름을 이너클래스에서 또 선언한다면? 바깥 범위의 이름이 가려지게 됩니다. 중첩 범위 규칙이 그대로 적용됩니다.

https://docs.oracle.com/javase/tutorial/displayCode.html?code=https://docs.oracle.com/javase/tutorial/java/javaOO/examples/ShadowTest.java

반대로 바깥 클래스 쪽의 코드에서 이너클래스에서 정의된 이름을 접근할 수 있을까요? 이너클래스는 바깥 클래스 입장에서는 같은 식구입니다. 단지 캡슐화의 목적으로 클래스로 묶었을 뿐이므로 바깥 클래스에서는 얼마든지 이너클래스의 private을 이용할 수 있습니다. 예를 들어 위의 코드에서 Card의 shape이나 num이 private이더라도 Hand의 toString에서 c.shape이나 c.num을 접근할 수 있습니다. 단, Card 객체 c를 통해서 접근해야 되겠지요...

자바의 일반 클래스에서 중첩범위규칙에 의해 지역변수 때문에 가려진 필드의 이름은 this. 으로 접근가능합니다. 그럼 이너클래스에서 필드 때문에 가려진 바깥 클래스 필드는 어떻게 접근할 수 있을까요? 여기서 문제는 바깥 클래스의 this도 이너클래스의 this 때문에 가려진 이름이 된다는 것입니다. 즉 this.x 라고 하면 이너클래스의 this를 의미하게 됩니다. 이것도 이너클래스에서 메소드의 지역변수에 가려질 수 있으므로... 그럼 바깥 클래스의 this는 어떻게 접근해야 할까요? OuterClass.this.x 이렇게 접근하게 됩니다. OuterClass.x하면 static을 찾게 되므로 this를 붙여주어야 한답니다.

여기서 잠깐 오라클 사이트에 나와있는 이너클래스의 목적을 잠깐 보고 갑시다.

Compelling reasons for using nested classes include the following:

  • It is a way of logically grouping classes that are only used in one place: If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such "helper classes" makes their package more streamlined.  (헬퍼 클래스)

  • It increases encapsulation: Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.  (캡슐화 목적)

  • It can lead to more readable and maintainable code: Nesting small classes within top-level classes places the code closer to where it is used. 

이너클래스는 어떨 때 쓰일까요? 위에서 설명한 것처럼 캡슐화를 위한 단위로 쓰이는 경우도 많지만 그보다는 자바에서는 함수포인터 역할을 하기 위해 많이 쓰입니다. 다른 곳에 함수를 전달해야 하는 일은 생각보다 많습니다. 대표적인 것이 핸들러 함수의 전달인데요, 어떤 이벤트에 대해 핸들러를 등록해 두고 그 이벤트가 발생했을 때 불려지게 하는 것이 필요합니다. 또는 비동기 호출에 대한 컬백 함수도 그런 예가 될 수 있겠지요? 핸들러나 컬백을 등록할 때는 이 함수를 써라 라고 변수에 저장하거나 매개변수로 넣거나 해야 됩니다. 이런 식으로 함수를 변수에 저장하고 매개변수로 전달할 수 있는 것을 일급함수라고 합니다. C의 함수포인터는 이런 기능을 해주는데 자바에는 이에 대응하는 개념이 없기 때문에 이것을 인터페이스로 대신하게 됩니다. 이것에 대해서는 앞의 인터페이스 포스트에서 다루었습니다. 사실 Comparable이나 핸들러를 구현하는 이너클래스가 자바에서 가장 많이 쓰이는 패턴입니다.

class TestDialog extends JDialog {
   JButton but = new JButton("OK");
   JDialog() {
      getContentPane().add(buttonPane);
      but.addActionListener(new ButtonHandler());
      ...
   }
   class ButtonHandler implements ActionListender {   // 내부클래스
        //close and dispose of the window.
        public void actionPerformed(ActionEvent e) {
            System.out.println("OK. Bye!");
            setVisible(false);
            dispose();
        }
    }   
}

이와 같이 핸들러 메소드를 가진 클래스를 이너클래스로 정의하고 그것을 new 해서 리스너에 등록하는 경우가 많습니다. 그런데 여기서 ButtonHandler 클래스는 핸들러로 전달되는 것 이외에 다른 데서 쓰일 일이 없고, 이 클래스 안에서도 쓰이지 않는다면? 내부 클래스는 정보 은닉이 되지 않고 외부에서도 접근가능하므로 캡슐화의 목적에는 별로 도움이 되지 않습니다. 이런 경우 아예 클래스를 필요한 수 안에서 정의하고 쓰자는 것이 로컬 이너클래스(줄여서 로컬클래스)입니다.

class TestDialog extends JDialog {
   JButton but = new JButton("OK");
   JDialog() {
      getContentPane().add(buttonPane);
      but.addActionListener(new ButtonHandler());
      ...
      class ButtonHandler implements ActionListender {   // 생성자 함수 안에 정의된 로컬클래스
        //close and dispose of the window.
        public void actionPerformed(ActionEvent e) {
            System.out.println("OK. Bye!");
            setVisible(false);
            dispose();
        }
      }
   }
}

이렇게 클래스 정의가 함수 안에 나타나는 것을 local inner class 라고 합니다. 함수 안에서 웬 클래스? 라고 생각할 수도 있는데, 앞에서 살펴본 것처럼 사용되는 곳 이외에는 접근할 수 없는 캡슐화를 위한 이너 클래스의 목적을 생각해 보면 좀더 잘 이해할 수가 있습니다. 

이런 이너클래스는 그럼 범위 규칙이 어떻게 적용될까요? 클래스 안에 메소드, 메소드 안에 클래스, 이너클래스 안에 또 메소드죠? 엄청 복잡한 중첩 범위를 가지게 됩니다. 여기서도 원칙은 간단합니다. 중첩 범위 안에서는 바깥 범위의 이름은 다 보입니다. 같은 이름은 가려집니다(shadowing). 로컬 이너 클래스는 바깥 클래스 이름들(필드나 메소드) 뿐 아니라 자기를 둘러싼 함수의 지역변수도 보입니다. 

그럼 여기서 잠깐 이름의 바인딩과 객체의 생명 주기에 대해 생각해 보아야 할 것 같습니다. 여기서 생성된 이너클래스 객체는 매개변수로 전달되어서 나중에 이벤트가 발생하거나 콜백이 불려져야 할 때 그 메소드를 호출하게 됩니다. 문제는 그 메소드가 호출될 때 여기서 사용된 변수 중에 비지역 변수들 (바깥 클래스 객체의 필드나 포함한 함수의 지역변수)를 접근하려고 하면 그 필드의 값을 가져올 수 있을까요? 위의 예에서 setVisible(false)는 바깥 클래스 TestDialog.this를 안 보이게 하는데, 이 함수가 호출되었을 때 그 this가 Dialog 객체를 잘 가리키고 있을까요? 만약 그 객체가 없다면? 댕글링 참조가 발생하게 됩니다...

이런 문제를 해결하는 것이 클로우저라는 개념입니다. 함수가 불려지는 범위와 함수가 정의된 범위가 다르므로 비지역 변수의 바인딩을 위해서는 함수가 정의된 범위의 객체들이 다 살아있어야 합니다. 객체지향에서는 이런 문제를 해결하는 것이 어렵지 않은데요, 바깥 클래스의 this를 이 메소드의 this로 불러주면 됩니다. 그리고 그 객체는 참조 카운트가 증가해서 이 함수에서 this로 바인딩되어 있으므로 쓰레기가 되지 않습니다. 그러므로 그 객체의 필드를 접근하거나 메소드를 호출하는 것이 가능하겠지요? 즉 클로우저라는 것 안에 this를 담아두고 나중에 이 함수의 호출에서 this로 주게 됩니다.

문제는 바깥 범위의 함수에 있는 지역변수 또는 매개변수입니다. 이너클래스가 정의되고 new 되어서 어딘가에 전달 또는 등록되고 나면 함수가 종료하고 그 함수의 지역변수들도 사라집니다. 핸들러 메소드에서 이런 변수를 접근하려고 할 때 객체가 없어졌으면 어떻게 접근해야 할까요? 다른 예를 한번 더 보겠습니다.

class TestDialog extends JDialog {
   JButton but = new JButton("OK");
   JDialog() {
      getContentPane().add(but);
      but.addActionListener(new ButtonHandler());
      ...
      String cancel = "Cancel";
      class ButtonHandler implements ActionListender {
        public void actionPerformed(ActionEvent e) {
            but.setText(cancel);
        }
      }
   }
}   

위의 예제에서 but는 바깥 객체의 필드고 cancel은 지역변수입니다. 버튼이 눌려지면 이 지역변수의 값으로 버튼의 텍스트를 바꾸려고 하는데, 과연 cancel 변수를 어떻게 찾을까요? 이 변수는 이미 함수가 종료되고 사라졌는데...

자바의 로컬이너클래스(간단히 로컬클래스라고 부름)에서는 클로우저를 통해 이러한 문제를 해결합니다. 즉 클로우저에 참조될 수 있는 지역 변수 값을 이름과 함께 저장해 둡니다. 스택에서 사라진 후에도 접근할 수 있게 지역변수 객체의 사본을 저장해 두는 것이지요. 그 사본은 new가 일어나는 시점에 값을 저장하게 됩니다. 그럼 이 값이 바뀌면 어떻게 할 것이냐의 문제가 있게 되지요. 예를 들어 위의 "Cancel"이 뒤로 가서 "Retry"로 또 바뀐다면? 이 핸들러는 Cancel로 바꾸어야 할 것인가 Retry로 바꾸어야 할 것인가? 이런 문제를 방지하기 위해서 자바에서는 로컬클래스에서 접근하는 지역변수는 final 또는 effectively final이어야 한다라고 정하고 있습니다. 즉 값을 한번 설정하고 바꾸지 말아야 한다는 것입니다. 자바 7 이전의 버전에서는 final이어야 컴파일이 되었는데, 자바 8부터는 effectively final이라는 것을 추가해서 값이 안 바뀌면 접근 가능한 것으로 보고 있습니다. 이것을 복사해서 클로우저에 넣어두고 나중에 이 함수가 호출되었을 때 쓰겠다는 것이지요.

그럼 이제 마지막으로 무명 로컬클래스(anonymous class)를 살펴보겠습니다. 위의 로컬클래스에서 한걸음 더 나가서 어차피 new하는 곳도 한 군데고 여기서만 쓰인다면 굳이 클래스 정의할 필요조차 없지 않은가 하고 생각해 볼 수 있습니다. 그래서 이름 없이(anonymous) 로컬클래스를 정의하면서 바로 new 해서 넘길 수 있게 하고 있습니다.

Age oj1 = new Age() {
            @Override
            public void getAge() {
                System.out.print("Age is "+x);
            }
        };

위의 코드는 Age라는 클래스의 객체를 하나 만드는데 getAge를 오버라이드하여 새로이 정의한 것을 사용하여 new를 하고 있습니다. 즉 원래 Age 클래스는 그대로 두고 그것을 상속하여 메소드를 오버라이드하여 정의한 클래스를 추가로 정의한 것입니다. 재사용할 일이 없으므로 굳이 클래스 이름을 붙일 필요가 없어서 무명으로 메소드만 하나 가진 클래스를 만든 것입니다. 이러한 형태의 무명 로컬클래스는 이벤트 핸들러의 등록과 같이 인터페이스를 구현한 내부 클래스를 만들 때도 많이 사용하게 됩니다. 무명 클래스는 항상 로컬로 즉 함수 코드 안에 정의되므로 그냥 무명 클래스(anonymous class)라고 줄여 부를 수 있습니다.

https://www.geeksforgeeks.org/anonymous-inner-class-java/   

 

Anonymous Inner Class in Java - GeeksforGeeks

Prerequisites :- Nested Classes in Java It is an inner class without a name and for which only a single object is created. An anonymous… Read More »

www.geeksforgeeks.org

 

Inner class in java - GeeksforGeeks

Inner class means one class which is a member of another class. There are basically four types of inner classes in java. 1) Nested Inner… Read More »

www.geeksforgeeks.org

앞에 살펴본 예를 무명클래스로 바꾸면 다음과 같습니다. 여기서 new 다음에는 상속할 슈퍼 클래스 또는 인터페이스 이름과 ()가 옵니다. 그리고 이어지는 { ... } 안에는 상속해서 만들어지는 클래스에서 오버라이드 또는 추가할 메소드를 넣어줄 수 있습니다.

class TestDialog extends JDialog {
   JButton but = new JButton("OK");
   JDialog() {
      getContentPane().add(but);
      but.addActionListener(new ActionListender() {
        public void actionPerformed(ActionEvent e) {
            but.setText(cancel);
        }
      });
   }
}   

여기서 ActionListener를 implements한 클래스를 바로 new해서 보내기 위해 무명 로컬클래스로 정의했습니다. 이 때 주의할 점은 new 뒤에 인터페이스나 클래스 타입의 이름이 오고 그것을 implements한 것이 { ... } 사이에 나타나야 한다는 점입니다. 만약 그 인터페이스나 클래스 타입에 추상 메소드가 있다면 그것을 모두 오버라이드하여 정의해 주어야 new가 될 수 있습니다. 클래스 정의와 마찬가지로 상속한 슈퍼 클래스나 인터페이스의 추상 메소드가 구현되지 않고 남아있으면 추상 클래스가 되고 추상 클래스는 new 할 수 없습니다.

이러한 무명 로컬클래스도 범위 규칙은 로컬클래스와 동일하게 적용되어서 바깥 클래스의 필드나 메소드는 모두 접근할 수 있고 바깥 함수의 지역변수는 final 또는 effectively final인 것만 접근할 수 있습니다.

이상에서 이너클래스와 로컬클래스, 무명로컬클래스까지 살펴보았습니다. 이것은 자바에서 상당히 어려운 부분에 속하지만 최근 인터페이스를 통한 메소드 전달이 람다나 스트림 API의 등장 후 중요성이 크게 높아져서 매우 중요한 내용이라고 할 수 있습니다. 잘 공부해 두시면 자바의 이해를 한 단계 업그레이드할 수 있습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함