Backends PHP

PHP 예제로 살펴보는 즉시실행함수의 필요성과 구현방법

즉시실행함수(IIFE, Immediately-Invoked Function Expression)는 소스코드 중간에 익명함수를 실행하여, 임시로 사용할 변수들을 지역변수로 선언하여 사용하는 디자인 패턴을 말합니다. 즉시실행함수가 즐겨 활용되어 온 대표적인 언어는 자바스크립트이지만, 최근에 PHP에서도 7버전에 익명함수가 등장하면서 사용이 가능해졌습니다.

이번 포스팅에서는 먼저 전역변수의 위험성을 살펴봄으로써, 즉시실행함수가 왜 필요한지를 설명합니다. 이어서 자바스크립트와 PHP7에서 작성한 소스코드를 통해 즉시실행함수의 사용방법에 대하여 설명합니다. 마지막으로 즉시실행함수를 사용할 수 없는 PHP5 및 PHP4에서 유사한 효과를 가질 수 있는 소스코드를 소개할 것입니다.

 

변수명 충돌의 위험성

자바스크립트와 PHP는 웹 서비스의 프론트엔드와 백엔드를 각각 맡고 있는 서로 다른 언어이지만, 공교롭게도 전역영역에 로직을 작성하는 것이 기본이라는 공통점을 가지고 있습니다. 전역변수 덕분에 두 언어는 쉽게 배우고 활용할 수 있는 언어로 많은 사람들의 사랑을 받았습니다.

그러나 프로그램 언어의 관점에서 이것은 다소 위험한 방식입니다. 전역영역에 대부분의 소스코드가 모여있다보니, 앞부분에서 선언한 같은 이름의 변수를 뒷부분에서 다른 값으로 할당하는 변수명 충돌(Name collision)이 벌어지기 쉽기 때문입니다.

이것이 문제가 되는 이유는 소스코드의 여러 곳에서 사용하는 변수가 어떤 특정 부분에서 변조될 수 있기 때문입니다. 아래 예제는 그러한 사례를 잘 보여주는 PHP 소스코드입니다. 한 회사의 R&D 본부에 소속 부서원들을 입사연도별로 묶어 표로 출력하는 간단한 기능을 담고 있는데요. 사번의 가장 앞 4자리가 입사연도라는 사실에 착안하여, 사번에서 입사연도를 추출해 입사연도별로 부서원을 분류하여 출력하고 있습니다. 또한 표의 가장 아래행에는 부서의 이름과 총원을 표시하도록 하고 있습니다.

// 부서명
$name = 'R&D 본부';

// 부서원 (사번과 이름)
$workers = array(
	'201100027' => '성동일 차장',
	'201500165' => '박보검 대리',
	'201500202' => '이혜리 주임'
);

// 입사연도별 부서원 정리
$data = array();
foreach($workers as $no => $name)
{
	$data[substr($no, 0, 4)][] = $name;
}

