Java의 Call by value, Call by reference
Java

Java의 Call by value, Call by reference

안녕하세요.

 

정리 된 글이 많지만 오늘 call by value, call by reference에 대해서 다시한번 차근차근 알아보려고 합니다!

(워낙 헷갈려서요)

 

테스트케이스 하나를 작성해서 실제로 값이 어떻게 들어있는지 확인을 해보도록 하겠습니다.

 

Java 소스코드는 https://github.com/lkimilhol/tistoryblog 에서 확인 가능합니다.

 

1. Call by Value


개념을 정리해 보고 넘어가보도록 하죠.

 

Call by Value란 무엇일까요?

직역하면 '값에 의한 호출' 이라고 하는데, 도통 무슨 소리인지 알 수가 없습니다.

 

값에 의한 호출은 메서도 인자로 값을 넘길 때 값을 '복사' 해서 넘기게 되는데요. 이 때문에 원본의 값은 변하지 않습니다.

 

다음 예시를 좀 보려고 하는데요. C++의 소스코드 그리고 Java의 소스코드 입니다.

 

 

Online C++ Compiler

#include <iostream>

using namespace std;

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

int main()
{
    int a = 10;
    int b = 20;
    
    swap(a, b);
    
    cout<<a<<endl;
    cout<<b<<endl;

    return 0;
}

 

 

ValueReferenceTest.java

public class ValueReferenceTest {
    @DisplayName("원시값 swap")
    @Test
    void swapPrimitive() {
        // given
        int a = 10;
        int b = 20;
        // when
        swap(a, b);
        // then
        assertThat(a).isEqualTo(10);
        assertThat(b).isEqualTo(20);
    }

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

이 두 값의 결과는 모두 어떨까요?

두 소스 코드 모두 swap 이라는 함수와 메서드를 통해 a, b 값을 서로 바꿔주고 있습니다.

 

swap이라는 함수, 메서드를 호출한 후에 그 값이 바뀌어 있을까요?
정답은 그렇지 않다 입니다.

 

 

C++의 경우 실행 시 10과 20의 값이 그대로 보이는 것을 볼 수 있으며 Java의 경우 테스트 케이스가 성공으로 나오면서 두 값이 바뀌지 않은 것을 알 수 있습니다.

 

이것이 Call by value에서 값이 복사 되었기 때문에 나온 결과라고 설명 드릴 수 있는데요.

바로 호출을 하면서 a, b의 변수의 값이 복사되고 호출 되기 때문에 그 값이 바뀌어도 원본에는 영향을 미치지 않는 것입니다.

 

간단한 그림입니다.

 

swapPrimitive() 라는 메서드 안에서 a와 b가 차지하고 있는 메모리가 있는데요.

swap(int a, int b)라는 메서드를 호출 하면서 메모리에 새로 10과 20의 값을 할당하여 적재합니다. 복사가 일어난 것입니다.

 

때문에 내부에서 값을 바꾸어도 원본의 10과 20이 바뀌는 것이 아닌 값을 복사한 곳의 10과 20이 바뀌겠죠?

아래 그림과 같이 말이죠!

 

아래 10과 20이 30, 40으로 바뀌었습니다.

이런 과정들을 Call by value 즉 값에 의한 호출이라고 볼 수 있습니다.

즉, Call by Value는 복사가 일어난다로 생각해 볼 수 있겠네요!

 

2. Call by Reference


자 다음은 Call by Reference입니다. 

 

Call by Reference는 어떤의미를 가지고 있을까요? 마찬가지로 직역하면 '참조에 의한 호출' 이라고 하며 역시나 무슨 소린지 알 수가 없습니다. (하지만 참조에 의한 호출은 좀 더 와닿긴합니다!)

 

참조에 의한 호출은 바로 주소값을 넘긴다 라고 이해해 볼 수 있는데요. 예제를 한번 보도록 하겠습니다!

 

Online C++ Compiler

#include <iostream>

using namespace std;

void swap(int& a, int& b) {
    int temp = b;
    b = a;
    a = temp;
}

int main()
{
    int a = 10;
    int b = 20;
    
    swap(a, b);
    
    cout<<a<<endl;
    cout<<b<<endl;

    return 0;
}

가장 중요한 부분은 swap(int& a, int& b) 입니다. & 가 붙게 되었는데요. 바로 주소 값을 받는다 정도로 생각해주시면 됩니다.

 

결과는 어떨까요?

값이 바뀌었습니다. 이번에는 왜 값이 바뀌었을까요? 바로 Call by Reference가 일어났기 때문입니다.

 

천재는 악필입니다

swap 함수를 보면 주소 값을 전달 받게 했고, 결국 int a, int b의 주소 값이 넘어오게 되어서 같은 메모리의 주소를 가르키고 있는 것을 볼 수 있습니다.

 

따라서 swap 함수 내에서 a, b 값을 바꾼다면 같은 주소를 보고 있는 것들은 바뀐 값을 볼 수 밖에 없겠죠! 

이것이 참조에 의한 호출, Call by Reference 입니다!

 

보시면 Java의 Call by Reference에 대한 내용은 빠져 있는데요. 왜 일까요?

Java는 Call by Reference가 아닌 Call by Value만 사용되기 때문입니다.

 

3. Java에는 Call by Reference가 정말 없을까?


Java에는 Call by Value만 사용된다. Call by Value만 있다. 라고 하지만 헷갈리는 경우가 많아요.

왜냐하면 객체를 생성해서 인자로 넘기면 주소값을 넘긴다고 알고 있기 때문이죠.

 

Primitive type(원시값)은 값을 복사해서 넘겨준다고 생각하지만 실제로 객체들도 값을 복사해서 넘겨주고 있을까요?

 

ValueReferenceTest.java

