자바의 Call by Value / Call by Reference
프로그래밍을 하다보면 반드시 마주치는 것이 바로 call by value / call by reference 개념이다.
함수의 매개변수에서 값을 복사하느냐 주소값을 참조하느냐에 따라 반환 결과가 달라지기 때문에 대부분의 프로그래밍 교육과정에선 중요시 하게 여긴다. (필자도 학부 시절에 C언어 쪽에서 접했던 기억이 있다.)
자바에서도 역시 call by value 와 call by reference 동작 차이가 존재한다.
자바의 데이터형을 알아보면 크게 두가지로 나뉘게 된다.
- 기본형(primitive type) - Boolean Type(boolean), Numeric Type(short, int, long, float, double, char)
- 참조형(reference type) - Class Type, Interface Type, Array Type, Enum Type, 기본형을 제외한 모든 것들
그런데 메소드에 입력값(파라미터)에 원시값(primitive type)을 전달하는 것과 객체(reference type)를 전달하는 것에는 큰 동작 차이가 있다.
아래 예제 코드에서는 int 타입의 변수와 int[] 배열 타입의 변수를 각각 main 메소드에서 선언 했다.
같은 정수형 데이터 이겠지만 자바에서는 이들을 각각 primitive 타입과 reference 타입으로 구분하여 인식한다.
그럼 이 두 가지 종류의 타입을 메서드의 입력값으로 전달해서 더하기 연산을 하면 어떻게 되는지 살펴 보자.
public class main
{
public static void main(String[] args)
{
Sample sample = new Sample();
int var = 1; // primitive 타입 변수 int
int[] arr = { 1 }; // reference 타입 변수 int[] 배열
// 변수 자체를 보냄 (call by value)
add_value(var);
System.out.println(var); // 1 : 값 변화가 없음
// 배열 자체를 보냄 (call by reference)
add_reference(arr);
System.out.println(arr[0]); // 101 : 값이 변화함
}
static void add_value(int var_arg) {
var_arg += 100;
}
static void add_reference(int[] arr_arg) {
arr_arg[0] += 100;
}
}
int 변수 var을 add_value 메서드의 입력값으로 보내 100을 더했지만, 출력해보니 변수의 값은 변화가 없다.
하지만 int[] 배열 변수 arr을 add_reference 메서드의 입력값으로 보내 첫번째 인덱스에 100을 더했더니 101이 출력 됨을 볼수가 있다.
똑같이 두 변수를 메서드의 입력값으로 보냈지만, 이렇게 결과의 차이가 일어나는 것이 바로 값의 복사(call by value)와 값의 주소 참조(call by reference)가 일어났기 때문이다.
Call by Value / Reference 과정
1. main 스택 프레임에 두 변수가 담기게 된다.
primitive 타입인 변수 var은 그대로 원시값 1을 지니게되며, reference 타입인 배열 변수 arr은 실제 데이터는 heap 영역에 저장되게 되고 이를 참조할 주소값을 저장하게 된다.
※ 참고
[ 스택 프레임(stack frame) ]
하나의 메서드에 필요한 메모리 덩어리를 묶어서 스택 프레임(Stack Frame)이라고 한다.
하나의 메서드당 하나의 스택 프레임이 필요하며, 메서드를 호출하기 직전 스택프레임을 자바 Stack에 생성한 후 메서드를 호출하게 된다.
스택 프레임에 쌓이는 데이터는 메서드의 매개변수, 지역변수, 리턴값 등이 있다.
만일 메서드 호출 범위가 종료되면 스택에서 제거된다.
2. add_value() 메서드에 입력값으로 변수 var을 넣어 호출한다.
add_value() 메서드가 호출되면서, add_value 스택 프레임이 생성되고 그 안에 지역변수(매개변수) var_arg가 값을 1을 받은 채 생성되게 된다.
그리고 자체 메서드 로직으로 100을 더해 값은 101이 된다.
3. var 변수값은 변하지 않는다.
add_value 스택 프레임 안에 있는 변수 var_arg가 바뀐 것이지, main 스택 프레임 안에 있는 변수 var 가 바뀐 것이 아니다.
매개변수 var_arg는 그저 변수 var로부터 원시값을 복사하여 받은 것 뿐이기 때문이다. (call by value)
즉, 메인에 정의되어 있는 var 변수와 add_value 메서드에 정의되어 있는 var_arg 변수는 서로 완전히 관련이 없다.
4. add_reference() 메서드에 입력값으로 변수 arr를 넣어 호출한다.
add_reference() 메서드가 호출되면서, add_reference 스택 프레임이 생성되고 그 안에 지역변수(매개변수) arr_arg가 생성된다.
이때도 전의 add_value() 메서드 호출 때 처럼 변수의 값이 복사되어 파라미터에 넘겨지는데, 자세히 살펴보니 스택 프레임에 있는 arr 변수가 들고 있는 값은 주소값 이다.
그래서 메서드의 입력값으로 주소값이 복사되어 넘겨지게 된다.
따라서 결과적으로 두 변수 arr 와 arr_arg는 같은 주소값을 들고 있게 되는 것이고, 이 주소가 가리키는 메모리는 같기 때문에 두 변수는 하나의 데이터를 동시에 참조하고 있다고 말 할 수 있다.
마지막으로 arr_arg 변수가 가리키는 값 1을 불러와 100을 더하니 heap 영역에 있는 데이터는 101로 변경되게 된다.
※ 참고
참조값(주소값) 복사는 배열 뿐만 아니라 클래스, 리스트, 맵 등 primitive 타입이 아닌 모든 타입에 대해서 적용된다.
5. 마지막으로 변수 arr을 출력해보면 값이 변경됨을 알 수 있다.
자바에는 Call by Reference 개념이 없다
여기 까지 보면 call by value / call by reference 답게 값 복사냐 주소 참조냐 동작 차이가 난다. 그렇다면 JAVA는 call by value / call by reference 둘 다 지원하는 것일까?
사실 자바(Java) 프로그래밍 언어에서는 call by reference 라는 것은 존재하지 않는다.
왜냐하면 C 와 달리 자바에서는 포인터를 철저하게 숨겨 개발자가 직접 메모리 주소에 접근 하지 못하게 조치했기 때문이다.
※ 참고
C언어는 포인터를 통해 그대로 주소를 통해 메모리를 참조할 수 있다
결론적으로 말하면, 자바에서의 파라미터는 call by value로서만 동작되며, 원시값이 복사 되느냐 주소값이 복사되느냐 차이가 있을뿐이다.
그리고 매개변수에 복사된 값에 따라, 원시값이면 바로 연산을하고 주소값이면 해당 메모리 주소를 참조해 값을 가져와 연산할 뿐이다.
C언어의 Call by Reference
자바에 call by reference 가 없다면 대체 call by reference가 무얼 뜻하는 것일까?
C언어에서는 포인터(*) 변수를 파라미터로 받게 하고 함수를 호출할때 주소연산자(&) 를 이용해 주소값을 직접 넘겨 직접적인 메모리 참조가 가능하다.
#include <stido.h>
void swap(int *, int *);
int main() {
int a = 10;
int b = 20;
swap(&a, &b);
}
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
1. 최초 실행 시 다음과 같이 메모리가 할당된다.
2. swap() 함수를 호출한다.
이때 입력값으로 변수 a, b를 주소연산자를 이용해 &a, &b로 주소를 인자값으로 넘기기 때문에 swap() 함수가 호출되면, 다음과 같이 swap() 함수의 포인터 매개변수 *a, *b에 주소값들이 대입된다.
3. swap 함수 로직을 실행한다
변수 a 와 변수 b의 값을 서로 치환하는 로직인데, 이때 swap() 함수 내의 변수들은 포인터 변수이기 때문에 직접 메모리 주소를 참조해 전혀 다른 곳에 있는 main() 함수안에 있는 변수 a, b 값을 변경하게 된다.
※ 참고
call by reference 로 변수의 값을 다루게 되면, 메모리를 절약할 수 있으며 속도도 빨라진다는 장점이 있기 때문에 대부분의 임베디드 환경에서는 C 포인터를 이용해 코딩한다.
아직도 C언어의 포인터 변수와 자바의 참조 변수의 차이가 애매하면 이렇게 이해하면 된다.
C언어는 포인터를 이용해 주소값을 직접 가져와 전달할 수 있다.
이 말은 주소값을 몰래 변조하여 다른 소스 파일에서 이용되는 주소값을 가져올 수 있다는 말이며, 이를 악용하면 핵(hack)과 같은 불법 변질 프로그램이 될 수 있다.
반면 Java에서는 이러한 부분을 철저하게 봉쇄하였다.
사실 자바도 포인터로 메모리가 참조되지만 자바는 포인터를 철저하게 숨겨 개발자가 포인터를 이용하지 못하도록 막아 버린 것이다.
그래서 주소값을 복사를 통해 이용이 가능할뿐 직접 엑세스해서 만질 수 없는 것이다.
자바는 Call by Value / Call by Address
이처럼 자바는 오로지 call by value 로서 동작한다.
그래도 원시값을 복사하느냐, 주소값은 복사하느냐에 따라 반환 결과가 달라지기 때문에 세간에서는 이 둘을 구분하기 위해 call by value / call by address 로 명명 지어 구분하기도 한다. (어차피 똑같은 value 복사지만 굳이 구분하자면 )
※ 참고
자바스크립트(JavaScript) 진영에서도 똑같이 call by value / call by address 로 구분한다.
다만 외부적인 관점에서 보면 주소값이 복사되든, 포인터를 이용해 참조하든, 메모리 안에 있는 데이터를 참조해 연산하는 동작 자체는 같기 때문에,
솔직하게 말하면 자바가 call by reference가 아니고 call by address 라 그러지만 사용하는 입장에서는 둘은 차이가 없다고 보면 된다.
참고
- https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
- https://programist.tistory.com/35