Java의 Call by Value

요약

자바에는 Call by Value만 존재한다.
Call by Value는 함수의 인자에 값을 전달하는 방식이고,
Call by Reference는 함수의 인자에 주소를 전달하는 방식이다.

Call by Value vs Call by Reference

프로그래밍 언어에서 함수에 인자로 전달하는 방식에 따라 나뉜다.

Call by Value

Call by Value는 함수 호출시 전달되는 변수의 값을 복사해서 전달한다.
이렇게 전달된 인자는 외부에서 있었던 변수와는 달리 함수 내부의 지역 변수로 활용된다.
즉 함수 안에서 인자를 변경해도 외부 변수 값은 변경되지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CallByValueTest {

private void swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}

@Test
public void swapTest() {
int a = 1;
int b = 2;
swap(1, 2);
assertThat(a == 2 && b == 1).isTrue();
}
}

위 테스트 코드를 살펴보자.
swap 함수는 두 int형 변수의 값을 바꾸는 함수다. 그런데 테스트 결과는 맞지 않게 나온다.
원인은 swap 함수에서 변수를 가져오는 과정이 Call by Value이기 때문이다.

테스트 코드에서 전달한 a와 swap 내부에서의 a는 서로 영향을 주지 않는다.
테스트 코드의 a에 저장된 1이라는 값을 복사해서 메서드의 a에 저장한 것일 뿐이다.

Call by Reference

Call by Reference는 함수 호출 시 전달되는 변수의 참조값을 전달하는 방식이다.
함수가 인자로 주소값을 전달받고나서 이를 함수 내부에서 접근하여 수정하면,
함수 외부의 변수도 변화가 생길 수 있다.

Java의 Call by Value

Java는 전달되는 인자의 타입에 따라 약간 다르게 보인다.

원시 타입

자바는 8가지 원시 타입이 있다.
(byte, short, int, long, float, double, char, boolean)

원시 타입 변수들은 스택 메모리에 그대로 저장된다.
그래서 원시 타입이 인자로 전달될 때는 스택에 저장된 값 그대로 복사되어서 전달된다.

전달된 값은 원래 변수와는 다른 별개의 변수. 즉 Call by Value 방식으로 전달된다.
전달된 갑쇼은 해당 메서드가 종료되면 스택에서 제거된다.

예시로 이해하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CallByValueTest {

@Test
public void swapTest() {
int a = 1;
int b = 2; //1단계
swap(1, 2); //2단계
assertThat(a == 2 && b == 1).isTrue();
}

private void swap(int c, int d) {
int tmp = c;
c = d;
d = tmp; //3단계
}
}

아까 본 예시로 다시 이해해보자.

1단계까지 실행하고 나면 스택에는 a = 1, b = 2 라는 정보를 저장하게 된다.
2단계를 실행하고 나면 a = 1, b = 2 라는 정보에 새로운 스택 프레임이 생기고 그곳에 c = 1, d = 2가 생긴다.
즉 (a =1 , b = 2), (c = 1, d = 2) 이런 식으로 스택에 저장된다.

이제 3단계까지 실행하면 스택은 (a = 1, b =2), (c = 2, d = 1, tmp = 1) 이렇게 저장된다.
swap 메서드가 종료되면 swap 메서드의 프레임이 종료되므로 결국 (a = 1, b = 2)만 스택에 남는다.

참조 타입

참조 타입은 쉽게 말해서 객체의 주소를 저장하는 타입이다.
보통 기본 타입을 제외한 모든 것을 의미한다.

참조 타입은 기본 타입과 다르게 값 그 자체를 저장하지 않는다.
대신 그 값의 주소 값을 저장한다.

그렇다면 그 값의 주소는 어디의 주소를 의미하는 걸까?
그 주소는 바로 힙 영역에 있는 인스턴스의 주소를 의미한다.

즉 참조 타입은 객체의 주소를 가지고 스택에 저장되고,
그 주소는 힙 영역에 있는 인스턴스의 주소다.

앞서서 자바는 Call by Value라고 했다.
참조 타입도 예외는 아니다. Call by Value다.
다만 전달되는 Value가 주소값이라서 기본 타입과는 다르게 작동한다.

예시로 이해하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserAge {
int value;
}

public class CallByValueTest {

@Test
void callByValueObject() {
UserAge a = new UserAge();
a.value = 1;
UserAge b = new UserAge();
b.value = 2; //1단계
swap(a, b); //2단계
assertThat(a.value == 2 && b.value == 1).isTrue();
}

private void swap(UserAge c, UserAge d) {
int tmp = c.value;
c.value = d.value;
d.value = tmp;
}
}

UserAge는 int 형 데이터를 저장하는 클래스다.
이제 두개의 UserAge를 만들어서 값을 변경하는 swap 메서드에 전달해보자.

1단계까지 오면 스택에 a = UserAge 주소값1, b = UserAge 주소값2 이 오게된다.
우리가 만든 실제 객체는 힙 영역에 있고 스택의 주소값은 힙 영역을 가르키는 값이다.

2단계까지 오면 이제 새로운 스택 프레임이 생긴다.
c와 d에 a, b가 가진 값(즉 주소값)을 복사해서 넣어준다. 즉 Call by Value 방식으로 전달한다.

스택에는 (a = UserAge주소1, b = UserAge주소2), (c = UserAge주소1, d = UserAge주소2) 이렇게 저장된다.

이때 주목할 점은 a와 c가 같은 주소값을 저장하고 있고, b와 d가 같은 값을 저장하고 있다는 점이다.
이제 c를 통해 힙에 있는 인스턴스를 변경하면 나중에 a를 통해 인스턴스에 접근했을 때 값이 변경되어 있게 된다.
(b와 d도 같은 관계다.)

정리

자바는 모두 Call by Value이다.
다만 원시 타입은 값 자체를 복사해서 전달하고,
참조 타입은 참조하는 객체의 주소를 복사해서 전달한다.

참고

https://deveric.tistory.com/92
https://www.baeldung.com/java-pass-by-value-or-pass-by-reference
https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
https://kingpodo.tistory.com/54
https://johngrib.github.io/wiki/jvm-stack/#stack%EA%B3%BC-frame
https://velog.io/@ahnick/Java-Call-by-Value-Call-by-Reference

Share