effective typescript를 스터디하면서 나온 'readOnly는 얕게 적용된다'라는 말에 대한 개념을 명확하게 정리하기 위해 씀
> 일단 Call by Value와 Call by Reference에 대한 내용은 구글링 하면 무수히 많이 나온다. 하지만 어떤 언어를 사용하냐에 따라 이에 대한 이해는 좀 다르게 해야한다. Call by Value와 Call by Reference에 대한 기본적인 개념 기술 후 내가 사용하는 Typescript(=Javascript)에서는 어떻게 이에 대한 처리가 되고 있는지 정리한다.
<Call by Value와 Call by Reference의 예시 중 하나>
const a = { a: 5 };
const testFunction = (b) => {
b.a = 1;
return b;
};
const testFunction = (c) => {
c = { b: 5 };
return c;
};
console.log(a);
console.log("------");
console.log(testFunction(a));
console.log("------");
console.log(testFunction2(a));
console.log("------");
console.log(a);
[콘솔엔 과연 뭐라고 나올까?]
일단 기본적인 정의는 아래와 같다.
> Call by Value (값에 의한 호출)
- 값 자체를 복사하여 (함수의) 매개변수로 전달하는 것
> Call by Reference (참조에 의한 호출)
- 객체를 참조하는 주소를 (함수의) 매개변수로 전달하는 것
즉 이 둘의 차이를 한줄로 설명하라고 한다면 객체를 매개변수로 넘겼을 때 Call by Reference는 그 자체를 넘겼고 Call By Value는 그저 값만 똑같은 변수를 카피해서 넘긴거라 생각하면 된다.
허나 단순히 이렇게 알고 넘어가면 안 되고 이들 사이에는 규칙이 존재한다.
Primitive data type (기본 타입) : number, string, boolean, undefined, null과 같은 기본 타입은 변수에 실제 값을 저장하고 call by value로 전달된다.
Non-Primitive data type (객체 or 참조 타입) : Object, Array와 같은 참조 타입은 call by reference로 전달된다.
그럼 여기서 더 알아볼 것은 없는지 위의 예시를 다시 보면서 잡아보자
const a = { a: 5 };
const testFunction = (b) => {
b.a = 1;
return b;
};
const testFunction2 = (c) => {
c = { b: 5 };
return c;
};
console.log(a);
console.log("------");
console.log(testFunction(a));
console.log("------");
console.log(testFunction2(a));
console.log("------");
console.log(a);
이렇게 하면 콘솔에는 아래와 같이 찍힌다.
{ a: 5 }
------
{ a: 1 }
------
{ b: 5 }
------
{ a: 1 }
위의 예시에서는 객체를 매개변수로 주고 받았으며 이는 Call by Reference로 넘겨지고 처리되기 때문에 최종 값은 맨 마지막으로 처리된 testFunction2 결과값인 "{ b : 5 }"가 나왔어야 했다. 하지만 최종적으로 a의 값은 testFunction의 결과값인 "{ a : 1 }"이였다.
> 참조 객체는 Call by Reference로 처리되기 때문에 testFunction 함수로 인해 변수 a의 값이 "{ a : 1 }"로 바뀐 것은 납득이 가지만 testFunction2의 값은 반영이 되지 않는다는 것은 Call by Reference가 아니라는 것이다.
- 즉 Javascript에서는 엄연히 말하면 Call by Reference는 지원하지 않는다. 위의 아래 그림을 통해 testFunction은 왜 call by reference처럼 처리됐는지 그리고 testFunction2는 왜 아닌지 다시 확인해보자
- 일단 이를 이해하기 위해서는 데이터가 어떻게 저장되는지 이해해야 한다.
- 기본 타입의 데이터와 참조 타입의 데이터는 각각 다르게 처리 된다. 기본 타입은 데이터 실제 값을 스택(stack)에서만 저장하고 처리하지만 참조타입은 힙(heap) 영역에 저장하고 힙 영역 어디에 저장되어 있는지 주소를 가지고 있는다. (구조는 아래와 같다)
- 이렇기 때문에 기본 타입은 call by value로 이루어지고 참조 타입은 call by reference로 처리된다고 하는 것
- 근데 이 얘기는 Java나 다른 언어의 이야기 이고 JS에서는 무조건 Call By Value로 넘겨지고 처리된다. 위의 예시에선 그럼 어떻게 넘겨지고 받아 처리가 된 걸까. 위의 예시를 아래 그림을 보고 다시 이해해보자
- 최초 const a = {"a" : 5} 라고 선언할 때 위의 그림 처럼 { "a": 5 } 값이 저장된 xxx라는 주소를 a를 가지고 있는다. 그리고 testFunction2에 a를 넣어주게 되면 JS 내부에서는 xxx 주소를 참조하고 있는 a를 가지고 있는 것이 아닌 동일한 주소, xxx를 가지고 있는 c를 call by value처럼 똑같이 복사하여 넘겨주게 된다. 즉 testFunction2 내부에선 call by reference가 아닌 call by value처럼 복사본으로 처리가 된다는 것
- 그럼 testFunction과 testFunction2는 왜 결과가 달라진 것일까? 일단 이를 Call by Sharing이라 한다 (기존의 개념과 다르게 처리가 되기에 혼동을 막기위해 새로 부르는 듯하다.) 아래 그림을 봐보자
- testFunction은 xxx 주소를 가진 b를 복사하여 내부로 가져왔다. 하지만 여기서 testFunction은 b.a를 통해 xxx주소를 통해 Heap영역에 있는 값에 직접적으로 접근했으며 이 값을 5에서 1로 바꿔주겠다고 선언하였다.
그래서 testFunction의 결과값도 {"a": 1}이 되고 이후에서도 a는 {"a": 1}로 전부 바뀌어 버린 것이다.
하지만 testFucntion2는 어떨까?
- testFunction2는 testFunction과 다르게 직접 Heap영역의 값을 수정하는 것이 아닌 xxx주소를 가진 복제된 c에 대해 { "b": 5 }로 선언하여 새로운 yyy를 가지도록 선언하였다.
그렇기 때문에 {"b": 5}가 있는 주소는 testFunction2 내부에만 있는 복제된 c만 가지고 있게되며 a는 여전히 xxx 주소를 바라보고 있기 때문에 testFunction2가 처리되어도 여전히 {"a": 1}이라는 값을 가지고 있게 된 것
<핵심>
> Primitive data type (기본 타입) : number, string, boolean, undefined, null과 같은 기본 타입은 변수에 실제 값을 저장하
고 call by value로 전달된다.
> Non-Primitive data type (객체 or 참조 타입) : Object, Array와 같은 참조 타입은 call by reference로 전달된다.
> 하지만 JS에서는 Call by Reference는 지원하지 않고 Call by Sharing으로 돌아가고 있으니 이를 헷갈리지 말고 개발해야한다
+ 이에 대해 공부하고 요약하면서 저번에 개발할 때 사용했던 lodash.cloneDeep가 있는데 너무 길어져 다음 포스트에서 정리함
'개발 일반' 카테고리의 다른 글
PR 코드 리뷰 문화에 대한 고찰 (0) | 2022.05.24 |
---|---|
REST-API의 한계 (0) | 2022.05.23 |
monorepo를 적용하며 (0) | 2021.09.07 |
이번 스프린트를 끝내며.. (0) | 2021.07.02 |
[cron job] 크론잡 부하 줄여주기 with spring batch, SNS, SQS (0) | 2021.06.28 |