변수 유효범위

변수 유효범위

PHP 변수의 유효범위(scope, 스코프)가 어떻게 결정되는지 살펴봐요. 함수 안팎에서 변수가 언제 접근 가능한지, 그리고 global 키워드와 static 변수가 어떤 역할을 하는지 다룹니다.

출처: Variable scope

본문

변수의 유효범위(scope)란 그 변수가 정의된 문맥(context)을 말해요. PHP에는 함수 유효범위와 전역(global) 유효범위가 있습니다.

함수 밖에서 정의된 변수는 전역 유효범위로 한정돼요. 파일이 include되면, 그 파일 안의 코드는 include가 일어난 줄의 변수 유효범위를 그대로 물려받습니다.

<?php
$a = 1;
include 'b.inc'; // Variable $a will be available within b.inc

이름 있는 함수나 익명 함수 안에서 만들어진 변수는 모두 그 함수 본문의 유효범위로 한정됩니다. 다만 화살표 함수(arrow function)는 부모 유효범위의 변수를 바인딩해서 함수 본문 안에서 쓸 수 있게 해주죠.

호출하는 파일의 어떤 함수 안에서 include가 일어난다면, 호출된 파일 안의 변수들은 마치 호출하는 함수 안에서 정의된 것처럼 사용할 수 있어요.

<?php
$a = 1; // global scope

function test()
{ 
    var_dump($a); // Variable $a is undefined as it refers to a local version of $a
}

test();

위 예제는 정의되지 않은 변수라서 E_WARNING(PHP 8.0.0 이전에는 E_NOTICE)을 발생시켜요. echo 문이 로컬 버전의 $a 변수를 참조하는데, 그 유효범위 안에 값이 할당되어 있지 않기 때문이죠.

이건 C 언어와는 조금 달라요. C에서는 전역 변수가 특별히 로컬 정의로 덮어쓰지 않는 한 함수에서 자동으로 사용 가능하거든요. 그래서 사람들이 실수로 전역 변수를 바꿔버리는 문제가 생길 수 있는데, PHP에서는 함수 안에서 쓰려면 반드시 해당 전역 변수를 global로 선언해야 합니다.

global 키워드

global 키워드는 전역 유효범위의 변수를 로컬 유효범위로 바인딩할 때 사용해요. 변수 여러 개를 나열하거나 하나만 쓸 수도 있습니다. 같은 이름의 전역 변수를 참조하는 로컬 변수가 만들어지고, 그 전역 변수가 없으면 전역 유효범위에 변수가 만들어지면서 null이 할당되죠.

<?php
$a = 1;
$b = 2;

function Sum()
{
    global $a, $b;

    $b = $a + $b;
} 

Sum();
echo $b;

3

함수 안에서 $a$bglobal로 선언하면, 두 변수에 대한 모든 참조가 전역 버전을 가리키게 돼요. 함수가 다룰 수 있는 전역 변수의 개수에는 제한이 없습니다.

전역 유효범위의 변수에 접근하는 두 번째 방법은 PHP가 특별히 정의해 둔 $GLOBALS 배열을 쓰는 거예요. 위 예제는 다음과 같이 다시 쓸 수 있죠.

<?php
$a = 1;
$b = 2;

function Sum()
{
    $GLOBALS['b'] = $GLOBALS['a'] + $GLOBALS['b'];
} 

Sum();
echo $b;

$GLOBALS 배열은 전역 변수의 이름이 키(key)가 되고 그 변수의 내용이 배열 요소 값이 되는 연관 배열이에요. $GLOBALS는 슈퍼글로벌이라 어떤 유효범위에서든 존재한다는 점을 눈여겨보세요. 슈퍼글로벌의 힘을 보여주는 예제를 하나 볼게요.

<?php
function test_superglobal()
{
    echo $_POST['name'];
}

참고: 함수 밖에서 global 키워드를 쓰는 건 오류가 아니에요. 파일이 함수 안에서 include될 때는 사용할 수 있습니다.

static 변수 사용하기

변수 유효범위의 또 하나 중요한 특징은 static 변수예요. static 변수는 로컬 함수 유효범위에만 존재하지만, 프로그램 실행이 그 유효범위를 벗어나도 값을 잃지 않습니다. 다음 예제를 볼까요.

<?php
function test()
{
    $a = 0;
    echo $a . PHP_EOL;
    $a++;
}

test();
test();
test();

이 함수는 호출할 때마다 $a0으로 설정하고 0을 출력하니 꽤 쓸모없어요. 증가를 담당하는 $a++도 함수가 끝나면 $a가 사라져 버려서 아무 의미가 없죠. 현재 카운트를 잃지 않는 쓸모 있는 카운팅 함수를 만들려면 $a를 static으로 선언하면 됩니다.

