글 작성자: beaniejoy

프로그래밍 언어를 배우면 Call by Reference와 Call by Value에 대한 내용이 나옵니다. 최근에 이 부분에 대한 오해(?)가 있었는데 이를 해소하면서 배운 점들을 블로그에 정리해보고자 합니다.

📌 Call by Reference?

public class CallByReferenceAndValue {

    static void changeObjectValue(Person target) {
        target.setName("changed");
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.setName("hello");

        System.out.println("before method - Person name: " + person.getName());
        changeObjectValue(person);

        System.out.println("after method - Person name: " + person.getName());
    }

    static class Person {
        private String name;

        public String getName() {
            return name;
        }

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

간단하게 조잡한 예시 코드를 짜봤는데요. 딱봐도 너무 쉬워보입니다. 프로그래밍 입문할 때 call by reference 예시로 들면서 메소드에 인자로 객체 reference를 받아서 해당 내부정보를 수정하는 경우 인자로 넣은 객체의 정보가 바뀐다는 내용입니다.

이게 무슨 말인지는 저 위의 실행로그를 보면 명료해집니다.

before method - Person name: hello
after method - Person name: changed

처음 Person name 값으로 "hello" 값을 넣었고 메소드로 해당 Person 객체를 받아서 메소드 내부에서 "changed"값으로 넣었습니다.
메소드 호출한 이후에 인자로 넣었던 person 객체의 name 값을 출력해보면 메소드 내부에서 수정했던 "changed" 값으로 변경되어 있는 것을 확인할 수 있습니다. 즉 메소드 내부에서 수정한 내용이 호출한 쪽에서도 반영이 된다는 내용입니다.

reference 내용을 메소드에 전달했을 때 내부 데이터값이 변경될 수 있다는 예시를 들어 call by reference라고 들었을 것입니다.
하지만 사실 call by reference가 아닌 call by value입니다.

📌 Call by Value

call by value는 말 그대로 값 자체가 전달이 된다는 내용입니다. 주로 int, long, float와 같은 primitive type에 해당하는 내용입니다.

public class CallByReferenceAndValue2 {

    static void changeValue(int target) {
    	target++;
    }

    public static void main(String[] args) {
    	int target = 1;

        System.out.println("before method - target: " + target);
        changeValue(target);

        System.out.println("after method - target: " + target);
    }
}

target은 primitive type으로 값 그자체입니다. 메소드에 전달해서 내부적으로 +1 을 해도 메소드 호출한 쪽에서는 값이 바뀌지 않습니다. 왜냐하면 메소드로 값을 복사해서 전달한 것이기 때문입니다.

메소드는 값을 복사해서 인자로 전달받는다는 관점에서 위의 call by reference를 보면 Person의 name이 바뀌는 예시도 결국 call by value임을 알 수 있습니다. 

public class CallByReferenceAndValue {

    // Person 객체의 주소값이 복사되어 전달받은 것일 뿐
    static void changeObjectValue(Person target) {
        System.out.println("call by reference(method): " + target);
        target.setName("changed");
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.setName("hello");

        System.out.println("before method - Person name: " + person.getName());
        System.out.println("call by reference: " + person);
        changeObjectValue(person);

        System.out.println("after method - Person name: " + person.getName());
    }

    static class Person {
        private String name;

        public String getName() {
            return name;
        }

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

Person 예시에서 객체의 주소값을 출력해보겠습니다.

before method - Person name: hello
call by reference: io.beaniejoy.call.CallByReferenceAndValue$Person@7344699f
call by reference(method): io.beaniejoy.call.CallByReferenceAndValue$Person@7344699f after method - Person name: changed

객체 참조값이 method 안에 인자로 복사되어 전달된다는 것을 알 수 있습니다.

stack 영역
- main method > person 객체 생성 (참조값: 7344699f)
- changeObjectValue 호출
- changeObjectValue(7344699f) > 파라미터에 인자값을 전달, 참조값 "7344699f" 복사해서 전달
- target.setName("...") > 7344699f 주소의 객체 내용 중 name 값 변경

heap 영역
- main method 단계 >7344699f 참조에 대한 객체 내용 저장(name: "hello" 저장)
- changeObjectValue 단계 > 7344699f 참조에 대한 객체 내용 변경(name: "hello" -> "changed")

메모리 구조와 접목했을 때 위와 같이 정리해볼 수 있을 것 같습니다.
참조값 7344699f 자체가 복사되어 메소드에 전달되고 메소드 안에서 해당 참조값의 객체 내용을 수정하면 참조값이 바라보고 있는 heap 영역의 객체 정보를 수정하게 됩니다. 그래서 changeObjectValue 메소드 호출 이후에 person의 name을 출력했을 때 바뀐 값으로 출력된 것을 확인할 수 있었습니다.

정리하자면, method 파라미터에 전달되는 인자는 call by value에 의해 값 자체가 복사되어 전달되고 그 값은 primitive type의 데이터뿐만 아니라 객체의 참조값 자체도 인자로 복사해서 전달됩니다.