PHP로 개발하다 보면 갑자기 콘솔에 떠오르는 경고 메시지가 있다. 바로 'Undefined property' 경고다. 대부분의 개발자들은 이 경고를 무시하거나 에러 로그를 끄는 식으로 대처한다. 다만 이게 왜 떴는지, 정확히 뭐가 잘못된 건지를 모른 채 넘어가는 경우가 많다. 이번에는 Undefined property 오류가 정확히 뭔지, 왜 발생하는지, 어떻게 완벽하게 해결하는지 실무 기반으로 정리해서 소개하겠다.

 

Undefined property 오류는 어떤 상황에서 뜨나

PHP에서 객체의 속성(property)에 접근할 때 그 속성이 정의되지 않았으면 경고를 던진다. 예를 들어 $user 객체가 있는데 $user->name이라는 속성이 없으면 'Undefined property' 경고가 나온다. 웹 개발에서는 데이터베이스 쿼리 결과를 객체로 받거나 API 응답을 stdClass로 변환할 때 이 문제가 가장 자주 터진다.

경고 수준이므로 스크립트가 멈추지는 않는다. 다만 개발 환경에서는 로그에 쌓이고 프로덕션 환경에서는 사용자 입장에서 느려진다. 게다가 이 경고가 누적되면 로그 파일의 용량이 계속 늘어나는 문제까지 생긴다.

 

실제 오류 사례와 원인 분석

 

상황 1: 데이터베이스 쿼리 결과 접근

✗ 잘못된 코드

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $pdo->query('SELECT id, name FROM users WHERE id = 1');
$user = $stmt->fetch(PDO::FETCH_OBJ);

echo $user->id;      // 정상
echo $user->email;   // Warning: Undefined property
echo $user->phone;   // Warning: Undefined property
?>

원인: SELECT 문에서 email, phone 컬럼을 조회하지 않았는데 접근하려고 한다. 데이터베이스에는 존재하는 컬럼이지만 현재 쿼리 결과에는 포함되지 않아서다.

 

✓ 올바른 코드

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
// 실제 필요한 모든 컬럼을 SELECT에 포함
$stmt = $pdo->query('SELECT id, name, email, phone FROM users WHERE id = 1');
$user = $stmt->fetch(PDO::FETCH_OBJ);

echo $user->id;      // ✓
echo $user->email;   // ✓
echo $user->phone;   // ✓
?>

또는 속성 존재 여부를 미리 확인:

<?php
// isset()으로 먼저 체크
if (isset($user->email)) {
    echo $user->email;
}

// property_exists()로 체크 (더 명확함)
if (property_exists($user, 'email')) {
    echo $user->email;
}
?>

 

상황 2: JSON API 응답 처리

✗ 잘못된 코드

<?php
$response = file_get_contents('https://api.example.com/user/1');
$data = json_decode($response);

echo $data->user->name;        // 정상
echo $data->user->profile_url; // Warning: Undefined property (필드가 없을 수 있음)
echo $data->meta->status;       // Warning: Undefined property (meta 객체가 없을 수 있음)
?>

원인: API 응답의 필드가 항상 반환되는 게 아니다. 특정 조건에서만 있는 필드를 무조건 접근하면 경고가 난다.

 

✓ 올바른 코드

<?php
$response = file_get_contents('https://api.example.com/user/1');
$data = json_decode($response);

// 1) isset() + is_object() 조합
if (isset($data->user) && is_object($data->user) && isset($data->user->profile_url)) {
    echo $data->user->profile_url;
}

// 2) property_exists() 사용 (더 명확)
if (property_exists($data, 'meta') && property_exists($data->meta, 'status')) {
    echo $data->meta->status;
}

// 3) Null Coalescing Operator (PHP 7.0+)
echo $data->user->profile_url ?? 'no_profile';
echo $data->meta->status ?? 'unknown';
?>

 

상황 3: 클래스 인스턴스 접근

✗ 잘못된 코드

<?php
class User {
    public $id;
    public $name;
    
    public function __construct($id, $name) {
        $this->id = $id;
        $this->name = $name;
    }
}

$user = new User(1, 'John');
echo $user->id;       // ✓ 정상
echo $user->email;    // Warning: Undefined property
echo $user->created_at; // Warning: Undefined property
?>

