다차원 설정 배열이나 외부 API 응답 데이터를 병합할 때 PHP의 기본 함수인 array_merge_recursive()를 썼다가 기존 값이 덮어씌워지지 않고 원치 않게 배열로 감싸지는 이상한 현상을 경험해봤을 것이다.
다만 왜 이런 동작이 발생하는지, 기본 함수만으로는 다차원 연관 배열의 깊은 병합(Deep Merge)을 제대로 처리할 수 없다는 사실을 모르는 개발자가 많다.
이번에는 array_merge_recursive()의 치명적인 한계와 동작 원리를 파악하고, 다차원 배열을 안전하게 덮어쓰며 병합하는 array_merge_deep() 서포트 함수 제작 및 활용법을 완벽하게 정리해서 소개하겠다.
PHP에서 두 개 이상의 배열을 합칠 때 주로 array_merge()나 array_merge_recursive()를 사용한다. 일차원 배열에서는 array_merge()가 키를 기준으로 값을 깔끔하게 덮어쓴다. 문제 배경을 알기 위해 기본 제공되는 두 함수의 동작 특성을 먼저 파악하자.
| 함수명 | 중첩 배열 지원 | 동일 문자열 키 발견 시 동작 | 주 활용 목적 |
|---|---|---|---|
| array_merge() | 지원 안 함(1차원만) | 나중 배열의 값으로 완전 덮어씀 | 일차원 배열 병합, 숫자 키 재배열 |
| array_merge_recursive() | 지원함(재귀적 탐색) | 덮어쓰지 않고 두 값을 배열로 묶어 병합 | 동일 키의 모든 값을 배열로 보존할 때 |
시스템 기본 설정 배열 위에 사용자 지정 설정을 덮어씌우는 실무 로직에서 array_merge_recursive()를 사용하면 기대와 전혀 다른 결과가 나온다. 기존 문자열 값을 새 값으로 교체하고 싶지만, PHP 내부 동작 방식상 두 값을 원소로 갖는 새로운 배열이 만들어지기 때문이다.
실제 코드로 동작 방식의 차이를 확인해보자. 설정값을 관리하는 애플리케이션 환경을 가정한다.
✗ 잘못된 코드 (array_merge_recursive 사용시)
$defaultConfig = [
'db' => [
'host' => 'localhost',
'port' => 3306
]
];
$userConfig = [
'db' => [
'host' => '192.168.0.100'
]
];
$result = array_merge_recursive($defaultConfig, $userConfig);
print_r($result);출력 결과
Array
(
[db] => Array
(
[host] => Array
(
[0] => localhost
[1] => 192.168.0.100
)
[port] => 3306
)
)db.host가 '192.168.0.100'으로 교체되어야 하지만, 문자열 데이터가 아닌 배열로 변경된다. 이 상태로 데이터베이스 연결을 시도하면 PDO나 MySQLi 엔진에서 문자열이 아닌 배열이 전달되어 Fatal Error가 발생한다.
이 문제를 해결하려면 연관 배열(Associative Array)의 문자열 키일 때는 기존 값을 덮어쓰고, 또 다른 배열을 만났을 때만 재귀적으로 하위 키를 탐색하는 커스텀 서포트 함수가 필수적이다.
✓ 올바른 코드 (array_merge_deep 서포트 함수 제작)
if (!function_exists('array_merge_deep')) {
/**
* 다차원 배열을 깊은 수준까지 탐색하여 안전하게 덮어쓰며 병합한다.
*
* @param array ...$arrays 병합할 배열들
* @return array 병합 완료된 배열
*/
function array_merge_deep(array ...$arrays): array
{
$merged = [];
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// 키가 문자열이고, 현재 값과 기존 값 모두 배열인 경우 재귀 호출
if (is_string($key) && isset($merged[$key]) && is_array($merged[$key]) && is_array($value)) {
$merged[$key] = array_merge_deep($merged[$key], $value);
} elseif (is_int($key)) {
// 순차 숫자 인덱스 키는 덮어쓰지 않고 순차적으로 추가
$merged[] = $value;
} else {
// 문자열 키의 단순 값은 덮어쓰기 수행
$merged[$key] = $value;
}
}
}
return $merged;
}
}
// 실전 적용 예시
$defaultConfig = [
'db' => [
'host' => 'localhost',
'port' => 3306
],
'modules' => ['auth', 'mail']
];
$userConfig = [
'db' => [
'host' => '192.168.0.100'
],
'modules' => ['payment']
];
$result = array_merge_deep($defaultConfig, $userConfig);
print_r($result);출력 결과
Array
(
[db] => Array
(
[host] => 192.168.0.100
[port] => 3306
)
[modules] => Array
(
[0] => auth
[1] => mail
[2] => payment
)
)원했던 대로 db.host 설정값은 깔끔하게 덮어씌워졌고, db.port 설정은 원본을 유지했다. modules 배열 같은 숫자 인덱스 리스트는 덮어쓰지 않고 원소가 순차적으로 병합되는 완성도 높은 결과를 보여준다.
서포트 함수를 제작할 때 자주 범하는 실수는 숫자로 된 인덱스 키(Indexed Array)와 문자열 기반 연관 배열(Associative Array)을 동일하게 다루는 것이다.
✗ 잘못된 구현 방식
인덱스 배열의 0번, 1번 키를 단순 $merged[$key] = $value 형태로 처리하면 리스트 형태의 데이터가 합쳐지지 않고 앞선 원소를 지워버리는 문제가 발생한다.
✓ 올바른 구현 방식
is_int($key) 조건문으로 인덱스 배열 여부를 판단하여 $merged[] = $value 형태로 순차 삽입 처리해야 데이터 손실 없는 병합 연산이 가능하다.
다차원 배열 깊은 병합은 웹 애플리케이션의 설정 관리, 공통 옵션 처리, REST API 패치(PATCH) 요청 데이터 동기화에 필수적인 작업이다. 안전하고 유연한 서포트 함수 사용 습관이 모여서 정교하고 버그 없는 백엔드 시스템을 만든다는 점을 잊지 말자. 이 글의 array_merge_deep() 코드 블록을 프로젝트 공통 헬퍼 파일에 추가해 활용하면 다차원 데이터 처리 중 발생하는 예기치 못한 배열 변형 에러를 완벽하게 차단할 수 있을 것이다.