본문 바로가기

Study/CS

함수 호출 시 인자 전달 방식 : call by value와 call by reference

반응형

함수 호출 시 인자 전달 방법에는 Call-by-value(값에 의한 호출)와 Call-by-reference(참조에 의한 호출)이 있다. 본 포스팅에서는 먼저 이 둘을 비교하며 설명한 후, 현재 공부하고 있는 java의 인자 전달 방식에 대해서도 설명할 것이다


Call-By-Value (값에 의한 호출)

일단 간단히 말해보자면, 이 전달 방식은 caller(호출하는 놈)의 인자의 값이 복사되어 함수(callee, 호출되는 놈)의 인자로 전달되는 방식이다. 

코드가 진행되는 순서대로 설명하겠다. 위는 call by value방식의 함수(swap)가 구현된 프로그램이 메모리에 할당되고, 흘러가는 방식을 도식화하여 나타낸 그림이다.

먼저 프로그램이 실행되면, 초기화한 변수에 대한 메모리 할당이 발생할 것이다. 메모리에 origin1과 origin2변수의 공간을 만들어, 그 값을 각각 10과 20으로 저장하는 작업이다.  -- (1)

여기서, printf("first : %d, %d", origin1, origin2);  first : 10, 20 을 출력한다.

그 다음으로 함수 swap(origin1, origin2); 가 호출되면, 변수 origin1과 origin2의 값들은 각각 a와 b로 대입되어 들어간다. 이 때, origin1과 origin2는 '인자'가 되는 것이며 호출된 함수 swap은, main함수 변수들의 값(value)만을 가져다가 메모리에 공간을 할당받아 저장하게 되는 것이다. -- (2)

호출된 swap() 함수는 코드에서 선언된 순서대로, 변수 a와 b값을 서로 맞바꾼다. (이 글은 call by value에 대해서 설명하고자 하므로 코드에 대한 자세한 설명은 생략한다.) 그럼, a = 20, b = 10이 된 채로 swap함수는 종료하게 된 것이다. 하지만, 그림상의 메모리 공간에서 보여지듯, swap 함수와 main 함수는 메모리 상 별개의 공간에 자리하고 있으며, swap 함수의 실행 결과는 main 함수에 아무런 영향을 끼치지 않는다. 즉, 함수 swap이 종료됨과 동시에 a와 b는 소멸한 것이고, 당연히 a = 20, b = 10 이라는 결과도 소멸된 것이다. 이렇게 swap 내에서 치환된 변수값은 결국 swap()함수 내에서만 유효한 셈이다. -- (3)

따라서, main함수로 다시 돌아와 실행되는 printf("second : %d, %d", origin1, origin2);  second : 10, 20 을 출력한다.

만약, 각각 20과 10으로 치환된 변수 값을 출력하고 싶다면, printf문을 swap()함수 내에서 작성해야 할 것이다.


Call-By-Reference (참조에 의한 호출)


call by reference는 변수의 주소와 포인터를 활용한 인자 전달 방식이다. 즉, 변수의 주소값을 호출한다는 것인데, 자세한 설명은 아래와 같이 코드와 그림으로 하겠다. 



마찬가지로, 프로그램의 흐름에 따라 설명하겠다. 위는 call by reference방식의 함수(swap)가 구현된 프로그램이 메모리에 할당되고, 흘러가는 방식을 도식화하여 나타낸 그림이다. 

프로그램이 최초로 실행되면, 메모리에 main함수의 멤버변수인 origin1과 origin2에 대한 메모리 상 공간이 생기고, 각각 10과 20이라는 변수값을 할당해 줄 것이다. -- (1) 

Call by value에서와 같이, 여기서 printf("first : %d, %d", origin1, origin2);  first : 10, 20 을 출력한다.

그 다음으로 함수 swap(&origin1, &origin2); 가 호출되면, swap()함수의 인자로서 origin1과 origin2의 주소값인 &origin1(0X0005)과 &origin2(0X0009)가 각각 a와 b의 포인터변수로 넘어간다. 결국, swap()이 호출되면 메모리 공간에서 가장 먼저 일어나는 변화는, int *a = 0X0005;  int *b = 0X0009 가 되는 것이다. -- (2)

이제, swap() 함수가 넘겨받은 origin1과 origin2의 주소를 참조하여 치환을 한다. (함수 실행) swap() 함수의 변수들은 (temp를 제외하고) 포인터 변수들이며, 해당 포인터 변수의 주소를 참조하여 값을 연산하게 되므로, 실질적으로는 참조된 주소값에 해당하는 main함수의 origin1값과 origin2값이 변경되는 것이다. -- (3)

따라서, main함수로 다시 돌아와 실행되는 printf("second : %d, %d", origin1, origin2);  second : 20, 10 을 출력한다.



그렇다면 call by value와 call by reference는 어떤 면에서 trade-off 관계를 유지할까?

