2022. 8. 24. 14:29ㆍ일문일답/Java
함수 호출 방법은 크게 두가지가 있다.
- Call by value (값에 의한 호출)
- Call by reference (참조에 의한 호출)
이를 설명하기 위해, 많은 교재에서는 그림으로 예시를 들고 있다. 예를 들어, 컵에 물을 채워서 이 물을 직접 가져다가 다른 컵에서 사용하느냐, 아니면 똑같은 컵과 물을 한 컵 더 준비하여 사용을 하느냐라는 식이다. 언뜻 보면 이해가 쉬울 수 있지만, 오히려 헷갈릴 수가 있다. 이는 프로그래밍적으로 직접 접근해서 알아보는 것이 가장 확실하다.
Call by value(값에 의한 호출)는 인자로 받은 값을 복사하여 처리를 한다. Call by reference(참조에 의한 호출)는 인자로 받은 값의 주소를 참조하여 직접 값에 영향을 준다. 간단히 말해 값을 복사를 하여 처리를 하느냐, 아니면 직접 참조를 하느냐 차이인 것이다.
프로그래밍 구조상 Call by value(값에 의한 호출)를 하면 복사가 되기 때문에 메모리량이 늘어난다. 요즘에는 기기의 성능이 좋아져서 상관이 없다지만 많은 계산이 들어간다면 과부하의 원인이 된다. 하지만 복사처리가 되기 때문에 원래의 값은 영향을 받지 않아서 안전하다.
Call by value (값에 의한 호출)
- 장점 : 복사하여 처리하기 때문에 안전하다. 원래의 값이 보존이 된다.
- 단점 : 복사를 하기 때문에 메모리가 사용량이 늘어난다.
Call by reference (참조에 의한 호출)
- 장점 : 복사하지 않고 직접 참조를 하기에 빠르다.
- 단점 : 직접 참조를 하기에 원래 값이 영향을 받는다.(리스크)
예제
처음에는 C++을 배우면서 함수 호출 개념을 배우게 되는데, 대표적인 예로 'void swap(int a, int b)'를 통해 배우게 된다.
Call by value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <stdio.h>
void swap(int num1, int num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
}
void main()
{
int a = 20, b = 60;
swap(a, b);
printf("a: %d, b: %d", a, b);
}
|
cs |
swap()에서 값만 받아와서 내부적으로 처리를 하고 아무 것도 넘기지를 않는다. 변수를 주소로 가져오거나 포인터로 통해서 가져온 것이 아니기 때문에, 새로운 변수를 만들어서 값을 대입해서 처리한 것이다. 이 경우 교체는 되지 않고 swap() 내부에서만 처리가 된다. 반환형이 없기에 사실상 위에서는 의미 없는 행동을 했다. 만일 swap()이 아닌 다른 함수로 리턴 값을 넣었다면, 안정적으로 처리를 해서 결과를 도출해준다. 하지만 swap()의 경우 이런 방법으로 사용하면 잘못된 방법이다.
Call by reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <stdio.h>
void swap(int &num1, int &num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
}
void main()
{
int a = 20, b = 60;
swap(a, b);
printf("a: %d, b: %d", a, b);
}
|
cs |
swap()에서 main()의 a, b 주소를 가져와서 처리를 했다. 직접 주소 가져와서 처리를 했기 때문에 swap()의 내부 처리로도 a, b가 교체가 되었다. 이렇게 보듯 단점으로는 주소나 포인터를 사용하면 직접 변수에 접근하기 때문에 리스크가 있다.
자바는 Call by Value
JVM 메모리에 변수가 저장되는 위치
Java 의 Call by Value 에 대해 이해하기 위해선 먼저 변수 생성 시 메모리에 어떤 식으로 저장되는지 알아야 한다.
Java 에서 변수를 선언하면 Stack 영역에 할당된다. 원시 타입(Primitive Type)은 Stack 영역에 변수와 함께 저장되며
참조 타입 (Reference Type) 객체는 Heap 영역에 저장되고 Stack 영역에 있는 변수가 객체의 주소값을 갖고 있습니다.
그림으로 표현하면 이렇다. 원시 타입, 참조 타입을 생성할 때마다 동일한 방식으로 메모리에 할당된다. 이제 각 타입별로 파라미터를 넘겨줄 때 어떤 식으로 동작하는지 알아봅니다.
원시 타입(Primitive Type) 전달
원시 타입은 Stack 영역에 위치합니다
메서드 호출 시 넘겨받는 파라미터들도 원시 타입이라면 Stack 영역에 생성됩니다.
간단한 예시 코드와 함께 확인해봅니다.
public class PrimitiveTypeTest {
@Test
@DisplayName("Primitive Type 은 Stack 메모리에 저장되어서 변경해도 원본 변수에 영향이 없다")
void test() {
int a = 1;
int b = 2;
// Before
assertEquals(a, 1);
assertEquals(b, 2);
modify(a, b);
// After: modify(a, b) 호출 후에도 값이 변하지 않음
assertEquals(a, 1);
assertEquals(b, 2);
}
private void modify(int a, int b) {
// 여기 있는 파라미터 a, b 는 이름만 같을 뿐 test() 에 있는 a, b 와 다른 변수
a = 5;
b = 10;
}
}
위 코드에서 test()의 변수 a, b와 modify(a, b)로 전달받은 파라미터 a, b의 이름과 값은 같지만 다른 변수이다.
modify(a, b)를 호출하는 순간 Stack 영역에 새로운 변수 a, b가 새로 생성되어 총 4개의 변수가 존재한다.
그림으로 보면 한눈에 이해할 수 있다.
Stack 내부에 test() 와 modify() 라는 영역이 나뉘어져 있고 거기에 동일한 이름을 가진 변수 a, b 가 존재한다. 그래서 modify() 영역의 값을 바꿔도 test() 영역의 변수는 변화가 없게 된다.
원시 타입의 전달은 값만 전달하는 Call by Value 로 동작한다.
참조 타입(Primitive Type) 전달
참조 타입은 원시 타입과는 조금 다르다.
변수 자체는 Stack 영역에 생성되지만 실제 객체는 Heap 영역에 위치하게 된다. 그리고 Stack 에 있는 변수가 Heap에 있는 객체를 바라보고 있는 형태이다.
마찬가지로 코드 예시와 함께 알아보도록 하자.
class User {
public int age;
public User(int age) {
this.age = age;
}
}
public class ReferenceTypeTest {
@Test
@DisplayName("Reference Type 은 주소값을 넘겨 받아서 같은 객체를 바라본다" +
"그래서 변경하면 원본 변수에도 영향이 있다")
void test() {
User a = new User(10);
User b = new User(20);
// Before
assertEquals(a.age, 10);
assertEquals(b.age, 20);
modify(a, b);
// After
assertEquals(a.age, 11);
assertEquals(b.age, 20);
}
private void modify(User a, User b) {
// a, b 와 이름이 같고 같은 객체를 바라본다.
// 하지만 test 에 있는 변수와 확실히 다른 변수다.
// modify 의 a 와 test 의 a 는 같은 객체를 바라봐서 영향이 있음
a.age++;
// b 에 새로운 객체를 할당하면 가리키는 객체가 달라지고 원본에는 영향 없음
b = new User(30);
b.age++;
}
}
원시 타입 코드와 마찬가지로 동일한 변수 a, b가 존재한다. 여기서 modify(a, b)를 호출한 후에 a.age의 값이 변경되었기 때문에 Call by Reference 로 파라미터를 넘겨주었다고 착각하기 쉽다.
하지만 Reference 자체를 전달하는 게 아니라 주소값만 전달해주고 modify() 에서 생긴 변수들이 주소값을 보고 객체를 같이 참조하고 있는 것이다.
단계별 그림으로 확인해보도록 하자.
처음 변수 선언 시 메모리 상태
원시 타입과는 다르게 변수만 Stack 영역에 생성되고 실제 객체는 Heap 영역에 생성된다. 각 변수는 Heap 영역에 있는 객체를 바라보고 있다.
modify(a, b) 호출 시점의 메모리 상태
넘겨받은 파라미터는 Stack 영역에 생성되고 넘겨받은 주소값을 똑같이 바라본다.
modify(a, b) 수행 직후 메모리 상태
test() 영역과 modify() 영역에 존재하는 a 라는 변수들은 같은 객체인 User01을 바라보고 있기 때문에 객체를 공유한다.
b라는 변수는 서로 같은 객체인 User02를 바라보고 있었지만 modify(a, b) 내부에서 새로운 객체를 생성해서 할당했기 때문에 User03 이라는 객체를 바라본다.
그래서 User03 의 age 값을 변경해도 test() 에 있는 b에는 아무런 변화가 없다.
test() 끝난 후 최종 메모리 상태
modify(a, b) 메서드를 빠져나오면 Stack 영역에 할당된 변수들은 사라진다.
최종적으로 위와 같은 상태가 되며 User03 은 어떤 곳에서도 참조되고 있지 않기 때문에 나중에 Garbage Collector에 의해 제거될 것이다.
결론
"결국 주소값을 넘기는 게 결국 Call by Reference 아닌가?" 라는 생각을 할 수도 있다.
하지만 Call by Reference 는 참조 자체를 넘기기 때문에 새로운 객체를 할당하면 원본 변수도 영향을 받는다.
가장 큰 핵심은 호출자 변수와 수신자 파라미터는 Stack 영역 내에서 각각 존재하는 다른 변수다 라고 생각할 수 있다.
[출처]
https://bcp0109.tistory.com/360
Java 의 Call by Value, Call by Reference
Overview Java 에서 메서드를 호출 시 파라미터를 전달하는 방법에 대해 알아봅니다. 순서는 다음과 같이 진행합니다. Call by Value, Call by Reference 차이 Java 에서의 파라미터 전달 방법 JVM 메모리에 변
bcp0109.tistory.com
'일문일답 > Java' 카테고리의 다른 글
[일문일답][Java] Reflection의 개념 및 사용 방법 (0) | 2022.08.24 |
---|---|
[일문일답][Java] 깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy) (0) | 2022.08.24 |
[일문일답][Java] 가비지 컬렉션 - 2 (0) | 2022.08.24 |
[일문일답][Java] 가비지 컬렉션(GC) - 1 (0) | 2022.08.24 |
[일문일답][Java] JVM의 메모리 구조란? (0) | 2022.08.24 |