다차원 설정 배열이나 외부 API 응답 데이터를 병합할 때 PHP의 기본 함수인 array_merge_recursive()를 썼다가 기존 값이 덮어씌워지지 않고 원치 않게 배열로 감싸지는 이상한 현상을 경험해봤을 것이다.
다만 왜 이런 동작이 발생하는지, 기본 함수만으로는 다차원 연관 배열의 깊은 병합(Deep Merge)을 제대로 처리할 수 없다는 사실을 모르는 개발자가 많다.
이번에는 array_merge_recursive()의 치명적인 한계와 동작 원리를 파악하고, 다차원 배열을 안전하게 덮어쓰며 병합하는 array_merge_deep() 서포트 함수 제작 및 활용법을 완벽하게 정리해서 소개하겠다.

 

1. PHP 기본 배열 병합 함수의 한계 이해하기

PHP에서 두 개 이상의 배열을 합칠 때 주로 array_merge()나 array_merge_recursive()를 사용한다. 일차원 배열에서는 array_merge()가 키를 기준으로 값을 깔끔하게 덮어쓴다. 문제 배경을 알기 위해 기본 제공되는 두 함수의 동작 특성을 먼저 파악하자.

함수명중첩 배열 지원동일 문자열 키 발견 시 동작주 활용 목적
array_merge()지원 안 함(1차원만)나중 배열의 값으로 완전 덮어씀일차원 배열 병합, 숫자 키 재배열
array_merge_recursive()지원함(재귀적 탐색)덮어쓰지 않고 두 값을 배열로 묶어 병합동일 키의 모든 값을 배열로 보존할 때

시스템 기본 설정 배열 위에 사용자 지정 설정을 덮어씌우는 실무 로직에서 array_merge_recursive()를 사용하면 기대와 전혀 다른 결과가 나온다. 기존 문자열 값을 새 값으로 교체하고 싶지만, PHP 내부 동작 방식상 두 값을 원소로 갖는 새로운 배열이 만들어지기 때문이다.

 

2. array_merge_recursive()가 문제를 일으키는 원인

실제 코드로 동작 방식의 차이를 확인해보자. 설정값을 관리하는 애플리케이션 환경을 가정한다.

✗ 잘못된 코드 (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가 발생한다.

 

3. array_merge_deep() 서포트 함수 구현

이 문제를 해결하려면 연관 배열(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 배열 같은 숫자 인덱스 리스트는 덮어쓰지 않고 원소가 순차적으로 병합되는 완성도 높은 결과를 보여준다.

 

4. 실무 적용 시 주의사항과 실수 방지

서포트 함수를 제작할 때 자주 범하는 실수는 숫자로 된 인덱스 키(Indexed Array)와 문자열 기반 연관 배열(Associative Array)을 동일하게 다루는 것이다.

✗ 잘못된 구현 방식
인덱스 배열의 0번, 1번 키를 단순 $merged[$key] = $value 형태로 처리하면 리스트 형태의 데이터가 합쳐지지 않고 앞선 원소를 지워버리는 문제가 발생한다.

✓ 올바른 구현 방식
is_int($key) 조건문으로 인덱스 배열 여부를 판단하여 $merged[] = $value 형태로 순차 삽입 처리해야 데이터 손실 없는 병합 연산이 가능하다.

 

5. 정리 및 결론

다차원 배열 깊은 병합은 웹 애플리케이션의 설정 관리, 공통 옵션 처리, REST API 패치(PATCH) 요청 데이터 동기화에 필수적인 작업이다. 안전하고 유연한 서포트 함수 사용 습관이 모여서 정교하고 버그 없는 백엔드 시스템을 만든다는 점을 잊지 말자. 이 글의 array_merge_deep() 코드 블록을 프로젝트 공통 헬퍼 파일에 추가해 활용하면 다차원 데이터 처리 중 발생하는 예기치 못한 배열 변형 에러를 완벽하게 차단할 수 있을 것이다.