Function과 Method

조회수 673회

Function과 Method

Function은 단 1나만 있어서 의문사항은 그렇게 생기지 않습니다만, Method에 대해서는 여러가지 의문사항이 있어서 이렇게 질문 드립니다.

Method는 Function과 같이 단 한번 선언되지만, Instance가 만들어지는 과정속에서 여러개의 Object가 만들어지고, 해당 Object들마다 각기 다른 Method들 또한 만들어지는 것 같아 보입니다.

단순하게 생각하면, 결국에는 같은 Code인데, 같은 Code를 여러개의 메모리 영역에 중복해서 나누는 것 보다는. Stack Frame 처럼, Method가 Method로서 작동할 수 있게끔(self나 this같은 키워드를 사용할 수 있게끔) Format을 취한다면 굳이 Method가 여러개일 필요가 없다는 생각이 듭니다.

실제로 C++, Java, Javascript, Python 에서는 Method가 어떠한 방식으로 구현이 되어있나요? 그냥 각각의 code들이 인스턴스가 만들어질 때 마다 메모리 영역에 올라가게 되는건가요?

Python 같은 경우에는 그냥 Method를 각 오브젝트마다 하나씩 만드는 것 같았습니다.

C++이나 Java같은 정적언어는 Method를 하나로 만들고, Stackframe 같은 장치를 이용하여 루틴을 공유하나요?