먼저, call by value의 경우 함수 호출을 할 때마다 값(데이터)을 복사하여 함수로 인자 전달하고, 이렇게 인자 전달이 발생할 때마다 메모리 공간이 할당되기 때문에 메모리 공간의 낭비를 초래할 수 있다.

call by reference의 경우 call by value가 초래하는 메모리 공간 낭비 문제는 거의 발생하지 않지만, 원본 데이터(값)가 손상될 수 있다는 문제점이 있다




Java의 인자 전달 방식


결론적으로, java는 메소드 호출 시 항상 call by value(값에 의한 호출) 인자 전달 방식을 채택한다. 따라서 호출하는 인자의 값이 복사되어 메소드의 인자로 전달된다. java의 인자 전달은 크게 2가지로 나눠볼 수 있다. 

첫번째로, 기본 타입이 인자로 전달되는 경우가 있다. byte, char, short, int, double 등의 자바 기본 타입이 인자로 전달되는 경우는 위의 call by value 설명에서 언급했던 것과 같다. 메소드가 호출되면 caller의 인자 값이 생성된 매개변수로 복사되고, 연산이 끝나면 해당 매개변수는 사라진다.

두번째는 객체 레퍼런스가 인자로 전달되는 경우이다. 이 경우 객체에 대한 레퍼런스 값이 복사되어 전달되는 데, 객체가 통째로 전달되는 것은 아님을 주의해야 한다.  이는 코드와 그림으로 설명하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Myname {
    String myname;
    Myname(String n){ //생성자
        myname = n;
    }
}
 
public class CallByValue{
    public static void main(String[] args) {
        Goods g = new Goods(); //Goods 객체 생성
        Myname m = new Myname("computer"); //Myname 객체 생성
        
        System.out.println(m.myname); --(1)
        g.setName(m); //레퍼런스 복사 ----------★
        System.out.println(m.myname); --(2)
    }
}
 
class Goods{
    public String name;
    
    public void setName(Myname a) {
        name = a.myname;
        a.myname = "pencil";
    }
}
   
cs





그림 속 1) 부터 차례로 살펴보겠다. 먼저, 프로그램이 최초로 실행된 후 Myname m = new Myname("computer");  코드가 실행된다면, Myname 객체가 생성되며, a가 Myname 객체를 가리키게 된다.  g.setName(m);  코드가 실행되어 m값이 setName() 메소드에 전달되면 m값도 a에 전달되고, public void setName(Myname a) 가 호출됨에 따라 메소드 인자 a는 m이 가리키는 Myname 객체를 동시에 가리키게 된다. (레퍼런스 복사)

2) 로 넘어와보자. a.myname = "pencil"; 이 실행되면 setName 메소드 내에서 a가 가리키는 객체 필드 myname 값이 변경된다. 그렇게 되면 그림에서 보이는 바와 같이, 객체 m의 필드 myname을 변경한 것과 같아지게 된다. 

3) setName() 메소드가 종료됨과 동시에, 레퍼런스 a는 사라지고, m은 그대로 "pencil" 이라는 값을 가진 myname 필드를 가리키고 있는 꼴이 되므로, System.out.println(m.myname); --(2) 이 실행되면,  이 출력된다. 물론, System.out.println(m.myname); --(1) 에 해당하는 출력은   이다.  이 코드를 실행하고 나면 java가 사실 call by reference를 채택하는 것과 같은 셈이 아닌가 하는 의문지 들 수 있다. 

하지만 위 코드를 이와 같이 변경해보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Myname {
    String myname;
    Myname(String n){ //생성자
        myname = n;
    }
}
 
public class CallByValue{
    public static void main(String[] args) {
        Goods g = new Goods(); //Goods 객체 생성
        Myname m = new Myname("computer"); //Myname 객체 생성
        
        System.out.println(m.myname);
        g.setName(m); //레퍼런스 복사
        System.out.println(m.myname);
    }
}
 
class Goods{
    public String name;
    
    public void setName(Myname a) {
        name = a.myname;
        //a.myname = "pencil";
        a = new Myname("pencil");
    }
}
   
cs

본 코드는 class Goods{...} 부분만 변경하였다. 만약 "pencil" 값을 가진 객체 a를 새로 다시 생성하여 할당한다면?
자바가 call by reference라면 이 코드도 방금 전과 같이, 각각 computer와 pencil을 출력해야 할 것이다. 하지만 이 코드를 출력해 보면,
(1)과 (2)는 둘 다 computer을 출력해 냄을 알 수 있을 것이다. 실제로 Myname 객체의 myname 필드는 변경되지 않았다. 이는 java가 객체에 대한 레퍼런스 값만을 복사하는 call by value 방식을 채택하고 있기 때문이고, 만약 call by reference 를 채택하였다면 주소를 가져와, 객체를 통째로 전달하는 것과 같은 결과를 출력해 낼 수 있었을 것이다. 






 

반응형