 @DisplayName("Integer swap")
 @Test
 void swapInteger() {
     // given
     Integer a = 10;
     Integer b = 20;
     // when
     swap(a, b);
     // then
     assertThat(a).isEqualTo(10);
     assertThat(b).isEqualTo(20);
 }

private void swap(Integer a, Integer b) {
    Integer temp = a;
    a = b;
    b = temp;
}

결과는 어떨까요? 바뀌지 않았을까요? 객체를 넘겼는데도?

예상한대로 값은 바뀌지 않았습니다.

 

다음은 Object의 주소값을 직접 출력하면서 값이 정말로 복사 되었는지 확인해보죠. 소스코드는 아래와 같습니다.

 

ValueReferenceTest.java

@DisplayName("Object swap")
@Test
void swapObject() {
    // given
    Object a = new Object();
    Object b = new Object();
    // when
    printReference(a, b);
    // then
    System.out.println("a ori: " + a);
    System.out.println("b ori: " + b);
}

private void printReference(Object a, Object b) {
    System.out.println("a ref: " + a);
    System.out.println("b ref: " + b);
}

 

결과가 어떻게 나올까요? 값이 복사가 되었으니 우리는 다른 주소값을 가지고 있을 수 있다고 생각합니다.

이해가 되지 않습니다. 마지막에 더 알아보도록 해요.

음? 이상한데요?

분명 값을 복사 하였으니 새로운 메모리 주소값을 가지고 있어야 한다고 생각했는데요. 아예 같은 주소값을 참조하고 있습니다.

 

어떻게 된걸까요? 참고를 해보기 위해 일단 검색을 하였고 다음과 같은 내용을 읽게 되었습니다.

 

https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value

pass-by-value, pass-by-reference는 값을 넘기고, 참조를 넘긴다고 하는것인데요.

바로 Call by value의 경우에는 pass-by-value로 값이 전달이 될테고, pass-by-reference는 참조가 넘어가게 된다고 생각하면 되겠네요.

 

그렇다면 답글의 내용을 천천히 보도록 하겠습니다.

 

java는 항상 pass-by-value 라고 합니다

파파고 번역으로는 다음과 같은 번역을 해주네요...

Java는 항상 pass-by-value입니다. 불행히도, 우리가 객체를 다룰 때는 실제로 참조라고 하는 객체 핸들도 다루게 됩니다. 이 객체 핸들도 가치(value)로 전달됩니다. 이 용어와 의미론은 많은 초보자들을 쉽게 혼란스럽게 합니다.

 

즉, 우리가 객체를 다룰때 참조라고 하는 것이 다뤄지긴 하는데, 이 참조가 결국 passed-by-value라고 하는군요?

 

그렇다면 객체를 reference로 다룬다는 것은 어떤 의미 이고 pass-by-value인 것을 어떻게 확인 할 수 있을까요?

 

4. reference, pass-by-value??


일단 테스트할 클래스를 하나 만들어 보려고합니다.

그리고 객체를 생성해서 값을 넘겨서 실제로 어떻게 값들이 변하는지 테스트 해볼게요.

 

일단 제일 처음과 마찬가지로 swap을 하여 값이 바뀌는지를 보려고 합니다.

 

ValueReferenceTest.java

class Color {
	String name;

