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 답변
-
견문이 좁아 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 역시 스택을 통해 인스턴스의 주소를 전달하지 메소드를 인스턴스 별로 생성하지 않습니다.
물론 모든 언어나 언어의 구현이 이런식으로 동작하지 않을 수 있습니다. 하지만 대부분의 경우 메소드는 한곳에 존재하고 인스턴스의 구분은 보이지 않는 파라미터를 통해 접근되게 됩니다.
-
자바스크립트는 원시 객체(Prototype Object)가 있으며 new 키워드로 인스턴스 생성시, 원시 객체에 기선언된 함수를 바라보도록 연결됩니다. 이를 프로토타입 체인이라고 하며 메모리 사용에 있어서 다른 언어의 기본적인 양상과 다르지 않을 겁니다.
다만 자바스크립트는 함수가 일급 시민 객체이며 고계 함수 표현도 가능해서 어떤 코드를 짜느냐에 따라 클래스 인스턴스화 할 때마다 매번 메모리영역에 올라가는 함수가 생성 될 수는 있습니다. (근데 다른 언어도 마찬가지 아닐까요... 이건 저도 잘 몰라서...)
자바스크립트는 정적 언어가 아니라서 컴파일 타임에
this
가 해석되지 않습니다.this
는 excution time에 정의됩니다. 각 인스턴스는 런타임에 자기 자신 context를 프로토 타입 메소드에 넘겨서this
를 확정합니다. 이 경우를 봐도 위에 첫 번째 주신 답변과 크게 다르지 않네요.
댓글 입력