PHP 개발 중 갑자기 나타나는 'Call to undefined method' 에러. 메서드를 분명히 정의했는데 왜 찾을 수 없다고 할까? 대부분의 개발자들은 이 에러가 단순한 오타나 함수 정의 누락 탓이라고 생각하고, 인터넷에서 찾은 코드를 그냥 복사해 붙인다. 하지만 실제 원인은 클래스 상속 구조, 인터페이스 구현, 네임스페이스, 트레이트 적용 과정에서 발생하는 미묘한 실수에 있다. 이번엔 이 에러의 정확한 원인이 뭔지, 왜 발생하는지, 어떻게 해결하는지 완벽하게 정리해서 소개하겠다.

 

PHP Fatal error: Call to undefined method란?

이 에러는 당신이 호출한 메서드가 클래스나 객체에 존재하지 않을 때 발생한다. 문제는 메서드가 정말 없는 경우도 있고, 있는데도 못 찾는 경우도 있다는 점이다.

에러 메시지는 보통 이렇게 나타난다.

PHP Fatal error: Call to undefined method ClassName::methodName() in /path/to/file.php on line 42

한 줄로는 보이지만 원인은 여러 가지다. 상속받지 않은 클래스의 메서드를 호출했거나, 인터페이스를 구현하지 않았거나, 정적 메서드와 인스턴스 메서드를 혼동했을 수 있다. 심지어 올바르게 정의된 메서드를 호출하는데도 namespace나 use 선언이 잘못되면 에러가 난다.

 

원인 1: 상속받지 않은 부모 클래스의 메서드 호출

자식 클래스가 부모 클래스를 extends하지 않았는데 부모의 메서드를 호출하려고 할 때 이 에러가 난다.

✗ 잘못된 코드

<?php
class Parent {
    public function greet() {
        return "Hello from Parent";
    }
}

class Child {
    // extends Parent를 빼먹음
}

$child = new Child();
echo $child->greet(); // Fatal error: Call to undefined method Child::greet()
?>

✓ 올바른 코드

<?php
class Parent {
    public function greet() {
        return "Hello from Parent";
    }
}

class Child extends Parent {
    // 부모 클래스 상속
}

$child = new Child();
echo $child->greet(); // "Hello from Parent" 출력
?>

상속 관계를 확인하는 가장 간단한 방법은 PHP의 class_parents() 함수를 사용하는 것이다.

<?php
$class = 'Child';
if (in_array('Parent', class_parents($class))) {
    echo "Child는 Parent를 상속받았습니다.";
} else {
    echo "상속 관계가 없습니다.";
}
?>

 

원인 2: 인터페이스 구현 누락

인터페이스에 정의된 메서드를 구현하지 않고 호출할 때 발생한다.

✗ 잘못된 코드

<?php
interface PaymentInterface {
    public function pay($amount);
    public function refund($amount);
}

class CreditCardPayment implements PaymentInterface {
    public function pay($amount) {
        return "Paid $amount";
    }
    // refund() 메서드 구현을 빼먹음
}

$payment = new CreditCardPayment();
echo $payment->refund(100); // Fatal error: Call to undefined method
?>

✓ 올바른 코드

<?php
interface PaymentInterface {
    public function pay($amount);
    public function refund($amount);
}

class CreditCardPayment implements PaymentInterface {
    public function pay($amount) {
        return "Paid $amount";
    }
    
    public function refund($amount) {
        return "Refunded $amount";
    }
}

$payment = new CreditCardPayment();
echo $payment->refund(100); // "Refunded 100" 출력
?>

인터페이스의 모든 메서드를 구현하지 않으면 클래스를 인스턴스화할 때 이미 에러가 난다. 이를 미리 확인하려면 class_implements() 함수를 사용하자.

<?php
$interfaces = class_implements('CreditCardPayment');
if (isset($interfaces['PaymentInterface'])) {
    echo "PaymentInterface를 구현했습니다.";
}
?>

 

원인 3: 정적(static) 메서드와 인스턴스 메서드 혼동

정적 메서드(클래스에 속함)를 인스턴스로 호출하거나, 인스턴스 메서드를 정적으로 호출할 때 발생한다.

✗ 잘못된 코드

<?php
class Database {
    public static function connect() {
        return "Connected to DB";
    }
    
    public function query($sql) {
        return "Executing: " . $sql;
    }
}

$db = new Database();
// 인스턴스로 정적 메서드 호출 (작동하긴 하지만 권장 안 함)
echo $db->connect();

// 정적으로 인스턴스 메서드 호출 (에러)
echo Database::query("SELECT * FROM users"); // Fatal error
?>

✓ 올바른 코드

<?php
class Database {
    public static function connect() {
        return "Connected to DB";
    }
    
    public function query($sql) {
        return "Executing: " . $sql;
    }
}

// 정적 메서드는 클래스명으로 호출
echo Database::connect(); // "Connected to DB"

// 인스턴스 메서드는 객체로 호출
$db = new Database();
echo $db->query("SELECT * FROM users");
?>

메서드 호출 규칙을 정리하면: 정적 메서드는 ClassName::methodName(), 인스턴스 메서드는 $object->methodName()이다.

 

원인 4: 트레이트(Trait) 관련 메서드 호출 오류

트레이트에 정의된 메서드를 사용하려고 했는데 use 선언을 빼먹었을 때 발생한다.

✗ 잘못된 코드

<?php
trait LoggerTrait {
    public function log($message) {
        return "[LOG] " . $message;
    }
}

class User {
    // use LoggerTrait;를 빼먹음
}

$user = new User();
echo $user->log("User created"); // Fatal error: Call to undefined method
?>

