Dot Programming/Java

[Java] 자바가 언제나 Call By Value인 이유 (Call By Reference X)

루지 2021. 9. 29. 16:30

    Intro

    시작하기 앞서 CS이론에서는 "Call by value"와 "Call by reference"를 구분하는 것은 더 이상 쓸모없다고 한다. 왜냐하면 "Call By Reference"은 이제 트렌드에 뒤쳐진 기술로 선호도 굉장히 낮아져 최신 언어에서는 더 이상 사용되지 않고 이를 통합한 방식으로 사용되고 있기 때문이다. 어쩐지 최근 자바 서적을 읽는데 Call By Value와 Reference에 대한 언급이 일절 없었다. 그러나 이는 자바 메모리 구조에 대해 이해하는데 도움이 되기 때문에 정리를 해봤다. 

     

    절차지향언어인 C언어는 Call By Value(값에 의한 호출)를 사용한다. 이후에 C언어를 기초로 문법은 그대로 유지하면서 OOP(객체지향 프로그래밍)기능만 추가한 C++언어에서 Call By Reference 사용이 가능하다. 그렇다고 Call By Reference는 C++에서 처음으로 사용된 것이 아니고 Fortran일 때 부터 Argument-Passing By Reference By Value라는 용어로 사용이 되었다고 한다.

     

    C++ 참조 변수 예시

    int a = 10; // 일반 변수
    int* p = &a; // 포인터 변수
    int& b = a; // 참조 변수 (reference variable) c언어에는 없고 c++에서만 가능

     

    자바가 Call by Value인 이유

    call by value는 말 그대로 함수 호출시 값을 넘겨주는 방식이고 call by reference는 값이 아닌 자신을 공유해서 사용하도록 허용하는 개념이다. Java에도 Call By Reference가 존재한다고 하지만 사실상 C++의 관점으로 바라본다면 Java에는 Call By Reference가 존재하지 않는다. 모든건 Call By Value로 작동된다.

     

    call by value call by reference
    값에 의한 호출방식은 인자로 받은 값을 복사하여 처리를 한다. 참조에 의한 호출방식은 인자로 받은 값의 주소를 참조하여 직접 값에 영향을 준다.
    원래 값이 수정되지 않는다. 원래의 값이 수정된다.
    변수의 복사본이 전달된다. 변수 자체가 전달된다.
    실제 인수가 다른 메모리 위치에 생성된다. 실제 인수가 같은 메모리 위치에 생성된다.

     

    "Reference"라는 뜻은 일반적으로 무엇을 참고하는 것이라고 사용하고 있지만 CS에서 사용되는 "Call By Reference"에서 "Reference"는 우리가 쓰는 '참조'라는 의미보다 더 좁은 의미로 사용되고 있다. 예전부터 CS에서 해당 parameter passing 방식을 여러가지 용어로 혼동되어 쓰였지만 ["Semantic Models of Parameter Passing" by Richard E Fairley, March 1973.] 논문에서 처음으로 "Call By Reference"로 정의되었다고 한다. 해당 논문에서 정의된 뜻을 살펴보면 다음과 같다.

    In Call By Reference, the address (name) of the actual parameter at the time of procedure call is passed to the procedure as the value to be associated with the corresponding formal parameter. References to the formal parameter in the procedure body result in indirect addressing references through the formal parameter values are immediately transmitted to the ceiling procedure, because both the acutal parameter and the formal parameter refer to the same register.

    Call By Reference에서 프로시저 호출 시 실제 파라미터의 주소(이름)가 해당 형식 파라미터와 연관될 값으로 프로시저에 전달됩니다. 보조 파라미터(값을 전달받는)와 공식 파라미터(값을 제공하는)가 모두 동일한 레지스터를 참조하기 때문에 절차 본문에서 형식 파라미터 값을 통한 간접 주소 지정 참조는 즉시 맨 위의 절차로 전송됩니다.

    일반적으로 우리가 알고있는 Call By Reference의 정의와 일치한다. 이 개념을 보고 판단해보면 Reference는 두 파라미터가 모두 같은 주소값을 가리키고 있는 것이지 보조 파라미터가 해당 주소값을 '값'으로 들고 있는 것이 아니라고 정의를 내릴 수 있다.

     

    예제1) 새로운 객체 참조하기

    Java에서는 참조 타입을 사용하면 각 타입의 변수들은 주소값을 가지게 된다. 이는 주소 '값'을 가지게 되는 것이므로 Call By Value로 동작한다.

     

    Java 코드 (Call By value)

    String 클래스 타입으로 예를 들어 보자.

    public class CallByEx {
    
    	public static void main(String[] args) {
    		String s = new String("abc");
    		String s2 = s;
    		
    		foo(s);
    		
    		System.out.println("#3 " + s.equals("abc")); // true
    		System.out.println("#4 " + s.equals("ccc")); // false
    		System.out.println("#5 " + (s==s2)); // true
    		
    		//만약 call by reference라면 
    		// System.out.println(s.equals("ccc")); // true
    		// System.out.println(s==s2); // true
    	}
    	
    	static void foo(String str) {
    		System.out.println("#1 " +str.equals("abc")); // true 
    		
    		str = new String("ccc");
    		System.out.println("#2 " + str.equals("ccc")); // true 
    	}
    }

     

    만약 자바가 Call By Reference라면 해당 String 변수 s는 foo(String str)에서 새롭게 생성된 new String("ccc")를 참조해야 하지만 결과를 보면 그렇지 않은 것을 볼 수 있다.

     

    실행 결과

     

    위의 코드를 그림으로 보면 다음과 같다. 참조 객체 s, s2, str은 각 독립적인 저장공간에서 동일한 주소값을 지니고 있는 것을 볼 수 있다. 그렇기 때문에 C++처럼 같은 주소값을 참조하고 있어 수정이 일어나면 같은 주소값을 가진 객체들이 모두 변경이 일어나지 않는다.

     

    String 객체 동작 과정

     

    C++ 코드 (Call By reference)

    같은 로직을 call by reference를 사용하는 C++를 사용하여 돌려보면 다음과 같다.

    #include <iostream>
    #include <string>
    using namespace std;
    
    void foo(string& param) { 
    	string str("ccc");
    	param = str;
    }
    
    void boo(string& param) { 
    	param += "zzz";
    }
    
    int main() {
    	string str1 = "abc";
    	foo(str1);
    	cout << str1 << endl;
    	boo(str1);
    	cout << str1 << endl;
    	return 0;
    }

    실행 결과

     

    C++에서는 Call By Reference이기 때문에 간접 주소 참조 객체(param)가 새로운 값(str("ccc"))을 참조하게 되면 원본 주소 참조 객체(str1)또한 새로운 값을 참조하게 된다. 왜냐하면 param과 src1 두 객체는 같은 레지스터 주소를 참조하고 있기 때문이다. 그리고 당연히 같은 주소를 참조하고 있으니 보조 객체가 값을 변경해도 원본 객체가 참조한 변수 또한 값이 변경된다.

    • (추가 설명) param의 화살표가 애매한 면이 있다. param은 str1에 할당된 메모리를 공유받아 같은 value 뿐만 아니라 address도 공유받게 된다. reference를 공유한 것이다.
      • call by reference: str1 (value: "abc", address: 0FDA) → param (value: "abc", address: 0FDA)
      • 만약 call by value 라면, str1 (value: "abc", address: 0FDA) → param (value: "abc", address: 0XDE) 이는 자바와 같이 param값을 변경해도 str1의 값은 변경되지 않는다.

     

    C++ Call By Reference 동작과정

     

    예제2) 참조 타입 객체 값 변경하기

    참조 객체가 각각 독립적인 저장공간에 주소값을 가지고 있는 것을 알게되었다. 하지만 Java또한 참조 객체를 다른 객체에게 주소값을 전달하여 대신해서 값을 변경할 수 있는 구조를 가지고 있다. 이러한 동작때문에 Call By Reference로 착각할 수 있지만 이는 같은 주소값을 가지고있는 보조 객체가 주소값을 통해 접근하여 주소값이 가리키는 내용을 변경시키는 것이기 때문에 Call By Value이다.

     

    Java 코드 (Call By value)

    Java의 배열 기본 타입 또한 참조 타입이므로 int배열을 통해 예를 들어보자.

    package java_practice.callby;
    
    import java.util.Arrays;
    
    public class CallByEx {
    
    	public static void main(String[] args) {
    		int[] src = {1,2,3};
    		
    		foo(src);
    		System.out.println(Arrays.toString(src));
    		boo(src);
    		System.out.println(Arrays.toString(src));
    	}
        
    	static void foo(int[] arr) {
    		arr = new int[]{3,4,5};
    		arr[0] = 9;
    	}
        
    	static void boo(int[] arr) {
    		arr[0] = 2; // 이건 주소 값의 가리키는 변수를 바꾼 것
    	}
    }

     

    다음과 같이 원래 [1, 2, 3]의 값을 지는 배열 원소가 boo(int[] arr)의 보조 객체인 arr을 통해 값이 변경되는 것을 볼 수 있다.

     

    실행 결과

    foo(int[] arr);

    위에 String객체로 했던 예시와 같이 새로운 배열을 생성하여 값을 변경하면 원본 객체(src)에게는 전혀 영향이 가지 않는다.

     

    boo(int[] arr);

    위의 코드를 그림으로 보면 다음과 같다. 자바 참조타입 객체는 주소값을 가지고 있기 때문에 보조 객체(arr)가 주소값을 통해 주소값이 가리키고 있는 값을 변경하면 같은 주소값을 가리키고 있는 원본 객체(src)의 값도 변경이 되는 것이다.

     

     

    초기값
    (위)foo메서드 동작과정, (아래)boo메서드 동작과정

     

     

     

    마지막으로...

    자바 창시자인 제임스 고슬링은 수많은  parameter passing 방법이 있지만 자바에는 간단하게 "call by value"만을 참고했다고 한다. -The Java Programming Language, 2nd ed. by Ken Arnold and James Gosling, section 2.6.1, page 40, 3rd paragraph.  (참고)

    the Java authors choose to only include one simple idea - pass-by-value, with no default values or optional parameter (overloading often provides a satisfactory alternative), no variable length parameter lists (added finally in Java 5), no named parameters, no pass-by-reference, no const (a general Java issue) parameters, etc. 

     


    ※ 추가 공부하러가기

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

    ⭐️ 2. https://stackoverflow.com/questions/373419/whats-the-difference-between-passing-by-reference-vs-passing-by-value/430958#430958 

    3. https://stackoverflow.com/questions/1856680/origin-of-term-reference-as-in-pass-by-reference

    4. https://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_reference

    5. https://www.codementor.io/@michaelsafyan/computer-science-different-ways-to-pass-parameters-du107xfer

    6. https://www.baeldung.com/java-pass-by-value-or-pass-by-reference

    7. https://deveric.tistory.com/92