2017년 7월 3일 월요일

[Java] 메소드 파라미터(Paratmeter) 전달, call by Reference vs Value

메소드를 호출할 때 우리는 파라미터를 전달 할 수 있다. 예를들어, 우리가 자주 쓰는 메소드인 "System.out.println(파라미터)" 도 파라미터를 받아 실행된다. 이런 파라미터를 전달하는 박식은 "call by reference", "call by value"라는 2가지 방식으로 나뉜다.

Call by Value 방식은 간단하다. 단순히 메소드를 호출하는 호출문(caller)이 전달하는 값(value)의 복사값이 메소드에 전달된다. 그러므로 호출된 메소드는 전달받은 변수 값을 변경해도 원래 변수에는 적용되지 않는다. 그에비해 Call by Reference 방식에서는, 메소드가 호출문에서 제공된 변수가 저장된 메모리 참조(Reference)값을 전달받는다. 그러므로 메소드에서 파라미터값을 변경한다면 해당 변수에 모두 적용된다.

이런 Call by ... 단어는 단순이 자바에서만이 아니라 프로그래밍 이론적으로도 메소드 파라미터의 습성을 다룰 때 쓰이는 단어이니 잘 알아두자. 옛 프로그래밍 언어인 Algol 언어에서는 Call by name이라는 개념도 존재했다.

자바(Java)에서는 언제나 Call by Value 방식으로 메소드를 호출한다. 그래서 메소드 내부에서 전달받은 파라미터를 수정하거나 해도 실제 변수에는 적용되지 않는다.


public static void main(String[] args) {
 int test = 0;
 raiseIntBy(test, 3);
 System.out.println(test); // 0
}

public static void raiseIntBy(int x, int y){
 x+=y;
}

test 변수의 값은 변하지 않는다. 우선 raiseIntBy 메소드의 변수 x는 test 변수의 복사값인 0으로 초기화된다. 그리고 x에 y가 더해져서 x는 3이 되었지만, 변수 test의 값은 그대로 0이게 된다. 그리고 메소드가 종료되고, 변수 x의 스코프에서 벗어나게 된다. 원시자료형(Primitive Type)의 경우에는 이런 시나리오로 진행이 되게 된다. 하지만 자바에는 2가지 자료형이 있고, 객체 자료형에서는 다른 결과를 내게 된다.
이는 객체 자료형이 저장하는 값이 원시자료형처럼 자료값이 아닌 자료가 저장된 메모리 참조값(Memory Reference)을 저장하기 때문이다.


// Person 객체를 설계해보자
public class Person {
 private String name;
 private int age;
 public Person(String name, int age) {
  this.name = name;
  this.age = age;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public int getAge() {
  return age;
 }
 public void setAge(int age) {
  this.age = age;
 } 
}

// Test 클래스에서 객체를 사용해보자
public class Test {

 public static void main(String[] args) {
  Person p = new Person("Jason", 22);
  System.out.printf("Name: %s, Age: %d \n", p.getName(), p.getAge());
  // Name: Jason, Age: 22

  changeName(p, "Eva");
  changeAge(p, 33);

  System.out.printf("Name: %s, Age: %d \n", p.getName(), p.getAge());
  // Name: Eva, Age: 33

  Person q = new Person("David", 45);
  System.out.printf("Name: %s, Age: %d \n", q.getName(), q.getAge());
  // Name: David, Age: 45

  swap(p, q);

  System.out.printf("Name: %s, Age: %d \n", p.getName(), p.getAge());
  // Name: Eva, Age: 33

  System.out.printf("Name: %s, Age: %d \n", q.getName(), q.getAge());
  // Name: David, Age: 45
 }

 public static void changeName(Person s, String newName) {
  s.setName(newName);
 }

 public static void changeAge(Person s, int a) {
  s.setAge(a);
 }

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

위 코드를 실행하면 객체의 name 변수와, age 변수의 값이 바뀌게 된다. 메소드가 호출될 대, Person sp의 값의 복사값을 받게 된다. 그리고 그 복사값은 p가 가진 객체 참조값과 동일한 값이다. 결국 s와 p는 둘다 같은 참조값을 가지게되고 같은 인스턴스를 참조하게된다. 그리고 둘이 같은 값을 참조하게 되니 둘중 한 객체에 적용되는 변경사항은 둘 모두에게 적용된다.

하지만 swap 메소드를 실행새서 나온 결과를 알다시피 객체를 서로 교체하는 메소드에서는 변경사항이 적용되지 않는것처럼 보인다. 약간 헷갈릴수도 있겠지만, Call by Reference 방식이라면 두 인스턴스가 서로 뒤바뀌는게 맞다. 하지만 Call by Value 방식에서는 앞서 말했듯이 파라미터가 변수의 복사값을 가진다. swap 메소드를 자세히 보자.
  1. 메소드 호출시에 Person 인스턴스 a 와 b는 또다른 Person 인스턴스 p 와 q의 참조값을 복사해 초기화 된다.
  2. 다른 Person temp라는 새로운 인스턴스가 생성되고 a의 참조값을 가지게 된다.
  3. Person 인스턴스 a는 b의 참조값을 가지게 된다.
  4. b는 a의 참조값을 가지게 된다.
  5. 그렇다면 서로 참조값이 바뀌었으니, 서로 바뀌어야하는게 아닌가 싶은데, 실질적으로 인스턴스 a, b는 p, q와는 서로가 참조하는 메모리 주소만 같지 완전 다른 인스턴스이다. 그러니 그 참조값에 저장하는 정보에 적용되는 변경사항이 아니고 인스턴스의 참조값이 변경되는건 적용되지 않는다. 그러면 swap 메소드를 마지막단계까지 진행해보자.

  6. 메소드가 끝나고 인스턴스 a와 b의 스코프(Scope)가 종료되어 a와 b에 접근할 수 없게 된다.
  7. a와 b가 저장하는 참조값은 변했지만 실질적으로 원래 인스턴스인 p와 q는 달라진게 없다.

Call by Value방식을 요약하게되면
  • 메소드는 원시자료형(Primitive Data Type) 파라미터를 변경하는게 불가능하다.
  • 메소드는 객체의 상태(State)를 변경할수있다.
  • 메소드는 객체가 또다른 객체를 참조하게 하는것은 불가능하다.

댓글 없음:

댓글 쓰기