원인: 클래스 정의에 없는 속성을 접근하려 했다.

 

✓ 올바른 코드 (1) 속성 선언

<?php
class User {
    public $id;
    public $name;
    public $email;        // 추가
    public $created_at;   // 추가
    
    public function __construct($id, $name, $email = null, $created_at = null) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->created_at = $created_at;
    }
}

$user = new User(1, 'John', 'john@example.com', date('Y-m-d H:i:s'));
echo $user->email;    // ✓
echo $user->created_at; // ✓
?>

 

✓ 올바른 코드 (2) Magic Method 활용

<?php
class User {
    private $data = [];
    
    public function __get($name) {
        return $this->data[$name] ?? null; // 없으면 null 반환
    }
    
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }
}

$user = new User();
$user->id = 1;
$user->name = 'John';
$user->email = 'john@example.com';

echo $user->email;     // ✓
echo $user->phone ?? '없음'; // 경고 안 남, null 반환됨
?>

 

해결 방법 선택 가이드
상황 해결 방법 예제
단순 속성 체크 isset() 또는 property_exists() isset($obj->prop) ? $obj->prop : ''
null 값 허용 Null Coalescing (??) $obj->prop ?? 'default'
중첩 객체 접근 property_exists() 체이닝 property_exists($obj, 'user') && property_exists($obj->user, 'name')
API/JSON 응답 null Coalescing + json_decode 연계 json_decode($json, false)->field ?? 'default'
동적 속성 많음 Magic Method (__get/__set) $obj->prop = 'value'; echo $obj->unknown ?? null;

 

주의사항과 함정

 

isset() vs property_exists()의 차이

isset()은 속성이 NULL이면 false를 반환하지만 property_exists()는 속성 존재 여부만 확인한다.

<?php
class Test {
    public $prop = null;
}

$obj = new Test();

var_dump(isset($obj->prop));           // false (NULL이므로)
var_dump(property_exists($obj, 'prop')); // true (속성 존재)

// 속성이 NULL일 수 있으면 property_exists() 사용
?>

 

JSON 디코딩 후 배열 vs 객체

json_decode()의 두 번째 인자가 true면 연관배열로 디코딩되어 속성 접근 문법이 달라진다.

<?php
$json = '{"name": "John", "email": "john@example.com"}';

// 객체로 디코딩
$obj = json_decode($json); // 기본값
echo $obj->name; // ✓

// 배열로 디코딩
$arr = json_decode($json, true);
echo $arr['name']; // ✓
echo $arr->name;   // 작동하지 않음
?>

 

개발 환경에서 경고를 모두 표시하되 자동으로 처리하기

지금까지는 각각의 접근 지점을 안전하게 만드는 방법을 봤다. 여기서는 개발 단계에서 모든 경고를 보되 프로덕션에서는 로그만 남기는 설정을 소개한다.

<?php
// php.ini 또는 runtime 설정
if (getenv('ENVIRONMENT') === 'development') {
    error_reporting(E_ALL);
    ini_set('display_errors', '1');
} else {
    error_reporting(E_ALL);
    ini_set('display_errors', '0');
    ini_set('log_errors', '1');
    ini_set('error_log', '/var/log/php-errors.log');
}
?>

이렇게 하면 개발 중에는 경고를 눈으로 확인할 수 있고 프로덕션에서는 로그에만 기록되어 사용자에게 영향을 주지 않는다.

 

정리: Undefined property는 습관이다

Undefined property 경고는 작은 실수처럼 보이지만 방치하면 로그가 무거워지고 디버깅이 어려워진다. isset()이나 property_exists() 체크, 혹은 Null Coalescing 연산자를 습관처럼 쓰는 것만으로도 대부분의 문제를 방지할 수 있다. 특히 API 응답이나 데이터베이스 쿼리를 다룰 때는 항상 필드/속성이 존재하는지 먼저 확인하는 방어 기제를 갖춰두자. 이 글의 해결 방법 섹션을 참고해 현재 프로젝트의 위험한 객체 접근 지점을 정리하면, 훨씬 안정적인 PHP 애플리케이션을 만들 수 있을 것이다.