일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- docker
- container
- 방화벽
- mybatis
- 소켓
- JPA
- Set
- Jenkins
- Collection
- 라우터
- sonarQube
- Pipeline
- 허브
- Java
- 액세스회선
- tomcat
- Linux
- jdk
- DevOps
- 캐시서버
- Spring
- STREAM
- LAN어댑터
- post
- AOP
- map
- ansible
- IntelliJ
- cloud
- gradle
- Today
- Total
거북이-https://velog.io/@violet_evgadn 이전완료
Call-by-Value & Call-by-Reference 본문
함수 호출
우리는 흔히 함수를 사용할 때 "함수이름()" 문구를 활용한다.
이렇게 함수 이름을 통해 특정 함수를 활용하는 것을 "함수의 호출"이라고 한다.
만약 호출한 함수가 Parameter를 가지고 있다면 우리는 함수를 호출하는 과정에서 인자를 전달해줘야 한다.
이때 인자를 전달하는 대표적인 방법이 Call by Value와 Call by Reference 방식인 것이다.
프로그래밍 언어별로 인자 전달 방식에도 약간 차이가 존재한다.
Python에서는 Passed by assignment라고 불리는 Call by Value와 Call by Reference의 혼합 방식을 사용하며, Java는 Call by Value만 사용하지만 C 언어의 Call by Value와는 다른 동작 방식을 가진다.
일단 가장 일반적으로 알려진 C언어의 Call by Value와 Call by Reference를 알아본 후 Python의 Passed by assignment를 알아보고 마지막으로 Java에서의 Call-by-Value에 대해 알아보자.
Call by Value
Call by Value는 "값에 의한 호출"이라는 의미로 인자로 받은 값을 새로운 공간에 복사하여 Paramter로 전달한다.
사진을 통해 Call by Value 동작 방식에 대해 알아보자.
◎ 코드 & 이미지로 보는 Call by value
0. 상황 설명
a와 b 값을 교체하는 아래 코드의 swap(int a, int b)라는 메서드를 호출하는 상황이라 가정하자.
void swap(int a, int b){
int tmp = 0;
tmp = a;
a = b;
b = tmp;
}
1. main에 a와 b라는 데이터 존재
int a = 3;
int b = 4;
위 사진처럼 C언어 Stack 영역에는 Main에서 지정된 a = 3 & b = 4가 저장되어 있을 것이다.
2. main에 지정한 a와 b를 swap의 인자로 전달
swap(a, b)
Call by Value 방법으로 인자가 전달되는 단계이다.
Call by Value는 위에서 말했듯 인자를 전달할 때 값을 새로운 공간에 복사하여 복사한 값을 Parmeter로 전달한다.
swap(a,b) 함수가 실행될 때 먼저 a 값을 복사한 a', b 값을 복사한 b'이 생성된 후 swap 함수에서 사용될 값으로 전달된다.
마지막으로 swap 함수가 종료될 때 a'과 b'은 (값이 Return 되는 것이 아니라면) 메모리 상에서 제거된다.
◎ Call by Value 특징 & 장단점
Call by Value는 함수를 호출하는 Caller에 선언된 데이터 값을 다른 공간에 복사하여 Callee에게 보내기 때문에 Callee에서 Parmeter로 전달한 데이터가 수정된다 하더라도 Caller에 저장된 데이터 값은 보존된다.
따라서 Call by Value는 원래 선언되어 있던 데이터를 안전하게 보존할 수 있다는 장점을 가진다.
하지만 Call by Value로 인자를 전달할 경우 함수가 호출될 때마다 인자 값이 복사되어야 한다.
만약 재귀함수를 활용할 경우 이전까지 수행되었던 재귀 함수의 Parmeter값 모두가 각각 메모리 공간을 차지하고 있기 때문에 메모리 상 매우 부담이 가는 함수 인자 전달 방식이다.
또한 인자 값이 복사되는 과정 및 메모리 상에서 삭제되는 과정에도 자원이 소모된다.
따라서 함수가 많이 호출되는 경우 핵심 로직과는 거리가 먼 데이터를 복사 및 삭제하는 과정에 너무 많은 자원이 소모될 것이다.
◎ Parameter가 객체일 경우 Call by Value
다른 블로그들을 찾아보더라도 대부분 Call by Value 예시는 "swap(int a, int b)"이다.
하지만 필자가 궁금한건 "swap(Member a, Member b)"처럼 Parmeter로써 객체가 들어가는 경우였다.
(이 부분은 필자가 C언어 자료구조에 대해 공부했기 때문에 든 의문이다. 만약 정확히 알고 싶다면 C언어 메모리 구조부터 공부하고 오자)
int형 자료는 Stack 공간에 데이터가 보관되기 때문에 Call by Value 방식이 어떻게 동작하는지 쉽게 알 수 있었다
하지만 객체 데이터는 변수가 주솟값을 저장하고 있고 Heap 영역의 주솟값 공간에 실제 값이 저장되어 있는 형태이다.
즉, Stack 영역의 데이터를 복사한다 하더라도 Stack 영역의 변수는 주솟값을 저장하고 있기 때문에 결과적으로 주솟값이 복사되는 것이고 이는 Call by Reference 방식으로 변수 전달이 이뤄지지 않을까라는 생각이 들었다.
결과만 말하자면 "C언어에서는 아니다"이다.
C언어에서는 아래 사진과 같이 객체 Parmeter에 대한 Call by Value 전달 방식이 이뤄진다.
위 사진처럼 C언어에서는 Member A와 똑같은 공간만큼 A'을 위한 공간을 만들고 A의 name & age 값을 복사한다.
즉, C언어에서는 Call by Value의 경우 Stack 영역에 저장된 데이터가 아닌 Stack 영역에 저장된 주솟값에 저장된 값 그 자체를 모두 다른 공간에 복사시켜 활용하는 것이다.
따라서 위에서 배웠던대로 Call by Value는 원본 객체에 영향을 주지 않게 되는 것이다.
#include <stdio.h>
struct Member {
unsigned int age;
char name[10];
};
void swapAge(struct Member m1, struct Member m2) {
int tmp = m1.age;
m1.age = m2.age;
m2.age = tmp;
printf("Swap 이후 Swap 함수 내 m1 - 나이 : %d, 이름 : %s\n", m1.age, m1.name);
printf("Swap 이후 Swap 함수 내 m2 - 나이 : %d, 이름 : %s\n", m2.age, m2.name);
}
int main()
{
struct Member child = { 10, "Children" };
struct Member adult = { 30, "Adult" };
swapAge(child, adult);
printf("\n");
printf("Swap 이후 Main 함수 내 child - 나이 : %d, 이름 : %s\n", child.age, child.name);
printf("Swap 이후 Main 함수 내 adult - 나이 : %d, 이름 : %s\n", adult.age, adult.name);
return 0;
}
위 실행 결과를 보면 Swap 함수 내에서는 나이가 바뀐 상태이지만 Main 함수에서는 나이가 바뀌지 않은 상태이다.
즉, Call by Value 특징처럼 Callee 함수 내 값의 변경이 Caller의 변수에 영향을 주지 않는 것이다.
Call by Reference
Call by Reference는 "참조에 의한 호출"이라는 의미로 인자로 받은 객체의 주솟값을 Parameter로 전달한다.
주솟값이 같다는 말은 동일한 객체라는 말과 같으므로 Caller와 Callee에서 활용하는 Parmaeter는 "완전히 동일한 변수"이다.
Call by Reference는 인자로 주솟값을 전달하기 때문에 이미 존재하고 있는 객체를 전달하는 것이다.
직접 참조를 수행하고 값을 복사할 필요가 없기 때문에 Call by Value 방식보다 빠르고 자원 또한 덜 소모한다.
하지만 주솟값을 Parameter로 전달하여 직접 참조를 수행하기 때문에 만약 Callee에서 값이 변경될 경우 주솟값에 저장된 값이 변경되는 것이므로 같은 주솟값을 바라보고 있는 Caller 객체 또한 값이 변경된다.
즉, Callee의 값 변경이 Caller의 객체에 영향을 끼치는 것이다.
Passed by assignment
Python이 사용하는 매개변수 전달 방식이다.
정말 특이한 방식인데, 어떤 값을 전달하느냐에 따라 매개변수 전달 방식이 다르다.
파이썬의 자료형은 (int, str) 같은 불변(immutable)이 있고 (list, dictionary) 같은 가변(mutable)이 존재한다.
이때 Parameter로써 불변 타입의 객체를 넘기면 "Call by Value"로 넘기고, 가변 타입의 객체를 넘기면 "Call by Reference" 방식으로 넘긴다.
이는 파이썬에서는 모든 것이 객체이기 때문에 가능한 일이다.
파이썬의 모든 객체는 "불변", "가변"으로 이분화시킬 수 있기 때문에 객체 형태에 따라 매개변수 전달 방식을 다르게 하는 것이 다른 프로그래밍 언어보다 훨씬 간편하다.
왜 불변 - Call by Value, 가변 - Call by Reference인지 알기 전 불변 객체와 가변 객체부터 가볍게 알아볼 필요가 있다.
불변(Immutable) 객체란 연산을 수행할 때마다 기본 Object를 변경하는 것이 아닌 새로운 Object를 생성하는 것이다.
반대로 가변(Mutable) 객체는 이전에 선언되었던 Object를 계속해서 사용하는 것이다.
Java를 예를 들어 설명해보자. int a = 3과 int b = 4가 있다고 가정하자. 그리고 int 자료형은 불변 객체이다.
b = a 명령이 수행될 경우 기존에 저장되어 있던 a값인 3을 사용해 b 값에 적용하는 것이 아닌 a값을 다른 공간에 새로 선언하고 새롭게 선언된 "3"이라는 값을 b에 적용하는 것이다.
이제 왜 불변 - Call by Value, 가변 - Call by Reference인지 이해가 갈 것이다.
Python에서 불변 객체를 넘길 경우 기존에 저장되어 있던 값이 아닌, "기존에 저장되어 있던 값을 새로운 공간에 선언한 값"을 Parameter로써 넘겨준다.
즉, Call by Value 방식인 것이다.
반대로 가변 객체를 넘길 경우 기존에 저장되어 있던 객체를 그대로 사용하기 때문에 주솟값을 넘기는 Call by Reference 방식을 활용하는 것이다.
Java에서의 함수 호출
먼저 알아둬야 할건 Java는 무조건 Call-by-Value만 사용한다는 것이다.
음? 뭔가 이상하다. 분명히 Java에서 Parameter로 Array나 Collection이 Callee에 전달된 후 값이 변경된다면 Caller 함수에서도 값이 변경되는데 이는 Call by Reference 특징이 아닌가?
이는 Java의 Call-by-Value가 C언어와는 다르게 특이하게 동작하기 때문이다.
아래 사이트에 이미지와 같이 설명이 잘 되어있으므로 읽어보는 것을 추천한다.
https://bcp0109.tistory.com/360
그렇다면 이미지를 통해 Java의 Call by Value에 대해 알아보자.
◎ JVM Memeroy 변수 저장 위치
Java에서는 보다시피 Primitive Type 자료들은 Stack 영역에 값까지 모두 저장시키고, Reference Type은 Heap 영역에 실제 데이터가 저장되어 있는 상태에서 Stack 영역에는 주솟값만 저장시킨다.
참고로 Wrapper Class에 대해 궁금할 사람이 있을 텐데, Wrapper Class는 Reference Type의 일종이라고 보면 된다.
Integer 코드를 살펴보자.
public final class Integer extends Number implements Comparable<Integer> {
...
private final int value;
...
}
즉, Integer이라는 Class는 "int value"라는 Primitive 원소를 Heap 영역에 저장하는 Reference Type임을 알 수 있다.
또한 "final"이 붙은 것으로 변경이 불가한 값이라는 것도 알 수 있다. 즉, Integer 값이 변경된다는 것은 새로운 Integer Class가 만들어지고 새롭게 만들어진 Integer Class를 사용한다는 의미이므로 "불변 객체"임을 알 수 있다.
◎ Primitive Type Parameter 전달
아마 Java를 많이 사용했다면 Primitive Type은 인자로 전달된 후 Callee에서 값을 변경해도 Caller에서는 값이 변경되지 않음을 알고 있을 것이다.
Primitive Type은 Call by Value임이 자명하므로 설명은 생략하겠다.
◎ Reference Type Parameter 전달
이 부분이 오늘 설명의 핵심이 아닐까 싶다.
왜 Java는 Call by Value로 데이터를 전달한다고 하는데 Reference Type이 전달될 경우 Call by Reference처럼 동작하는 것일까?
정답부터 말하자면 "Reference 자체를 전달하는게 아니라 주솟값만 전달해주고 Callee는 주솟값을 보고만 있는 상태"로 Parameter를 전달한다.
먼저 test() 메서드가 실행될 때 User01과 User02의 주솟값은 각각 Stack 영역의 a와 b에 저장될 것이다.
그리고 실제 User01과 User02의 값은 Heap 영역에 저장되어 있다.
이제 test()라는 Caller 함수에서 modify()라는 Callee 함수를 호출했다.
이 과정에서 User01과 User02가 Parmeter로 전달하고, 결과는 위 이미지와 같다.
상황이 꽤 재밌다. modify()라는 메서드는 a, b와는 다른 a', b'이라는 새로운 영역을 Stack 공간에 만들었다.
그런데 이 a'과 b'이 각각 a의 주솟값에 저장된 User01, b의 주솟값에 저장된 User02를 바라보고 있다.
즉, a와 a'은 엄연히 다른 Stack 영역에 선언된 다른 값이지만, 저장하고 있는 주솟값은 같은 상황인 것이다.
a와 a', b와 b'이 다르다는 것은 b'(혹은 a')에 새로운 User03이라는 객체를 선언해보면 알 수 있다.
위 이미지처럼 b'에 User03이라는 새로운 객체를 선언할 경우 원래 바라보고 있던 User02 주솟값이 아닌 새롭게 선언된 User03의 주솟값을 새롭게 저장할 것이다.
그런데 b와 b'은 엄연히 다른 Stack 영역에 저장되어 있으므로 원래 b'이 바라보고 있던 User02에는 아무런 변경사항이 없다.
단지 User02를 바라보고 있는 객체가 b 하나가 될 뿐이다.
또한 위 이미지에서 왜 Java에서 Call by Reference를 사용하는 것과 같이 느껴지는지도 나온다.
원래 User01의 age=10이였지만 위 이미지에서는 11 임을 볼 수 있다.
이때 User01은 Callee인 modify의 a' 값을 변경시킨 것이다.
물론 a와 a'은 엄연히 다른 Stack 영역에 저장된 다른 값이지만, 같은 주솟값을 바라보고 있음은 잊으면 안 된다.
즉, a'을 변경하면 User01의 값이 변경되는 것인데 이는 User01의 주솟값을 바라보고 있는 a 또한 User01의 변경된 값을 확인할 수 있는 것이다.
마지막으로 modify() 함수가 종료된다면 a'과 b'은 Stack 영역에서 사라질 것이므로 User03의 주솟값을 저장하고 있는 객체는 사라질 것이다.
따라서 User03은 GC에 의해 메모리 상에서 삭제되는 것이다.
◎ Call by Value인 이유
결국 주솟값을 넘기네? Call by Reference네?라고 생각할 수도 있다.
하지만 Call by Reference는 주소값 자체의 소유권을 Callee에 줘버린다. 따라서 만약 Callee에서 Parameter로써 받은 주솟값에 새로운 객체를 선언할 경우 Caller의 객체 또한 새롭게 선언된 객체를 바라보게 된다.
하지만 Java에서는 Stack 영역에서 다른 2개의 데이터가 같은 주솟값을 "바라만"보고 있는 상태이므로 Callee에서 Parameter로 받은 값에 새로운 객체를 선언해도 Callee Parameter 값이 바라보는 객체가 달라질 뿐 Caller 객체의 상태가 변경되지는 않는다.
즉, "Caller의 변수와 Callee의 Parameter는 Stack 영역 내에서 다른 공간에 존재하고 있지만 주솟값만 같은 것을 바라보고 있는 다른 변수" 상태이므로 Call by Value 방식이라고 하는 것이 맞을 것이다.