// 테이블 출력
echo '<table>';
echo '<thead><th>입사년도</th><th>부서원</th></thead>';
echo '<tbody>';
foreach($data as $year => $names)
{
	echo '<tr>';
	echo '<th>' . $year . '년</th>';
	echo '<td>' . implode(', ', $names) . '</td>';
	echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
echo '<th>' . $name . ' 총원</th><td>' . count($workers) . '명</td>';
echo '</tfoot>';
echo '</table>';

변수명 충돌 예제 #1

실제로 위 예제코드를 실행해보면, 가장 아래 총원을 출력하는 부분에 우리가 원하는 결과가 표시되지 않음을 발견하게 됩니다. 좌측 칸이 “R&D 본부 총원”이라고 표시되어야 함에도 “이혜리 주임 총원”이라고 출력되는 결과가 나온 것인데요. (다음 문단을 읽기 전에 잠시 예제를 곱씹어보며 원인을 찾아보는 시간을 가지시면 좋겠습니다.)

원인은 13행의 foreach 문에 있습니다. 부서명을 담고 있던 변수 $name을 반복문의 각 요소를 할당할 목적으로 사용하여 덮어쓰고 말았던 것이지요. 결국 31행 도달했을 때는 반복문이 마지막으로 작동했을 때의 $name 값이 남게 되는 것입니다. 위 예제코드는 30여행 남짓의 짧은 소스코드였기에 원인을 쉽게 발견했지만, 중대형 프로젝트에서 이런 일이 벌어졌다면 include된 파일을 포함한 몇 천행의 소스코드를 해짚어야 할지도 모릅니다.

자바스크립트와 PHP는 소스코드가 길어지면 유지보수가 어려워지는 것으로 악명이 높은 이유가 여기에 있습니다. 전역영역에 로직이 작성되는 두 언어는 모두 개발시 전역변수 또한 폭넓게 쓰일 수 밖에 없습니다. 이는 소스코드의 일부분을 수정할 때도 변수명 충돌을 피하기 위해 소스코드 전체를 살펴보아야 하고, 디버깅 과정 또한 무척 고통스럽기 마련입니다. (이 때문에 C언어 등을 주력언어로 하는 분들은 전역변수의 참조를 GOTO문 만큼이나 악랄한 존재로 취급하기도 합니다.)

 

변수명 충돌을 피할 수 있는 즉시실행함수

즉시실행함수 패턴은 익명함수를 활용하여, 전역변수를 사용하는 로직을 지역변수를 사용하는 로직으로 바꾸어 놓습니다. 이를 통해 전역변수가 변수명 충돌로 다른 값에 오염될 가능성을 원천적으로 막을 수 있습니다.

아래 소스코드는 위에서 소개한 입사연도별 임직원 표 출력 코드에서 “입사연도별 부서원 정리” 부분에 즉시실행함수 패턴을 적용한 것입니다.

// 부서명
$name = 'R&D 본부';

// 부서원 (사번과 이름)
$workers = array(
	201100027 => '성동일 차장',
	201500165 => '박보검 대리',
	201500202 => '이혜리 주임'
);

// 입사연도별 부서원 정리 (★즉시실행함수 패턴 적용)
$data = (function($arr) {
	$res = array();
	foreach($arr as $no => $name)
	{
		$res[substr($no, 0, 4)][] = $name;
	}

	return $res;
})($workers);

// 테이블 출력
echo '<table>';
echo '<thead><th>입사년도</th><th>부서원</th></thead>';
echo '<tbody>';
foreach($data as $year => $names)
{
	echo '<tr>';
	echo '<th>' . $year . '년</th>';
	echo '<td>' . implode(', ', $names) . '</td>';
	echo '</tr>';
}
echo '</tbody>';
echo '<tfoot>';
echo '<th>' . $name . ' 총원</th><td>' . count($workers) . '명</td>';
echo '</tfoot>';
echo '</table>';

변경된 코드에서 처음 눈에 띄는 것은, $arr라는 이름의 인자를 1개 받는 익명함수입니다. 이에 따라 $workers라는 전역변수를 직접 참조하여 가공하였던 원래 로직은, 인자로 받은 지역변수를 참조하는 것으로 바뀌었습니다. 함수 안에서 입사연도별로 가공된 임직원 정보는 $res라는 지역변수에 담은 후 반환됩니다.

이어서 이 익명함수를 괄호( )가 감싸고 있고, 괄호에 이어서 위치한 새로운 괄호( )에 원래 코드에서 참조하였던 전역변수 $workers가 인자로 들어가 있음을 볼 수 있습니다. 이를 통해 괄호 안에서 정의한 익명함수에 첫 번째 인자로 $workers를 넣어, 함수가 만들어지는 즉시 이를 실행하는 것을 볼 수 있습니다. 마지막으로 이렇게 즉시실행된 익명함수의 반환값은 할당연산자에 의해 $data 변수에 들어가게 됩니다.

이처럼 단계를 하나하나 따져보면, 앞선 소스코드와 새로운 소스코드의 실행결과는 동일합니다. 그러나 두 코드에는 중요한 차이가 있습니다. 바로 foreach문에서 배열 각 요소의 키와 값을 할당하기 위해 사용한 변수 $no와 $name이 지역변수가 되었다는 것입니다. 이 변수들은 더이상 전역영역의 어떠한 값도 오염시키지 않고, 함수실행을 끝마치면 사라집니다. 변수명 충돌의 걱정에서 해방된 것입니다.

 

이미 작성된 소스코드에 즉시실행함수 패턴을 적용하기

전역변수를 참조하여 작성되어 있는 로직에 즉시실행함수 패턴을 적용하는 일은 간단합니다. 앞서 소개한 소스코드에서 11~16행에 즉시실행함수 패턴을 적용하는 과정은 아래와 같습니다.

  1. 익명함수 하나를 선언하고, 기존에 작성한 로직을 함수 안으로 옮깁니다.
    function()
    {
    	$data = array();
    	foreach($workers as $no => $name)
    	{
    		$data[substr($no, 0, 4)][] = $name;
    	}
    }
  2. 옮긴 로직에서 참조하였던 전역변수를 모두 함수의 인자로 삼습니다.
    function($workers)
    {
    	$data = array();
    	foreach($workers as $no => $name)
    	{
    		$data[substr($no, 0, 4)][] = $name;
    	}
    }
  3. 익명함수를 괄호( )로 감싼 후, 그 뒤에 다시 괄호( )를 열어 참조하였던 전역변수를 인자로 삼은 순서대로 다시 넣습니다.
    (function($workers)
    {
    	$data = array();
    	foreach($workers as $no => $name)
    	{
    		$data[substr($no, 0, 4)][] = $name;
    	}
    })($workers);
  4. 옮긴 로직을 실행한 결과로 산출되는 데이터들은 함수 실행 후에 반환값(Return Value)으로 받도록 합니다.
    $data = (function($workers)
    {
    	$data = array();
    	foreach($workers as $no => $name)
    	{
    		$data[substr($no, 0, 4)][] = $name;
    	}
    	return $data;
    })($workers);

 

PHP 7 미만 버전에서의 즉시실행함수

불행히도 즉시실행함수 패턴은 PHP 5.x 또는 PHP 4.x에서는 사용할 수 없습니다. 즉시실행함수 패턴에 필수적인 익명함수가 PHP 7에서 처음으로 등장하였기 때문입니다.

그러나 소스코드의 간결성에 조금 눈을 감는다면, PHP 7 미만 버전에서도 완전하지는 않지만 즉시실행함수 패턴의 효과를 얻을 수 있는 소스코드를 작성할 수 있습니다. 바로 1회용 함수를 정의한 후에 그 함수를 실행하도록 하는 것입니다. 아래의 예제에서 그 방법을 살펴볼 수 있습니다.

function arrangeWorkersByYear($workers)
{
	$data = array();
	foreach($workers as $no => $name)
	{
		$data[substr($no, 0, 4)][] = $name;
	}
	return $data;
}

$data = arrangeWorkersByYear($workers);

예제를 살펴보면, 즉시실행함수 대신 사용자 정의함수로 바뀐 것 외에는 앞서의 예제와 동일하게 작동함을 알 수 있습니다. 1회용 함수를 정의하여 사용한다는 것이 아무래도 최선의 방법처럼 느껴지지는 않지만, 변수명 충돌과 이로 인한 데이터 오염에 대한 분명한 해결책임은 부정하기 어려울 것입니다.

Leave a Reply

Your email address will not be published. Required fields are marked *