java는 Pass by value 일까? reference 일까?

  • 자바를 처음 배울 즈음, 메소드 인자(parameter)로 사용되는 객체는 value가 아닌 reference를 전달한다는 교과서 구문을 읽고 이게뭐야 하고 가볍게 흘려넘긴 기억이 있다.
  • 말도 안되는 코드지만 다음 코드의 결과를 예측할 수 있었을까??
Person p = new Person("noname");

setAgentName(p);
System.out.println(p.getName());  // 1

p = setAgentName(p);
System.out.println(p.getName());  // 2

private Person setAgentName(Person p) {
  p.setName("firepizza");
  return p;
}
  • 물론 모두 예상했겠지만 1,2 모두 ‘firepizza’가 출력된다.
  • 그럼 이건 어떨까?
Person p = new Person("noname");

setAgentName(p);
System.out.println(p.getName());  // 1

p = setAgentName(p);
System.out.println(p.getName());  // 2

private Person setAgentName(Person p) {
  p = null;
  return p;
}
  • NPE가 발생 했을까?
  • 1에서는 ‘noname’이 출력되고, 2에서는 NPE가 발생한다.


프로그램을 처음 배울 때 이 부분이 직관적으로 납득되지 않았던 것 같다.
첫번째 예시 코드의 1번에서는 메소드를 실행하는것만으로도 p의 값이 변경되었는데 (인자가 메소드 내부의 행위로 인해 오염될 수 있다고 인지함)
두번째 예시 코드의 1번에서는 마찬가지로 메소드 내부에서 p를 오염시켰으나(null로), 이 경우는 실제로 영향받고 있지 않았기 때문이다.

지금이야 너무나 당연한 결과이지만, 처음 겪은 당황스러움을 해결했던 것이 바로 자바는 메소드에 객체를 전달하는 경우 레퍼런스를 전달한다 (by reference) 라는 내용이었다.

자바에서는 메소드의 인자로 전달되는 객체에 대해 실제 해당 객체의 값을 전달하는 것이 아니라, 해당 객체가 존재하는 메모리의 reference를 전달한다. (정확히는 reference의 copy를 전달하며, 실제로 그렇다는것은 두번째 예시코드의 1번에서 NPE가 발생하지 않음을 통해 알 수 있다.)



  • 사실 개인적으로 Call by reference냐, Call by value냐를 명확하게 구분하는 것은 개발자라기 보다는 학자의 입장에 가깝다고 생각하고 있다.
  • 따라서 용어의 선택과 옳고 그름보다 해당 작동과 원리를 이해하는 부분이 조금 더 중요하다고 생각하는데, 위 내용을 정확히 이해하는 것이 다음으로 설명하게 될 shallow/deep copy와 어느정도 연관이 있기 때문이다.

사족을 달아보자면, java는 기본적으로 pass by value 형태를 취하고 있으며, 인자가 객체인 경우 해당객체의 reference를 전달하는데(by reference) 그것은 카피본이다. (엄밀한 의미의 call by reference는 아니다.)

Shallow / Deep copy

  • 예시의 편의를 위해 javascript로 예를 들겠으나, 맥락을 관통하는 줄기는 같다.
  • 위 내용이 명확히 이해 되었다면, 왜 객체의 copy에 2종류가 존재하며, 명확히 이해해야 하는지가 자연스레 납득이 갈 것이다.
  • 객체를 복사하는 가장 단순한 방법은 할당하는 것이다.
let a = { x: 1 };
let b = a;

a.x = 2;

console.log(a.x); // 2
console.log(b.x); // 2
  • 위와같은 처리가 이루어지는 이유는, b = a 구문에서 b변수에 할당될때에, a객체의 주소(즉 reference)를 복사(할당)하기 때문이다.
  • 따라서 변수는 a와 b 두개로 나누어져 있으나, 실제로 각 변수가 참조하고 있는 실제 값(value)는 동일한 주소상의 값이라는 말이 되는 것이다.

Shallow copy

  • 얕은 복사의 개념은 해당 객체의 구성요소를 그대로 복사해 새로 할당하는 것을 말한다.
  • 이를테면 다음과 같은 작업인데
var a = { x: 1, y: 2 };
var b = { x: a.x, y: a.y };
  • 혼란하다 혼란해 소리가 절로 나오는 구문이다.
  • 결국 위에서 하고자 했던것은 대상객체의 요소를 이루는 값(value)을 그대로 복제하는 것이니,
  • 루프를 통해 해당 객체 내부 요소들을 복사하는 함수를 만들어보도록 하자.
function shallowCopy(target) {
  let result = {};
  for (let key in target) {
    result[key] = target[key];
  }
  return result;
}
  • 사실 실제로는 javascript 내장함수인 Object.assign()을 많이 사용했던 것 같다.
var a = { x: 1, y: 2 };
var b = Object.assign({}, a);
  • 그런데 이런 복사에는 문제가 있는데, 말 그래도 객체의 enumerable한 1depth의 값들만 복사하기에 다음과 같은 문제가 발생한다.
var a = {
  x: 1,
  y: {
    ya: 1,
    yb: 2,
  },
};

var b = Object.assign({}, a);

a.y.ya = 3;

console.log(b.y.ya); // 3
  • 얕게 복사해 두었기 때문에 2depth에 있는 y객체의 요소는 여전히 레퍼런스가 할당되어 a객체의 값을 변경하면 해당 레퍼런스를 참조하고 있던 b객체의 값도 바뀌어 버리게 된다.
  • 그래서 완벽하게 복사를 하기 위해서는 깊은 복사가 필요하게 된다.

Deep copy

  • 깊게 복사하기 위해서는 몇depth의 객체이더라도, 마지막까지 파고들어가서 해당 값을 복제해 내야만 한다.
  • 이 부분에 주안점을 두면 왠지 재귀를 써서 파내야 할것 같은데? 하는 생각이 들기 마련이고, 다음과 같은 함수를 떠올려 볼 수 있다.
function deepCopy(target) {
  if (!target || typeof target !== "object") {
    return target;
  }

  let result = {};
  for (let key in target) {
    result[key] = deepCopy(target[key]);
  }
  return result;
}
  • 그렇다 위와 같이 하면 생각했던대로 해당depth의 복사대상이 객체라면 한번 더 재귀로 파고들어가 복사하게 될 것이다.
  • 그러나 항상 그렇듯 이러한 유틸성 함수는 라이브러리의 도움을 받는게 하이패스와 같이 빠르고 편리하다.
  • jquery에서도 extend() 라는 함수로 지원하고 있고, 내가 즐겨쓰는 lodash에서도 cloneDeep 함수로 지원하고 있기에 편리한 복사를 이용해 보도록 하자.

  • 개인적인 경험에 의하면 이 내용들을 막 열심히 공부해서 익혀둬야지 !!! 하기 보다는
    아 이런 내용이 있지~ 정도로만 눈에 익혀 둔다면 나중에 언젠가 마주칠 아주 골치아픈 버그(사실 매우 간단했던)에서 1분이라도 빨리 벗어날 수 있었던 것 같다.
firepizza's profile image

firepizza

2021-02-03 19:00

firepizza 님이 작성하신 글 더 보기