2 답변

  • 좋아요

    4

    싫어요
    채택 취소하기

    견문이 좁아 javascript 는 잘 모르겠지만 다른 언어들과 유사할 것입니다.

    말씀하신 메소드(비 정적 멤버 함수)의 경우도 일반적인 함수와 같이 함수 코드가 있는 메모리는 한곳에만 있습니다. 인스턴스 별로 메소드가 만들어지는 않습니다.

    물론 이를 표준에서 그렇지 않다라고 하지는 않지만 언어 구현을 할때 구지 메모리를 메소드를 인스턴스 별로 할당하지 않아도 처리가 가능한 부분이기 때문에 대부분의 구현에서 메소드의 코드는 동일한 위치를 갖습니다.

    그러면 클래스의 인스턴스에 따라 메소드는 어떻게 동작하느냐 인데 이는 말씀하신 것처럼 함수에서 self 나 this 키워드를 사용할 수 있게 끔 구현이 됩니다. 메소드 내에서 멤버 변수(필드)를 접근하게되면 this를 사용하지 않아도 컴파일러가 알아서 this에서 해당 멤버 변수를 접근할 수 있게 메소드를 작성합니다.

    C++를 예로 들어보면 아래와 같은 클래스가 있습니다.

    class Test {
        int a;
    public:
        void test() {
            a = 1;
        }
    };
    int main() {
        Test t;
        t.test();
    }
    

    여기서 test()의 x86-64 어셈블리를 봐보면 아래와 같습니다.

    Test::test():
      push rbp
      mov rbp, rsp
      mov QWORD PTR [rbp-8], rdi
      mov rax, QWORD PTR [rbp-8]
      mov DWORD PTR [rax], 1
      nop
      pop rbp
      ret
    main:
      push rbp
      mov rbp, rsp
      sub rsp, 16
      lea rax, [rbp-4]
      mov rdi, rax
      call Test::test()
      mov eax, 0
      leave
      ret
    

    어셈블리에서 test()란 메소드는 고정된 위치에 있지 인스턴스별로 새로 생성되지 않습니다.

    내용 중 아래와 같은 부분이 있습니다.

      lea rax, [rbp-4]
      mov rdi, rax
      call Test::test()
    

    test()를 호출하기 전에 rdi에 인스턴스의 주소를 저장하고 있습니다. 즉 rdi레지스터는 this의 의미를 갖는다 생각하시면 됩니다. 그 다음 test()내부 코드를 보면 아래와 같습니다.

      mov QWORD PTR [rbp-8], rdi
      mov rax, QWORD PTR [rbp-8]
      mov DWORD PTR [rax], 1
    

    rdi레지스터에서 주소를 읽어와 해당 주소에 1이란 값을 저장하고 있습니다. 처음 C++코드에서 a란 변수에 1을 설정했는데, 위 어셈블리는 rdi의 주소에 1을 설정합니다. Test의 처음에 a란 변수가 있기 때문에 this는 결국 a의 주소가 됩니다.

    즉, C++의 구현에서는 메소드는 인스턴스 별로 존재하는 것이 아닌 클래스 별로 존재하며 메소드 내에서 인스턴스에 접근하기 위한 this가 호출 단계에서 전달 됨을 알 수 있습니다. 통상 C++에서 메소드는 보이지 않는 첫번째 파라미터가 있고 그곳에 this가 저장되는 식으로 구현됩니다.

    Python 의 경우 언어 자체에서 메소드를 정의할 때 명확하게 어떤식으로 동작할지가 나옵니다.

    class Test:
        def __init__(self):
            self.a = 0
        def test(self):
            self.a = 1
    
    t = Test()
    t.test()
    

    Python에서 메소드를 정의할 때 첫번째 파라미터로 self를 넣게 됩니다. 메소드를 호출할 때 위와 같이 별도로 self파라미터에 대한 인자를 넣지 않습니다. 이는 인터프리터가 호출단계에서 알아서 넣습니다.

    언어적 차원에서 파라미터를 통해 인스턴스를 받게 되어는데 구지 이를 인스턴스 별로 메소드를 생성할 필요는 없고, 구현 또한 그렇습니다.

    Java의 경우도 한번 생각해 보시죠.

    class Test {
        private int a;
        public void test() {
            a = 1;
        }
        public static void main() {
            Test t = new Test();
            t.test();
        }
    }
    

    위 코드의 바이트 코드는 아래와 같습니다.

    class Test {
         <ClassVersion=52>
         <SourceFile=OtherClass.java>
    
         private int a;
    
         Test() { // <init> //()V
             L1 {
                 aload0 // reference to self
                 invokespecial java/lang/Object.<init>()V
                 return
             }
         }
    
         public test() { //()V
             L1 {
                 aload0 // reference to self
                 iconst_1
                 putfield Test.a:int
             }
             L2 {
                 return
             }
         }
    
         public static main() { //()V
             L1 {
                 new Test
                 dup
                 invokespecial Test.<init>()V
                 astore0
             }
             L2 {
                 aload0
                 invokevirtual Test.test()V
             }
             L3 {
                 return
             }
         }
    }
    

    test()를 호출하는 코드를 보면 아래와 같습니다.

                 aload0
                 invokevirtual Test.test()V
    

    aload0는 지역변수 0의 값을 스택에 넣습니다. 이는test()의 호출 인자를 입력하는 과정이며, 지역변수 0은 Test t가 됩니다. invokevirtual를 통해 Test.test()를 호출합니다.

         public test() { //()V
             L1 {
                 aload0 // reference to self
                 iconst_1
                 putfield Test.a:int
             }
             L2 {
                 return
             }
         }
    

    test()에서 aload0는 지역변수 0을 스택에 넣는 명령입니다. 하지만 실제 Java 코드에서는 지역변수가 없지만, invokevirtual에서 스택에 쌓은 값이 지역변수가 됩니다. 보이진 않지만 this가 파라미터를 통해 전달됩니다.

    즉, Java 역시 스택을 통해 인스턴스의 주소를 전달하지 메소드를 인스턴스 별로 생성하지 않습니다.

    물론 모든 언어나 언어의 구현이 이런식으로 동작하지 않을 수 있습니다. 하지만 대부분의 경우 메소드는 한곳에 존재하고 인스턴스의 구분은 보이지 않는 파라미터를 통해 접근되게 됩니다.

    • 와.. 이렇게 친절하게 답변 해 주셔서 감사합니다. dbwodlf3 2018.9.10 15:01
  • 자바스크립트는 원시 객체(Prototype Object)가 있으며 new 키워드로 인스턴스 생성시, 원시 객체에 기선언된 함수를 바라보도록 연결됩니다. 이를 프로토타입 체인이라고 하며 메모리 사용에 있어서 다른 언어의 기본적인 양상과 다르지 않을 겁니다.

    다만 자바스크립트는 함수가 일급 시민 객체이며 고계 함수 표현도 가능해서 어떤 코드를 짜느냐에 따라 클래스 인스턴스화 할 때마다 매번 메모리영역에 올라가는 함수가 생성 될 수는 있습니다. (근데 다른 언어도 마찬가지 아닐까요... 이건 저도 잘 몰라서...)

    자바스크립트는 정적 언어가 아니라서 컴파일 타임에 this가 해석되지 않습니다. this는 excution time에 정의됩니다. 각 인스턴스는 런타임에 자기 자신 context를 프로토 타입 메소드에 넘겨서 this를 확정합니다. 이 경우를 봐도 위에 첫 번째 주신 답변과 크게 다르지 않네요.

    • 답변 감사합니다. dbwodlf3 2018.9.10 15:01

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)