티스토리 뷰
개발을 하다 보면 함수의 파라미터로 변수를 넘겨주어야 한다. 각 언어마다 변수를 넘겨주는 방법(Pass By Value, Pass By Reference)이 다른데, 이를 정확히 인지하지 못하면 예상치 못한 버그를 발생시킬 수 있다. 이번에는 두가지 방법의 차이점을 알아보도록 하자.
아래의 내용은 dzone.com/articles/pass-by-value-vs-reference-in-java 를 바탕으로 작성하였습니다.
1. Pass By Value(Call By Value)에 대한 이해
[ Pass By Value(값에 의한 전달)의 의미 ]
Pass By Value(값에 의한 전달)는 복사된 데이터를 전달하여 구성함으로써, 값을 수정하여도 원본의 데이터에는 영향을 주지 않도록 하는 방식이다.
예를 들어 다음과 같은 어떤 int값을 파라미터로 넘기는 process 함수가 있다라고 가정하자.
#include <iostream>
using namespace std;
void process(int value) {
cout << "Value passed into function: " << value << endl;
value = 10;
cout << "Value before leaving function: " << value << endl;
}
int main() {
int someValue = 7;
cout << "Value before function call: " << someValue << endl;
process(someValue);
cout << "Value after function call: " << someValue << endl;
return 0;
}
위의 process 함수는 매개변수로 전달받은 int형 값인 value를 10으로 변경하고 있다. 위의 프로그램을 실행한 결과를 예측해보고, 실제 출력 결과와 비교해보자.
Value before function call: 7
Value passed into function: 7
Value before leaving function: 10
Value after function call: 7
process 내에서 해당 값을 수정하여도, 해당 해당 함수가 종료되고 나면 원본의 값은 기존의 상태를 유지하고 있다. 그러한 이유는 해당 과정이 Pass By Value 방식으로 동작하였기 때문이며, 이를 함수의 Call Stack을 통해 자세히 살펴보도록 하자.
[ Pass By Value(값에 의한 전달)의 동작 방식 ]
우리가 어떤 함수를 호출한다고 하면 Stack 메모리에 먼저 함수의 Return Address가 쌓이게 되고 그 위에 매개변수 등의 값이 쌓이게 된다. 위에서 보여준 코드를 기준으로 메모리 할당을 그림으로 표현하면 다음과 같다.
먼저 우리는 main 함수를 호출하였기 때문에 main 함수의 내용이 Stack에 먼저 존재할 것이고, 그 중에서 지역 변수로 선언한 someValue가 존재할 것이다. 이후에 process라는 함수를 호출하는 상황이라고 한다면, 먼저 Return Address가 Stack에 쌓이고, 그 다음으로 process의 파라미터인 int형 변수 value 역시 쌓이게 된다.
그런데 여기서 우리가 주목해야 할 부분은 메모리에 할당된 main 함수의 someValue는 그대로 유지된 상태로, 7이라는 값만을 참고하고 복사하여 새롭게 메모리를 할당한다는 점이다. 실제로 main함수의 someValue의 주소값은 0x07040 이지만, process함수의 파라미터인 value의 주소값은 0x07000 이라는 점에서 두 변수는 값만 같을뿐 다른 존재임을 확인할 수 있다.
그렇다면 process 함수로 전달받은 파라미터 value에 어떤 값이 더해지고 빼지고 곱해진 상태로 process 함수가 종료된다면 어떻게 되겠는가? process를 위해 할당된 Call Stack이 pop될 것이고 해당 메모리는 모두 소멸될 것이다.
그렇기 때문에 process가 종료된 이후 main 함수에서 someValue를 다시 출력하여 보아도 7이라는 기존의 값을 유지하는 것이다. 그리고 이렇게 어떤 함수를 호출할 때 파라미터로 값을 복사하여 전달하는 방식을 Pass By Value라고 한다.
2. Pass By Reference(Call By Reference)에 대한 이해
[ Pass By Reference(참조에 의한 전달)의 의미 ]
몇몇 언어들에서는 pass by reference와 pass by pointer를 다른 기술로 정의하기도 한다. 하지만 이론적으로 pass by reference가 어떤 alias를 구성하여 실제 값에 접근하는 것이라는 관점에서, pass by pointer는 alias로 주소값을 넘겨주었을 뿐이기 때문에 동일한 기술이라고 볼 수 있다.
Pass By Reference(참조에 의한 전달)는 주소 값을 전달하여 실제 값에 대한 Alias를 구성함으로써, 값을 수정하면 원본의 데이터가 수정되도록 하는 방식이다. C++에서는 참조값을 전달하기 위해 &를 사용하는데, 위에서 설명했던 process 함수를 참조값을 전달하도록 수정하면 다음과 같다.
#include <iostream>
using namespace std;
// value 앞에 &가 추가됨
void process(int& value) {
cout << "Value passed into function: " << value << endl;
value = 10;
cout << "Value before leaving function: " << value << endl;
}
int main() {
int someValue = 7;
cout << "Value before function call: " << someValue << endl;
process(someValue);
cout << "Value after function call: " << someValue << endl;
return 0;
}
위의 process 함수 역시 매개변수로 전달받은 int형 값인 value를 10으로 변경하고 있다. 위의 프로그램을 실행한 결과를 예측해보고, 실제 출력 결과와 비교해보자.
Value before function call: 7
Value passed into function: 7
Value before leaving function: 10
Value after function call: 10
이번에는 실제 값에 대한 Alias를 넘겼기 때문에 process 함수에서 값을 변경하면 실제 main 함수의 someValue에도 영향을 주게 된다. 이러한 Pass By Reference의 동작 방식 역시 함수의 Call Stack을 통해 자세히 살펴보도록 하자.
[ Pass By Reference(참조에 의한 전달)의 동작 방식 ]
앞에서 살펴봤던 것과 마찬가지로 Stack 메모리에 먼저 process 함수의 Return Address가 쌓이게 되고 그 위에 매개변수 등의 값이 쌓이게 된다. 하지만 Pass By Value에서는 실제 값이 쌓였던 것과 다르게 Pass By Reference에서는 실제 값의 주소가 쌓이게 된다. 이를 그림으로 표현하면 다음과 같다.
process 함수에서 파라미터로 전달받은 value는 someValue가 저장된 값을 참조하는 Alias이며 value를 10으로 변경하는 것은 value가 가리키는 메모리(실제 someValue가 저장되어 있는 메모리)의 값을 7에서 10으로 변경하는 것이다. 그렇기 때문에 process 함수 종료 후에 someValue를 출력하여 확인해보면 10으로 변경이 된 것을 확인할 수 있다.
3. Java에서 Pass By Value의 동작 방식
[ Java에서 Pass By Value의 동작 방식 ]
앞의 포스팅에서 Java는 Pass By Value로 동작한다고 설명하였다. 실제로 Java Language Specification의 (Section 4.3) 에서는 원시 값이든 객체든 상관없이 모든 데이터를 Pass By Value로 전달한다고 나와 있다.
이러한 규칙은 표면적으로 상당히 쉬워보인다. 하지만 앞의 포스팅에서 살펴보았듯 원시 값은 Stack 메모리에 실제 값이 저장되고, 객체는 실제 메모리를 참고하기 위한 값이 저장된다는 것을 확인하였다. 그렇기 때문에 Java에서는 객체에 한해 확장된 규칙이 적용된다. 그것은 개체와 관련되어 복사 후 전달되는 값은 실제 메모리를 가리키는 Reference(참조값)인 포인터라는 것이다.
Java에서 객체를 생성할 때 Dog dog = new Dog()과 같은 표현을 사용한다. 여기서 dog은 실제로 생성된 Dog 클래스의 객체를 저장하고 있는 것이 아니고, Dog 클래스의 객체가 저장된 주소값(포인터 값)을 가지고 있는 것이다. 그리고 Java에서 객체를 전달한다고 하면 이러한 dog 변수가 복제되어 전달된다. Object Section (Section 4.3.1)에 따르면 Java 언어에서는 이를 Object Reference라고 부르며, 객체가 전달될 때마다 복제된다.
Object Reference를 통해서는 다음과 같은 연산들이 가능하다.
- Field 값으로의 접근
- Method Invocation
- The Cast Operator
- String의 + 연산자
- instanceof 연산자
- == 또는 != 또는 ? :
그렇기 때문에 우리는 Java에서 어떤 객체가 파라미터로 전달되었을 때, 필드값에 접근하여 해당 값을 수정하는 것은 가능하지만 그 객체 자체는 변경 불가능한 것이다. 이를 그림으로 표현하면 다음과 같다.
기존의 예시와는 다르게 이번에는 main 함수에서 Foo 클래스에 대한 객체를 생성하고, 이를 process 함수의 파라미터로 넘기고 있다. 앞서 설명한대로 Java에서는 Pass By Value에 따라 Foo 객체가 존재하는 주소(0x07070)를 갖는 someFoo 변수를 복사하여 전달함으로써, process의 함수가 주소값을 통해 필드값에 접근하는 것을 도와주고 있다. 하지만 process가 종료되면 copied pointer(0x07000)는 소멸되고, foo 객체 자체에 변경사항이 있었다면 해당 부분 역시 반영되지 않는다.
분명 누군가는 Java의 객체 전달 방식이 Pass By Reference가 아닌가 혼란이 올 수 있다. 하지만 앞서 정의되었던 Pass By Reference의 정의를 살펴보면 이를 해결할 수 있다. Pass By Reference(참조에 의한 전달)는 주소 값을 전달하여 실제 값에 대한 Alias를 구성함으로써, 값을 수정하면 원본의 데이터가 수정되도록 하는 방식이라고 정의하였다. Java에서 객체를 전달하는 방식에는 분명 주소값이 전달되고 있지만, 이는 그저 someFoo에 대한 복사본일 뿐이다. 물론 process의 파라미터로 전달받은 foo 역시도 실제 주소값을 참조하고 있기 때문에 foo의 setValue를 통해 실제 객체의 필드값을 수정하면 변하게 된다. 하지만 이는 그저 객체의 주소값으로 객체의 필드 값에 접근하여 값을 변경하는 것일 뿐, 실제 객체 자체에 변화를 주는 것이 아니다.
이를 이해하기 위해 process에 객체를 변경하는 코드를 추가하여 살펴보도록 하자.
class Foo {
private int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
Foo someFoo = new Foo(7);
process(someFoo);
System.out.println(someFoo.getValue());
}
public static void process(Foo foo) {
// 주소 값을 통해 필드 값을 변경
foo.setValue(10);
// 객체 자체를 변경하는 것은 영향 X
foo = new Foo(15);
}
}
위의 process 함수에서는 파라미터로 전달받은 foo 객체를 15라는 값을 지닌 새로운 객체로 변경하고 있다. process 함수가 종료되면 어떻게 되겠는가? 당연히 10이라는 값을 유지하게 된다. 그러한 이유는 foo는 Pass By Reference처럼 실제 객체에 대한 Alias가 아니라, 그저 someValue가 복사되어 전달된 Reference이기 때문이며, Java가 Pass By Value 방식으로 동작하기 때문이다. Java가 Pass By Reference 방식으로 동작하였다면 process 함수가 종료된 후에도 15라는 값으로 유지되었을 것이다.
4. 요약
[ Pass By Value와 Pass By Reference 차이 ]
관련 포스팅
- Pass By Value 실행 결과 예측해보기 (1/3)
- 메모리 관리 및 Pass By Value의 동작 방식 (2/3)
- Pass By Value와 Pass By Reference의 차이 및 이해 (3/3)
'Java & Kotlin' 카테고리의 다른 글
[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface) - (2/5) (15) | 2021.01.22 |
---|---|
[Java] Stream API에 대한 이해 - (1/5) (16) | 2021.01.22 |
[Java] 메모리 관리 및 Pass By Value의 동작 방식 (2/3) (13) | 2021.01.18 |
[Java] Pass By Value 실행 결과 예측해보기 (1/3) (2) | 2021.01.18 |
[Java] equals와 hashCode 함수 (21) | 2020.10.29 |