언어/Javascript & Typescript

[JS] 복제, 참조 그리고 함수의 매개변수

샥쿠 2024. 5. 1. 15:27

복제

변수 a, b가 정수(원시 데이터 타입)인 경우를 살펴보자. 아래는 변수 b에 변수 a의 값을 복제한다. 즉 a와 b가 가리키는 주소가 다르므로, b의 값을 변경해도 a의 값은 그대로이다. 원시 데이터 타입과 참조 데이터 타입

let a = 1;      // a는 원시 타입
let b = a;      // b에 a의 값 복제 (원시 타입)
b = 2;          // b의 값 변경
console.log(a); // 1 (a의 값은 그대로)

참조

이번에는 변수 a, b가 객체 (참조 데이터 타입)인 경우를 살펴보자. b에 a를 할당하게 되면 b는 a와 동일한 객체를 참조한다. 즉 b의 값을 바꾸면 a의 값도 바뀐다.

let a = {'id': 1};    // a는 객체(참조 타입)
let b = a;            // b는 a의 주소를 참조
b.id = 2;             // b의 속성 값 변경
console.log(a.id);    // 2 (a의 속성 값도 변경)

다만, 새로운 객체를 생성해서 b에 할당하면 a, b가 다른 주소를 참조하게 된다.

let a = {'id': 1};    // a는 객체(참조 타입)
let b = a;            // b는 a의 주소를 참조
b = {'id': 2};        // b에 새로운 객체 할당
console.log(a.id);    // 1 (a의 속성 값은 그대로)

원본 파일의 주소를 참조함으로써 동일한 데이터를 중복 저장하지 않을 수 있으므로 저장 장치 용량을 절약할 수 있다. 또한 원본 파일을 사용하고 있는 모든 복제본이 동일한 내용을 유지할 수 있고 한곳에서 업데이트한 내용을 복제본 모두에서 공유할 수 있다.

함수의 매개변수

함수의 매개변수(parameter)도 일종의 변수 할당이라고 할 수 있다. 함수를 호출할 때 전달되는 값들은 함수 내부에서 매개변수에 할당된다.

함수 호출시 인자를 전달하는 방식: Call by Value & Call by Reference

프로그래밍 언어에서 함수를 호출할 때 인자를 전달하는 방식은 크게 Call by value와 Call by reference로 나뉘며, 데이터 전달 방식에 따라 함수 내부에서 변수를 처리하는 방식이 달라진다. (C언어로 Call by Value, Call by Reference를 구현한 것은 아래 더보기를 참조)

더보기

Call by value는 함수를 호출할 때 인자로 전달된 값의 복사본을 생성하여 함수에 전달하는 방식이다. 이 방식에서 원본 데이터는 변경되지 않는다.

함수 호출시 메모리 공간에 함수를 위한 별도의 임시 공간이 생성되고, 함수 호출시 전달되는 변수 값을 복사해서 함수 인자로 전달한다. 복사된 인자는 함수 안에서 지역 변수 속성을 가지며 함수 종료시 사라진다.

#include <stdio.h>

void func(int n) {
	n = 20;
}

int main() {
	int n = 10;
	func(n);
	printf("%d\\n", n); // 10
}

참조에 의한 호출 방식은 함수 호출 시 인자로 전달되는 변수의 레퍼런스(포인터)를 전달한다.

함수 안에서 인자 값이 변경되면, 인자로 전달된 원본 객체의 값도 변경된다.

C에서는 함수의 인자로 포인터를 넘겨줌으로써 참조에 의한 호출이 가능하다.

#include <stdio.h>

void func(int *ptr) {
    *ptr = 20;
}

int main() {
    int n = 10;
    func(&n);
    printf("%d\\n", n); // 20
}

원시 타입을 인자로 넘기는 경우 (Call by value)

함수의 매개변수로 원시 타입을 전달할 때에는 값에 의한 호출(call by value)이 발생한다. 이는 전달된 변수(인자)의 복제본이 매개변수에 할당되는 것을 의미한다. 즉, 매개변수로 전달된 값이 함수 내부에서 변경되어도 원본 변수의 값에 영향을 주지 않는다.

let a = 1;
function func(b) {
	b = 2;
}
func(a);
console.log(a); // 1

위 동작은 아래와 같이 매개변수 b에 a를 할당하는 동작으로 표현할 수 있다.

let a = 1;
let b = a; // func(a)를 호출할 때 매개변수 b에 a를 할당하는 동작
b = 2;     // 함수 안에서 b = 2 할당하는 동작
console.log(a); // 1 (a의 값은 변경되지 않는다)
let a = {'id': 1};
let b = a;         // func(a)를 호출할 때 매개변수 b에 a를 할당하는 동작
b.id = 2;          // 함수 안에서 b.id = 2 할당하는 동작
console.log(a.id); // 2 (a의 속성 값도 변경)

참조 타입을 인자로 넘기는 경우 (Call by reference)

함수의 매개변수로 참조 타입(객체)을 전달할 때에는 참조에 의한 호출(call by reference)이 발생한다. 이것은 매개변수에 전달된 객체의 복제본이 전달되는 것이 아니라, 해당 값들에 대한 참조(메모리 주소)가 전달되는 것을 의미한다. 즉, 함수 내에서 매개변수로 전달된 객체를 변경할 경우 원본 객체도 영향을 받게 된다.

let a = {'id': 1};
function func(b) {
	b.id = 2;
}
func(a);
console.log(a.id); // 2

위 동작은 매개변수 b에 객체 a를 할당한다. 따라서 b는 a를 참조하게 되므로 함수 안에서 b의 id 속성 값을 변경하면 a의 id 속성 값도 변경된다.

let a = {'id': 1};
let b = a;         // func(a)를 호출할 때 매개변수 b에 a를 할당하는 동작
b.id = 2;          // 함수 안에서 b.id = 2 할당하는 동작
console.log(a.id); // 2 (a의 속성 값도 변경)

반면 함수 안에서 b에 새로운 객체를 할당하면 a에는 영향을 미치지 않는다. 서로 다른 주소를 바라보게 되었기 때문이다.

let a = {'id': 1};
function func(b) {
	b = {'id': 2};
}
func(a);
console.log(a.id); // 1

Call by value, Call by reference 장단점

  • Call by value
    • 장점: 복사하여 처리하기 때문에 안전하다. 원래 값이 보존된다.
    • 단점: 메모리 사용량이 늘어난다.
  • Call by reference
    • 장점: 복사하지 않고 직접 참조하기 때문에 빠르다.
    • 단점: 원본의 값이 영향을 받는다.

참고자료

https://opentutorials.org/course/743/6507

https://velog.io/@younoah/call-by-value-call-by-reference