    public Color(String name) {
	    this.name = name;
    }

    public String getName() {
	    return this.name;
    }

    public void setName(String name) {
	    this.name = name;
    }
}

다음은 테스트입니다. 빨강이라고 하는 객체를 생성해서 파랑을 새로 넣어주도록 해보죠. 물론 새로운 메서드를 호출해서 해야겠죠. 코드입니다.

 

ValueReferenceTest.java

 @DisplayName("pass-by-value")
 @Test
 void passByValue() {
     // given
     Color 빨강 = new Color("빨강");
     Color 빨강_복제 = 빨강;
     // when
     newColor(빨강);
     // then
     assertThat(빨강.getName()).isEqualTo("빨강");
     assertThat(빨강 == 빨강_복제).isTrue();
 }

private void newColor(Color color) {
	color = new Color("파랑");
}

 

결과는 어떨까요?

바뀌지 않는 검증에 대하여 테스트가 성공하였습니다. 즉 객체를 생성해서 새로운 객체를 할당하더라도 원본에는 영향이 없네요.

추가로 빨강과 빨강 복제를 ==(동일 비교) 하였는데도 결과가 true 입니다. 즉 어떠한 값도 할당 되지 않았어요.

 

pass-by-value가 일어났습니다. 이렇게 해서 pass-by-value에 대한 검증은 진행했는데, 그렇다면 reference에 관련 된 내용은 무엇일까요?

 

ValueReferenceTest.java

@DisplayName("reference test")
@Test
void reference() {
    // given
    Color 빨강 = new Color("빨강");
    // when
    changeColor(빨강);
    // then
    assertThat(빨강.getName()).isEqualTo("파랑");
}

private void changeColor(Color color) {
	color.setName("파랑");
}

 

위의 코드의 결과는 어떻게 될까요?

changeColor 메서드 내에서 이름을 바꾸니 원본 값도 함께 수정이 되었습니다. 이것은 복사가 이뤄지지 않았다는 거 같은데요?

이걸 어떻게 설명해야 할까요?

 

Java에서는 객체를 메소드 인자로 넘길 때 참조하고 있는 지역 변수를 가르키는 값을 새로운 지역 변수를 생성(복사)하여 가르키게 합니다.

때문에 메서드 내에서 새로운 객체를 생성하여 가르키게 하더라도 호출을 한 원본에 대해서는 아무런 변경이 일어나지 않는 것이죠.

 

위의 예시에서도 들었던 주소값 출력은 결국 메서드 인자의 값이 아닌 가르키고 있는 값이었던 것으로 생각할 수 있을거 같군요!

결국 같은 메모리를 참조, 가르키고 있는것!

 

 

5. 마치며


기초라곤 할 수 있지만 상당히 복잡한 내용이네요. 메서드에 인자로 객체를 넘기면 실제로 어떻게 동작하는지는 설명 하였지만 아직 애매모호한 부분들이 있는거 같습니다. 이상한 부분은 여과없이 코멘트 달아 주시면 확인 해 보도록 하겠습니다!

 

감사합니다.

'Java' 카테고리의 다른 글

제네릭에 대한 간단한 정리  (0) 2021.10.06
간단한 Spring AOP 개념과 적용  (0) 2021.07.30
간단하게 상속과 조합을 이해해보기  (0) 2021.07.26
JPA N+1 문제  (0) 2021.07.22