✓ 올바른 코드

<?php
trait LoggerTrait {
    public function log($message) {
        return "[LOG] " . $message;
    }
}

class User {
    use LoggerTrait;
}

$user = new User();
echo $user->log("User created"); // "[LOG] User created"
?>

트레이트를 사용하는 클래스의 메서드들을 확인하려면 get_class_methods() 함수를 써서 실제로 사용 가능한 메서드 목록을 출력할 수 있다.

<?php
$methods = get_class_methods('User');
if (in_array('log', $methods)) {
    echo "log() 메서드가 존재합니다.";
} else {
    echo "log() 메서드가 없습니다.";
}
?>

 

원인 5: 네임스페이스와 use 선언 오류

다른 네임스페이스의 클래스를 사용할 때 use 선언이 잘못되거나 생략되면 메서드를 찾을 수 없다.

✗ 잘못된 코드

<?php
namespace App\Services;

class UserService {
    public function getUser($id) {
        return "User " . $id;
    }
}

// 다른 파일에서
namespace App\Controllers;

$user = new UserService(); // UserService를 찾을 수 없음
?>

✓ 올바른 코드

<?php
namespace App\Controllers;

use App\Services\UserService;

$user = new UserService();
echo $user->getUser(1); // "User 1"

// 또는 전체 경로를 명시
$user2 = new \App\Services\UserService();
echo $user2->getUser(2);
?>

네임스페이스가 복잡할 때는 get_class() 함수로 현재 객체의 완전한 클래스명을 확인해보자.

<?php
$user = new UserService();
echo get_class($user); // "App\Services\UserService" 또는 "App\Controllers\UserService"
?>

 

원인 6: 메서드의 접근 제어자(Visibility) 문제

private 또는 protected 메서드를 클래스 외부에서 호출하려고 할 때도 비슷한 에러가 발생할 수 있다.

✗ 잘못된 코드

<?php
class BankAccount {
    private function validatePin($pin) {
        return $pin === "1234";
    }
    
    public function withdraw($amount, $pin) {
        if ($this->validatePin($pin)) {
            return "Withdrew " . $amount;
        }
        return "Invalid PIN";
    }
}

$account = new BankAccount();
echo $account->validatePin("1234"); // Fatal error: Call to private method
?>

✓ 올바른 코드

<?php
class BankAccount {
    private function validatePin($pin) {
        return $pin === "1234";
    }
    
    public function withdraw($amount, $pin) {
        if ($this->validatePin($pin)) {
            return "Withdrew " . $amount;
        }
        return "Invalid PIN";
    }
}

$account = new BankAccount();
// 공개 메서드를 통해서만 접근
echo $account->withdraw(100, "1234");
?>

메서드의 접근 제어를 확인하려면 ReflectionMethod를 사용하자.

<?php
$reflect = new ReflectionMethod('BankAccount', 'validatePin');
if ($reflect->isPrivate()) {
    echo "이 메서드는 private입니다. 외부에서 호출할 수 없습니다.";
}
?>

 

실전 디버깅 팁: 메서드 존재 여부 체계적으로 확인하기

이 에러를 빠르게 진단하려면 다음 코드를 참고하자.

<?php
function debugMethodCall($object, $methodName) {
    $className = get_class($object);
    
    // 1. 메서드가 존재하는가?
    if (!method_exists($object, $methodName)) {
        echo "Error: {$className}에 {$methodName}() 메서드가 없습니다.\n";
        echo "사용 가능한 메서드:\n";
        print_r(get_class_methods($object));
        return false;
    }
    
    // 2. 메서드의 접근 제어 확인
    $reflect = new ReflectionMethod($className, $methodName);
    if ($reflect->isPrivate()) {
        echo "Error: {$methodName}()은 private 메서드입니다.\n";
        return false;
    } elseif ($reflect->isProtected()) {
        echo "Error: {$methodName}()은 protected 메서드입니다.\n";
        return false;
    }
    
    // 3. 정적/인스턴스 메서드 타입 확인
    if ($reflect->isStatic()) {
        echo "Info: {$methodName}()은 정적 메서드입니다. {$className}::{$methodName}()로 호출하세요.\n";
        return false;
    }
    
    echo "OK: {$methodName}()을 호출할 수 있습니다.\n";
    return true;
}

// 사용 예시
class Demo {
    public function publicMethod() {}
    private function privateMethod() {}
    public static function staticMethod() {}
}

$demo = new Demo();
debuguMethodCall($demo, 'publicMethod');   // OK
debuguMethodCall($demo, 'privateMethod');  // Error: private
debuguMethodCall($demo, 'staticMethod');   // Info: static
debuguMethodCall($demo, 'unknownMethod');  // Error: not found
?>

이 디버깅 함수를 프로젝트에 추가하면 메서드 호출 문제를 훨씬 빠르게 파악할 수 있다.

 

핵심 정리와 다음 단계

'Call to undefined method' 에러는 단순한 오타가 아니라 클래스 설계, 상속 구조, 메서드 접근 제어 등 여러 계층의 문제를 나타낸다. 이 글에서 배운 여섯 가지 원인을 체크리스트로 삼아 에러를 만났을 때 순차적으로 확인해보자. 상속 관계 → 인터페이스 구현 → 정적/인스턴스 메서드 타입 → 트레이트 use 선언 → 네임스페이스와 use 문 → 접근 제어자 순서로 진단하면 대부분의 경우를 해결할 수 있을 것이다. 디버깅 함수를 프로젝트에 추가해두면, 앞으로 비슷한 문제가 생겼을 때 원인을 단 몇 초 만에 파악할 수 있을 것이다.