개발하다 보면 더 이상 필요 없는 변수나 배열 요소를 삭제해야 할 상황이 자주 생긴다. 대부분의 개발자는 단순히 `unset()`을 호출하면 된다고 생각하지만, 실제로는 메모리 해제 시점, 배열 구조 변화, 그리고 반복문 내에서의 예상치 못한 동작까지 신경 써야 할 게 많다. 이번에는 `unset()`의 정확한 동작 원리부터 실전에서 자주 하는 실수까지 완벽하게 정리해서 소개하겠다.
`unset()`은 단순히 변수나 배열 요소를 "삭제"하는 함수가 아니라, 변수 자신을 Zend Engine의 심볼 테이블에서 제거하는 구문이다. 중요한 점은 메모리를 즉시 해제하는 게 아니라 변수에 대한 참조를 끊는 것이라는 뜻이다. PHP는 자동 가비지 컬렉션을 실행하므로, `unset()` 후 실제 메모리 해제는 참조 카운트가 0이 될 때 발생한다.
예를 들어, 변수 하나를 만들고 바로 `unset()`하면 메모리에서 제거된다. 하지만 그 변수를 다른 변수에 할당했다면, `unset()`은 한쪽 참조만 끊을 뿐 실제 데이터는 남아있다.
✗ 잘못된 코드: unset() 후 변수 참조
<?php
$name = "John";
unset($name);
echo $name; // PHP Notice: Undefined variable: $name
?>
`unset()`으로 변수를 제거한 후 접근하면 Undefined 에러가 발생한다. 대신 `empty()` 체크로 변수 존재 여부를 먼저 확인해야 한다.
✓ 올바른 코드: 조건부 접근
<?php
$name = "John";
unset($name);
if (isset($name)) {
echo $name;
} else {
echo "Variable does not exist";
}
?>
출력: `Variable does not exist`
배열에서 특정 요소를 `unset()`으로 제거하면, 배열의 키(인덱스)가 자동으로 재정렬되지 않는다. 이는 많은 개발자들이 놓치는 함정이다.
✗ 잘못된 코드: unset() 후 배열 재구성 미흡
<?php
$fruits = array(0 => "Apple", 1 => "Banana", 2 => "Cherry");
unset($fruits[1]);
var_dump($fruits);
// 결과: array(2) { [0]=>string "Apple" [2]=>string "Cherry" }
// 인덱스 1이 빠졌다!
?>
Banana를 제거했지만 Cherry의 인덱스는 여전히 2로 남아있다. JSON 직렬화나 순차 처리 시 문제가 될 수 있다.
✓ 올바른 코드: array_values()로 재인덱싱
<?php
$fruits = array(0 => "Apple", 1 => "Banana", 2 => "Cherry");
unset($fruits[1]);
$fruits = array_values($fruits); // 키를 0부터 다시 정렬
var_dump($fruits);
// 결과: array(2) { [0]=>string "Apple" [1]=>string "Cherry" }
?>
이제 배열이 0부터 시작하는 연속된 인덱스를 가진다. API 응답이나 JSON 직렬화에서 예상한 형태가 나온다.
연관 배열에서는 키 문자열이 유지되므로 `array_values()`가 필요 없는 경우가 많다.
<?php
$user = array(
"name" => "John",
"email" => "john@example.com",
"phone" => "010-1234-5678"
);
unset($user["phone"]);
var_dump($user);
// 결과: array(2) { ["name"]=>"John" ["email"]=>"john@example.com" }
?>
연관 배열은 키 순서가 영향을 받지 않으므로 `unset()` 후에도 문제없다. 다만 데이터 구조 일관성을 위해 필요한 키만 빼도록 주의하자.
반복문 내에서 `unset()`을 사용하면 예상 밖의 동작이 발생할 수 있다. 특히 `foreach` 루프에서 참조 변수를 사용할 때 더 위험하다.
✗ 잘못된 코드: foreach 중 참조 변수 unset()
<?php
$numbers = array(1, 2, 3, 4, 5);
foreach ($numbers as &$num) {
if ($num == 3) {
unset($num); // 참조만 제거, 배열 자체는 유지
} else {
$num *= 2;
}
}
var_dump($numbers);
// 결과: array(5) { [0]=>int 2 [1]=>int 4 [2]=>int 3 [3]=>int 8 [4]=>int 10 }
// 3은 수정되지 않았다
?>
참조 변수를 `unset()`하면 그 순간의 참조만 끊길 뿐, 배열 요소 자체는 삭제되지 않는다.
✓ 올바른 코드: 조건부로 배열 요소 스킵 또는 array_filter() 사용
<?php
$numbers = array(1, 2, 3, 4, 5);
$numbers = array_filter($numbers, function($num) {
return $num != 3;
});
$numbers = array_values($numbers); // 재인덱싱
var_dump($numbers);
// 결과: array(4) { [0]=>int 1 [1]=>int 2 [2]=>int 4 [3]=>int 5 }
?>
`array_filter()`는 조건에 맞는 요소만 남기므로 명확하고 안전하다. 반복문이 끝난 후 한 번에 처리되므로 참조 문제도 없다.
✗ 잘못된 코드: for 루프 내 배열 요소 unset()
<?php
$items = array("a", "b", "c", "d", "e");
for ($i = 0; $i < count($items); $i++) {
if ($items[$i] == "c") {
unset($items[$i]);
// 인덱스가 재구성되지 않아 다음 반복에서 오류 발생 가능
} else {
echo $items[$i] . "\n";
}
}
var_dump($items);
?>
이 코드는 배열 구조가 변경되므로 예상 밖의 결과를 낼 수 있다.
✓ 올바른 코드: 루프 완료 후 재인덱싱
<?php
$items = array("a", "b", "c", "d", "e");
$items = array_filter($items, function($item) {
return $item != "c";
});
$items = array_values($items);
foreach ($items as $item) {
echo $item . "\n";
}
// 출력: a b d e
?>
루프 외부에서 배열을 정리하는 것이 더 안전하고 명확하다.
<?php
$data = array("user" => "John", "age" => 30);
unset($data); // 배열 변수 전체 삭제
if (isset($data)) {
echo "Data exists";
} else {
echo "Data deleted"; // 이 문장이 출력됨
}
?>
<?php
$config = array(
"db_host" => "localhost",
"db_user" => "root",
"db_pass" => "password",
"debug" => true
);
// 여러 키를 한 번에 제거
unset($config["db_pass"], $config["debug"]);
var_dump($config);
// 결과: array(2) { ["db_host"]=>"localhost" ["db_user"]=>"root" }
?>
`unset()`은 쉼표로 구분된 여러 인자를 받을 수 있다. 민감한 정보(비밀번호)를 빠르게 제거할 때 유용하다.
| 방법 | 사용 사례 | 키 재정렬 | 메모리 해제 |
|---|---|---|---|
| `unset($arr[0])` | 배열 요소 제거 | 아니오 | 참조 제거 후 GC |
| `$arr[0] = null` | 값만 초기화 | 아니오 | 아니오(메모리 유지) |
| `array_splice($arr, 0, 1)` | 요소 제거 후 재정렬 필요 | 예 | 참조 제거 후 GC |
| `array_filter($arr, fn)` | 조건부 필터링 | 아니오(수동 필요) | 참조 제거 후 GC |
`unset()`은 간단하지만 키가 그대로 남는다. 순차 인덱스가 중요하면 `array_splice()` 또는 `array_filter()` + `array_values()`를 쓰자. 단순히 값을 비우는 것이라면 `null` 할당으로 충분하다.
API 응답에서 민감한 정보를 제거하고 클라이언트에 전송하는 경우를 생각해보자.
✗ 잘못된 코드: 반복문 내 직접 unset()
<?php
$users = array(
array("id" => 1, "name" => "John", "password" => "secret123"),
array("id" => 2, "name" => "Jane", "password" => "secret456"),
array("id" => 3, "name" => "Bob", "password" => "secret789")
);
// 각 사용자 객체에서 password 제거
foreach ($users as &$user) {
unset($user["password"]); // 작동하지만 참조 문제 발생 가능
}
unset($user); // 참조 끊기 필수
echo json_encode($users);
?>
이 방식은 작동하지만, 참조 변수 `&$user`를 쓴 후 반드시 `unset($user)`로 참조를 끊어야 한다. 그렇지 않으면 마지막 요소 참조가 유지되어 이후 수정 시 예상치 못한 동작이 발생할 수 있다.
✓ 올바른 코드: array_map() 또는 배열 재구성
<?php
$users = array(
array("id" => 1, "name" => "John", "password" => "secret123"),
array("id" => 2, "name" => "Jane", "password" => "secret456"),
array("id" => 3, "name" => "Bob", "password" => "secret789")
);
// array_map으로 각 사용자에서 password 제거
$users = array_map(function($user) {
unset($user["password"]);
return $user;
}, $users);
echo json_encode($users);
// 출력: [{"id":1,"name":"John"},{"id":2,"name":"Jane"},{"id":3,"name":"Bob"}]
?>
`array_map()`을 쓰면 참조 문제가 없고, 각 요소가 독립적으로 처리된다. 클로저 내에서 배열을 수정하고 반환하는 방식이 더 함수형이고 안전하다.
메모리 제약이 있는 환경에서 대용량 배열을 처리할 때는 `unset()`이 실제로 도움이 될 수 있다. 다만 인스턴스나 리소스가 포함된 배열은 `unset()` 후에도 소멸자(destructor)가 호출될 때까지 완전히 해제되지 않을 수 있다.
메모리 효율적 처리:
<?php
// 대용량 CSV 파일 행 처리
if (($handle = fopen("large_file.csv", "r")) !== FALSE) {
$batch = array();
$count = 0;
while (($row = fgetcsv($handle)) !== FALSE) {
$batch[] = $row;
$count++;
// 배치 처리: 1000행마다 저장
if ($count % 1000 == 0) {
processBatch($batch);
unset($batch); // 메모리 즉시 해제
$batch = array();
}
}
// 남은 데이터 처리
if (!empty($batch)) {
processBatch($batch);
unset($batch);
}
fclose($handle);
}
function processBatch($data) {
// 데이터 처리 로직
// INSERT INTO database ...
}
?>
이 패턴은 메모리 사용을 제한적으로 유지하면서 대용량 파일을 처리한다. 매 배치마다 `unset()`을 호출해 참조를 끊고, 새로운 배열을 시작한다.
`unset()`은 단순해 보이지만, 배열 키 구조, 참조 변수, 반복문 내 사용 시 예상 밖의 동작을 일으킬 수 있다. 단순히 변수를 지우려고 할 때는 문제없지만, 배열 요소를 제거한 후 순차 처리가 필요하면 `array_values()`로 재인덱싱하자. 루프 내에서 요소를 제거해야 한다면 `array_filter()`를 먼저 고려하고, 루프 후에 정리하는 방식이 훨씬 안전하다. 이런 작은 차이가 버그 없는 코드와 예상치 못한 오류를 구분한다. 이 글의 패턴을 참고해 배열 조작 방식을 다시 한 번 점검하면, 더 견고한 데이터 처리 로직을 만들 수 있을 것이다.