<?php
function test()
{
    static $a = 0;
    echo $a . PHP_EOL;
    $a++;
}

test();
test();
test();

이제 $a는 함수의 첫 번째 호출에서만 초기화되고, test()가 호출될 때마다 $a의 값을 출력한 뒤 증가시킵니다.

static 변수는 재귀 함수를 다루는 방법을 제공하기도 해요. 다음 간단한 함수는 static 변수 $count로 언제 멈출지 알면서 10까지 재귀적으로 셉니다.

<?php
function test()
{
    static $count = 0;

    $count++;
    echo $count . PHP_EOL;
    if ($count < 10) {
        test();
    }
    $count--;
}

test();

PHP 8.3.0 이전에는 static 변수를 상수 표현식으로만 초기화할 수 있었어요. PHP 8.3.0부터는 동적 표현식(예: 함수 호출)도 허용됩니다.

<?php
function foo(){
    static $int = 0;          // correct 
    static $int = 1+2;        // correct
    static $int = sqrt(121);  // correct as of PHP 8.3.0

    $int++;
    echo $int;
}

익명 함수 안의 static 변수는 그 특정 함수 인스턴스 안에서만 유지돼요. 익명 함수가 호출될 때마다 다시 만들어진다면, static 변수도 다시 초기화됩니다.

<?php
function exampleFunction($input) {
    $result = (static function () use ($input) {
        static $counter = 0;
        $counter++;
        return "Input: $input, Counter: $counter\n";
    });

    return $result();
}

// Calls to exampleFunction will recreate the anonymous function, so the static
// variable does not retain its value.
echo exampleFunction('A'); // Outputs: Input: A, Counter: 1
echo exampleFunction('B'); // Outputs: Input: B, Counter: 1

PHP 8.1.0부터는 static 변수를 사용하는 메서드가 상속만 되고(오버라이드되지 않고) 상속될 때, 그 상속된 메서드는 부모 메서드와 static 변수를 공유하게 됐어요. 즉 메서드 안의 static 변수는 이제 static 프로퍼티와 같은 방식으로 동작합니다.

PHP 8.3.0부터 static 변수는 임의의 표현식으로 초기화할 수 있어요. 예를 들어 메서드 호출로 static 변수를 초기화할 수도 있죠.

<?php
class Foo {
    public static function counter() {
        static $counter = 0;
        $counter++;
        return $counter;
    }
}
class Bar extends Foo {}
var_dump(Foo::counter()); // int(1)
var_dump(Foo::counter()); // int(2)
var_dump(Bar::counter()); // int(3), prior to PHP 8.1.0 int(1)
var_dump(Bar::counter()); // int(4), prior to PHP 8.1.0 int(2)

전역·static 변수와 참조

PHP는 변수에 대한 staticglobal 수식(modifier)을 참조(reference)로 구현해요. 예를 들어 global 문으로 함수 유효범위 안으로 들여온 진짜 전역 변수는 사실 전역 변수에 대한 참조를 만들어냅니다. 이 때문에 예상치 못한 동작이 생길 수 있는데, 다음 예제가 그 이야기를 해요.

<?php
function test_global_ref() {
    global $obj;
    $new = new stdClass;
    $obj = &$new;
}

function test_global_noref() {
    global $obj;
    $new = new stdClass;
    $obj = $new;
}

test_global_ref();
var_dump($obj);
test_global_noref();
var_dump($obj);

NULL
object(stdClass)#1 (0) {
}

비슷한 동작이 static 문에도 적용돼요. 참조는 static으로 저장되지 않습니다.

<?php
function &get_instance_ref() {
    static $obj;

    echo 'Static object: ';
    var_dump($obj);
    if (!isset($obj)) {
        $new = new stdClass;
        // Assign a reference to the static variable
        $obj = &$new;
    }
    if (!isset($obj->property)) {
        $obj->property = 1;
    } else {
        $obj->property++;
    }
    return $obj;
}

function &get_instance_noref() {
    static $obj;

    echo 'Static object: ';
    var_dump($obj);
    if (!isset($obj)) {
        $new = new stdClass;
        // Assign the object to the static variable
        $obj = $new;
    }
    if (!isset($obj->property)) {
        $obj->property = 1;
    } else {
        $obj->property++;
    }
    return $obj;
}

$obj1 = get_instance_ref();
$still_obj1 = get_instance_ref();
echo "\n";
$obj2 = get_instance_noref();
$still_obj2 = get_instance_noref();

Static object: NULL
Static object: NULL

Static object: NULL
Static object: object(stdClass)#3 (1) {
  ["property"]=>
  int(1)
}

이 예제는 static 변수에 참조를 할당하면, &get_instance_ref() 함수를 두 번째로 호출할 때 그 참조가 기억되지 않는다는 걸 보여줘요.

더 알아보기