Java는 Call By Reference일까, Call By Value일까?
Java

Java는 Call By Reference일까, Call By Value일까?

감사하게도 우테코 크루 검프께서 많은 레퍼런스를 주셔서 글을 더 다듬었습니다 😉

 

오늘은, 우테코에서 재밌게 토론했던 주제인 Java는 Call By Reference일까, Call By Value일까에 대해 다루어보고자 한다. 

 

우선, 함수의 호출 방식에는 두 가지가 있다. 

첫번째는 Call By Value(값에 의한 호출)이다. 이 방식은 함수 호출 시 전달되는 변수의 값을 복사하여 함수의 인자로 전달한다. 복사된 인자는 함수 안에서 지역 변수의 특성을 가진다. 따라서 함수 안에서 인자의 값이 변경되어도, 외부에는 영향을 끼치지 않는다. 

두번째는 Call By Reference(참조에 의한 호출)이다. 이 방식은 인자로 받은 값의 주소를 참조하여 처리한다. (인자로 전달되는 변수의 레퍼런스를 전달한다.) 따라서 함수 안에서 인자의 값이 변경되면, 인자로 전달된 객체의 값도 함께 변경된다. 

 

잠깐, 그렇다면 Call By Value는 깊은 복사와 같은 개념일까? 그리고 Call By Reference는 얕은 복사와 같은 개념일까?

 

질문에 대한 답을 알아보기 이전 깊은 복사와 얕은 복사에 대해 먼저 알아보자.

 

깊은 복사란 객체를 복사할 때 해당 객체와 인스턴스 변수까지 모두 복사하는 것을 의미한다. 따라서 이전 객체와의 관계를 완전히 끊어주기 때문에 복사하여 만들어낸 객체의 값이 변경되어도 기존의 객체에 영향을 끼치지 않는다.

얕은 복사란 객체를 복사할 때, 해당 객체만 복사하여 새 객체를 생성한다. 따라서 복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다. 따라서 복사하여 만들어낸 객체의 값이 변경되면 원본 및 복사 객체 모든 값이 변경된다. 

 

다시 위 질문으로 돌아가서, stackoverflow에서 비슷한 질문을 올린 글을 확인할 수 있었다. 

Is pass-by-value/reference equivalent to making a deep/shallow copy, respectively?

 

위 게시글의 채택된 답변에서는 얕은 복사와 깊은 복사는 객체의 복사에 대해 이야기하는 반면, call-by-value/call-by-ref는 변수의 전달에 대해 이야기하는 것이기에 서로 다르다는 뜻이었다. 객체를 복사하는 것과 변수로 값을 전달할 때 값을 복사하는 것은 서로 다른 것일까? 아직 이 부분에 대해서는 잘 모르겠어서 출근하는 날 다른 크루들에게 물어보고 얻은 답변을 추가하도록 하겠다. 

 

아무튼 돌고 돌았는데, 그렇다면 Java는 Call By Reference일까, Call By Value일까?

Java는 결론적으로 말하자면, Call By Value이다. (충격!)

 

먼저, Oracle Docs를 통해 확인해보자.

문서의 Passing Primitive Data Type Arguments라는 부분에서 프리미티브 타입에 대한 내용을 확인할 수 있다. int와 double과 같은 프리미티브 타입은 argument에 call-by-value로 전달된다. 즉, 아래 그림과 같이 값이 복사되고 넘어간다고 생각할 수 있다. 

 

 

즉, 메소드인자로 들어온 값의 변경은 호출한 메서드의 스코프 내에서만 이루어지고, 내부에서 값을 변경한다 하더라도 외부 값에는 영향을 미치지 않는다. 하지만 여기까진 그렇다치고.. Reference Type은 그럼 왜 Call-By-Value일까?

 

앞선 Oracle docs에서 Passing Reference Data Type Arguments에 대해 레퍼런스 타입 또한 프리미티브 타입과 같이 메서드 인자에 값으로 전달된다고 한다. 

Reference data type parameters, such as objects, are also passed into methods by value.

그림으로 확인하면 참조값을 넘겨주는 것 같지만, 실제로는 heap의 값(실제 객체)이 아닌, stack의 값(힙 메모리 영역의 주소)가 넘어간다. 즉, 위 그림에서 copyNumber를 메서드 인자로 넘길 때, 실제 객체의 참조값이 넘어가는 것이 아닌 복사된 힙 메모리 영역의 주소가 넘어간다. 따라서 call-by-value인 것이다. 

 

이 부분까지만 해도 사실 단번에 이해하긴 어려운 것 같다. 참고한 블로그에서 가져온 예제를 통해 동일한 과정들을 수행하며 알아보자.

 

changeName이라는 메서드의 인자로 person을 넘겨준다. 그리고 changeName 내부에서 이름을 after로 변경해준다. 그럼, 기존 main 메서드 내의 person의 이름도 after로 바뀔까?

class Person {
  private String name;
  public Person(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

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


class Main {
  public static void main(String[] args) {
    Person person = new Person("before");
    
    changeName(person);

    System.out.println(person.getName());
  }

  private static void changeName(Person person) {
    person.setName("after");
  }
}


// 출력 결과
after

 

출력 결과를 확인해보니 after로 나온다. 엥? 그럼 call-by-reference 아닌가?

 

다음으로는 changeName에서 인스턴스 값을 바꾸는 것이 아닌, 새 객체를 할당해보자. 

class Person {
  private String name;
  public Person(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

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

class Main {
  public static void main(String[] args) {
    Person person = new Person("before");
    System.out.println(person.toString());

    changeName(person);

    System.out.println(person.toString());
    System.out.println(person.getName());
  }

  private static void changeName(Person person) {
    person = new Person("after");
  }
}

// 출력 결과
before

call-by-reference로 동작한다면 (person의 참조를 받았다면) after가 출력되어야할 것 같은데, before가 출력된다. 무언가 이상하다. 

 

다시 원점으로 돌아가서, Java는 먼저 Call-By-Value로 동작한다고 이야기했는데, 결국 마지막 예시를 통해 순서대로 생각해보자.

 

1. Cat myCat;
2. Cat myCat = new Cat("butter");
3. foo(myCat);
4. void foo(Cat cat){
6.    cat = new Cat("snack");
7.    cat.setName("bibi");
8. }

 

1. myCat이라는 변수를 선언한다. 초기화가 아직 이루어지지 않은 상태다.

2. myCat에 새로운 인스턴스를 할당한다. 여기에는 새로운 인스턴스의 주소가 할당된다. 1이라고 가정하자.

3. foo라는 함수를 호출하며, 인자로 myCat을 넘긴다.

4. foo라는 함수가 실행된다. 인자로는 주소값1이 들어왔다. Java는 Call-By-Value이므로 cat은 그저 복사된 주소공간의 역할만을 한다.

5. cat에 새로운 인스턴스를 할당하였다. cat은 이제 snack이라는 이름을 가진 Cat 클래스의 새로운 인스턴스 주소값 2를 갖는다.

6. cat이 가리키는 인스턴스의 이름을 bibi로 수정한다. 현재 변수에는 6에서 생성한 인스턴스의 주소인 2가 저장되어있으므로 2번 주소에 존재하는 인스턴스의 이름이 변경된다.

 

결과적으로 foo 함수의 호출이 종료되었을 때, myCat이 가리키는 인스턴스는 변하지 않는다

왜? myCat은 1번 주소를 저장하고 있을 뿐이고, foo 함수의 인자 또한 call-by-value이기 때문에 1이라는 주소 값을 복사하고 foo 함수 내부를 실행하기 때문이다. 

 

어렵다 어려워..!