References Explained: PHP 레퍼런스(참조) 이해하기
References Explained: PHP 레퍼런스(참조) 이해하기
PHP에서 **레퍼런스(reference, 참조)**는 아주 자주 쓰이지만 처음엔 헷갈리기 쉬운 개념이에요. 변수에 담긴 값이 아니라, 그 변수가 가리키는 '자리'를 공유하는 방식이라고 이해하면 편합니다. 이 글에서는 레퍼런스가 무엇이고, 무엇을 할 수 있고, 무엇을 할 수 없는지 하나씩 살펴볼게요.
이 문서는 PHP 공식 매뉴얼「References Explained」장을 정리한 내용입니다. 아래 순서로 다룹니다.
- 레퍼런스란 무엇인가
- 레퍼런스가 하는 일
- 레퍼런스가 아닌 것
- 참조에 의한 전달(Passing by Reference)
- 참조 반환(Returning References)
- 참조 해제(Unsetting References)
- 레퍼런스 알아보기(Spotting References)
레퍼런스란 무엇인가
PHP의 레퍼런스는 같은 변수 내용을 서로 다른 이름으로 접근하는 수단이에요. C 언어의 포인터와는 달라요. 예를 들어 포인터 연산(pointer arithmetic)을 하거나 실제 메모리 주소를 다루는 건 불가능하죠. 더 자세한 내용은 뒤의 「레퍼런스가 아닌 것」에서 다뤄요.
레퍼런스는 사실 **심볼 테이블(symbol table)의 별칭(alias)**이라고 보면 돼요. PHP에서는 변수의 '이름'과 변수의 '내용'이 분리되어 있어서, 같은 내용을 여러 이름으로 가리킬 수 있거든요.
가장 비슷한 비유는 유닉스 파일 시스템입니다. 변수 이름은 디렉터리 항목(directory entry)이고, 변수 내용은 파일 그 자체라고 생각하면 돼요. 그래서 레퍼런스는 유닉스 파일시스템의 **하드링크(hardlink)**와 비슷하다고 이해할 수 있어요.
레퍼런스가 하는 일
레퍼런스로 할 수 있는 연산은 크게 세 가지로 나뉘어요.
- 참조에 의한 대입 — 두 변수가 같은 내용을 가리키게 하기
- 참조에 의한 전달 — 함수에 변수를 참조로 넘기기
- 참조 반환 — 함수가 참조를 되돌려주기
이 절에서는 세 연산을 차례로 소개하고, 더 자세한 내용은 해당 링크로 이어질게요.
참조에 의한 대입 (Assign By Reference)
첫 번째 연산은 PHP 레퍼런스를 이용해 두 변수가 같은 내용을 가리키게 만드는 거예요. 아래 코드를 보면,
<?php
$a =& $b;
이 코드는 $a와 $b가 같은 내용을 가리킨다는 뜻이에요.
여기서 주의할 점이 하나 있어요. $a와 $b는 완전히 동등합니다. $a가 $b를 가리키는 것도, 그 반대도 아니에요. 그냥 둘 다 '같은 자리'를 바라보고 있는 거죠.
참고: 정의되지 않은 변수를 참조로 대입·전달·반환하면, 그 변수는 그 자리에서 만들어져요. 아래 예제를 봐요.
예제 1 — 정의되지 않은 변수를 참조로 사용하기
<?php
function foo(&$var) {}
foo($a); // $a is "created" and assigned to null
$b = array();
foo($b['b']);
var_dump(array_key_exists('b', $b)); // bool(true)
$c = new stdClass();
foo($c->d);
var_dump(property_exists($c, 'd')); // bool(true)
foo()는 매개변수 &$var를 참조로 받지만 함수 안에서 아무것도 하지 않아요. 그런데도 foo($a)를 호출하는 순간 $a가 생성되어 null로 초기화되는 걸 볼 수 있어요. 배열의 원소나 객체의 프로퍼티를 넘겼을 때도 마찬가지로 그 자리가 만들어지죠.
같은 문법은 참조를 반환하는 함수에도 쓸 수 있어요.
<?php
$foo =& find_var($bar);
단, 참조를 반환하지 않는 함수에 이 문법을 쓰면 오류가 나요. new 연산자의 결과에도 같은 이유로 쓸 수 없고요. 객체는 포인터처럼 전달되지만, 이것은 레퍼런스와 다른 개념이에요. 자세한 내용은 「객체와 레퍼런스」에서 다뤄요.
경고
함수 안에서 global로 선언한 변수에 레퍼런스를 대입하면, 그 레퍼런스는 함수 내부에서만 보여요. 이걸 피하려면 $GLOBALS 배열을 쓰면 됩니다.
예제 2 — 함수 안에서 전역 변수 참조하기
global $var;을 $var =& $GLOBALS['var'];의 줄임말이라고 생각해 봐요. 그렇다면 $var에 다른 레퍼런스를 대입해도 그건 지역 변수의 레퍼런스만 바꿀 뿐이라는 게 이해가 되죠.
<?php
$var1 = "Example variable";
$var2 = "";
function global_references($use_globals)
{
global $var1, $var2;
if (!$use_globals) {
$var2 =& $var1; // visible only inside the function
} else {
$GLOBALS["var2"] =& $var1; // visible also in global context
}
}
global_references(false);
echo "var2 is set to '$var2'\n"; // var2 is set to ''
global_references(true);
echo "var2 is set to '$var2'\n"; // var2 is set to 'Example variable'
참고:
foreach문에서 레퍼런스를 가진 변수에 값을 대입하면 그 레퍼런스들도 함께 수정돼요.
예제 3 — 레퍼런스와 foreach 문
<?php
$ref = 0;
$row =& $ref;
foreach (array(1, 2, 3) as $row) {
// Do something
}
echo $ref; // 3 - last element of the iterated array
$row가 $ref를 가리키고 있는 상태에서 foreach가 $row에 값을 계속 대입하니까, 마지막에는 $ref까지 3으로 바뀌는 거예요. 일반적인 대입(비참조)보다는 예외지만, 이런 동작이 있다는 걸 알아두면 디버깅할 때 도움이 돼요.
한 가지 더 짚고 넘어갈게요. 배열 안의 레퍼런스는 잠재적으로 위험해요. 오른쪽에 레퍼런스가 있는 일반 대입은 왼쪽을 레퍼런스로 만들지는 않지만, 배열 내부의 레퍼런스는 이런 일반 대입에서도 그대로 보존돼요. 함수에 배열을 값으로 넘길 때도 마찬가지예요. 예를 들어,
<?php
$a = 1;
$b = array(2, 3);
$arr = array(&$a, &$b[0], &$b[1]);
$arr[0]++;
$arr[1]++;
$arr[2]++;
/* $a == 2, $b == array(3, 4); */
var_dump($a);
var_dump($b);
즉 배열의 레퍼런스 동작은 원소 하나하나 기준으로 정해진답니다. 개별 원소의 레퍼런스 상태는 배열 컨테이너 자체의 레퍼런스 여부와 분리되어 있어요.
참조에 의한 전달 (Pass By Reference)
두 번째 연산은 변수를 참조로 전달하는 거예요. 함수 안의 지역 변수와 호출한 쪽의 변수가 같은 내용을 가리키게 만드는 방식이죠. 예제를 볼게요.
<?php
/* Assignment of scalar variables */
$a = 1;
$b =& $a;
$c = $b;
$c = 7; // $c is not a reference; no change to $a or $b
print "a = {$a}; b = {$b}; c = {$c}\n\n";
/* Assignment of array variables */
$arr = array(1);
$a =& $arr[0]; // $a and $arr[0] are in the same reference set
$arr2 = $arr; // Not an assignment-by-reference!
$arr2[0]++;
/* $a == 2, $arr == array(2) */
/* The contents of $arr are changed even though it's not a reference! */
print "a = {$a}\n";
var_dump($arr);
var_dump($arr2);
여기서 한 가지 중요한 사실을 확인할 수 있어요. $arr2 = $arr;처럼 복사해도 $arr[0]은 $a와 같은 레퍼런스 셋에 묶여 있어서, $arr2[0]++이 원본 $arr[0]와 $a까지 바꿔버립니다. 이런 동작 때문에 배열 복사할 때 의도치 않은 변경이 생기기 쉬우니 주의해야 해요.
참조 반환 (Return By Reference)
세 번째 연산은 참조를 반환하는 거예요. 함수가 참조를 반환하면 호출한 쪽에서 그 결과에 참조를 묶을 수 있어요. 자세한 내용은 아래 「참조 반환」 절에서 이어져요.
레퍼런스가 아닌 것
앞에서 말했듯이, 레퍼런스는 포인터가 아니에요. 그래서 아래 같은 코드는 기대한 대로 동작하지 않아요.
<?php
function foo(&$var)
{
$var =& $GLOBALS["baz"];
}
foo($bar);
실제로 일어나는 일은 이래요. foo 안의 $var는 호출한 쪽의 $bar와 일단 묶이지만, 그다음 $GLOBALS["baz"]에 다시 묶여버려요. 호출한 쪽의 $bar를 다른 곳에 묶는 건 레퍼런스 메커니즘으로는 불가능해요. 왜냐하면 $bar는 함수 foo 안에 존재하지 않거든요.($var로 표현되지만, $var는 호출한 쪽의 심볼 테이블에 있는 이름-값 바인딩을 갖고 있지 않아요.) 함수가 고른 변수를 참조하고 싶다면 참조 반환을 쓰면 돼요.
참조에 의한 전달 (Passing by Reference)
함수에 변수를 참조로 넘기면, 함수 안에서 그 변수를 수정할 수 있어요. 문법은 이렇게 생겼어요.
<?php
function foo(&$var)
{
$var++;
}
$a = 5;
foo($a);
print $a; // $a is 6 here
참고: 함수 호출에는 참조 기호가 없어요. 참조 기호는 함수 정의에만 붙어요. 정의만 올바르게 되어 있으면 인수를 참조로 넘기는 건 자동으로 처리됩니다.
참조로 넘길 수 있는 것들은 이래요.
- 변수 — 예:
foo($a) - 함수에서 반환된 참조 — 예:
<?php
function foo(&$var)
{
$var++;
print $var;
}
function &bar()
{
$a = 5;
return $a;
}
foo(bar());
이 밖의 다른 표현식은 참조로 넘기면 안 돼요. 결과가 정의되지 않기 때문이에요. 아래처럼 참조로 넘기려는 코드는 모두 잘못된 예시예요.
<?php
function foo(&$var)
{
$var++;
}
function bar() // Note the missing &
{
$a = 5;
return $a;
}
foo(bar()); // Produces a notice
foo($a = 5); // Expression, not variable
foo(5); // Produces fatal error
class Foobar {}
foo(new Foobar()) // Produces a notice as of PHP 7.0.7
// Notice: Only variables should be passed by reference
foo($a = 5) 처럼 대입식이나 foo(5)처럼 상수, foo(new Foobar())처럼 객체 생성 결과를 넘기면 레퍼런스로 받을 수 없어요. 레퍼런스는 변수 자리를 넘기는 것이기 때문에, "값"이 아닌 "자리"가 있을 때만 동작한다고 기억하면 돼요.
참조 반환 (Returning References)
참조 반환은 어떤 변수에 참조를 묶을지 함수가 찾아줄 때 유용해요. 성능을 높이려고 참조를 반환하면 안 돼요. 엔진이 알아서 최적화하기 때문이에요. 정당한 기술적 이유가 있을 때만 참조를 반환하세요. 문법은 이렇게 돼요.
<?php
class Foo
{
public $value = 42;
public function &getValue()
{
return $this->value;
}
}
$obj = new Foo();
$myValue = &$obj->getValue(); // $myValue is a reference to $obj->value, which is 42
$obj->value = 2;
echo $myValue; // Prints the new value of $obj->value, i.e. 2
이 예제에서 getValue 함수가 반환하는 객체의 프로퍼티는, 참조 문법을 쓰지 않았을 때처럼 복사본이 아니라 원본 그대로가 되요. 그래서 $obj->value = 2;로 값을 바꾸면 $myValue에도 그 변경이 그대로 반영되는 거죠.
참고: 매개변수 전달과 달리, 여기서는
&를 두 곳에 써야 해요. 하나는 참조(복사본이 아니라)로 반환하겠다는 표시, 다른 하나는$myValue에 일반 대입이 아니라 참조 바인딩을 하겠다는 표시예요.
참고:
return ($this->value);처럼 괄호를 쳐서 참조를 반환하려고 하면 동작하지 않아요. 그것은 변수가 아니라 표현식의 결과를 반환하려는 셈이 되거든요. 함수에서는 변수만 참조로 반환할 수 있어요.
반환된 참조를 사용하려면 반드시 참조 대입을 해야 해요.
<?php
function &collector()
{
static $collection = array();
return $collection;
}
$collection = &collector();
// Now the $collection is a referenced variable that references the static array inside the function
$collection[] = 'foo';
print_r(collector());
위 예제는 이렇게 출력돼요.
Array
(
[0] => foo
)
참고:
&없이$collection = collector();처럼 대입하면, 함수가 반환한 참조가 아니라 값의 복사본을 받게 돼요.
반환된 참조를 또 다른 참조 인자 함수에 넘길 수도 있어요. 문법은 이래요.
<?php
function &collector()
{
static $collection = array();
return $collection;
}
array_push(collector(), 'foo');
참고:
array_push(&collector(), 'foo');처럼 쓰면 동작하지 않아요. fatal error가 납니다.
참조 해제 (Unsetting References)
unset()으로 레퍼런스를 해제하면, 변수 이름과 변수 내용 사이의 바인딩만 끊어져요. 변수 내용 자체가 파괴되는 건 아니에요. 예를 들어,
<?php
$a = 1;
$b =& $a;
unset($a);
var_dump($a);
var_dump($b);
위 코드에서 unset($a)를 해도 $b는 해제되지 않고 $a만 해제돼요.
이것도 유닉스의 unlink() 호출과 비슷하다고 생각하면 좋아요. 파일시스템에서 unlink는 파일을 지우는 게 아니라 그 이름으로 가리키는 항목 하나를 끊는 것뿐이죠. 마찬가지로 레퍼런스 해제는 그 이름으로의 접근만 끊고, 실제 내용은 다른 이름이 여전히 참조하고 있으면 계속 살아있어요.
레퍼런스 알아보기 (Spotting References)
PHP의 많은 문법 구조가 참조 메커니즘으로 구현돼 있어요. 그래서 지금까지 설명한 참조 바인딩 규칙이 그 구조들에도 그대로 적용돼요. 앞에서 이미 참조에 의한 전달과 참조 반환은 다뤘고, 참조를 사용하는 다른 구조들도 있어요.
global 참조
global $var 로 변수를 선언하면, 그건 사실 전역 변수에 대한 참조를 만드는 것이에요. 즉 이 코드는,
<?php
$var =& $GLOBALS["var"];
이 코드와 같은 뜻이에요.
따라서 $var를 unset()해도 전역 변수는 해제되지 않아요. $var는 $GLOBALS["var"]의 참조일 뿐이니까, 그 참조를 끊어도 원본 전역 변수는 그대로 남아있죠.