PHP 클래스: 기초

PHP 클래스: 기초 (The Basics)

원문: PHP 공식 매뉴얼 — Language Reference > Classes and Objects > The Basics 이 문서는 PHP 공식 매뉴얼 language.oop5.basic 페이지를 한국어로 옮긴 내용이에요.

PHP로 객체지향 코드를 짜기 시작하면 가장 먼저 만나는 게 클래스예요. 클래스는 데이터와 그 데이터를 다루는 동작을 한 덩어리로 묶는 틀인데, 이 페이지에서는 클래스를 정의하고 객체를 만드는 가장 기본적인 문법을 차근차근 살펴볼게요.

class

기본적인 클래스 정의는 class 키워드로 시작해요. 그 뒤에 클래스 이름이 오고, 중괄호 한 쌍이 그 클래스에 속한 속성(property)과 메서드(method)의 정의를 감싸줘요.

클래스 이름은 PHP의 예약어만 아니라면 유효한 라벨이면 뭐든 쓸 수 있어요. 다만 PHP 8.4.0부터 밑줄 하나짜리 _를 클래스 이름으로 쓰는 건 deprecated 처리됐고요. 유효한 클래스 이름은 문자나 밑줄로 시작해서, 그 뒤에 문자·숫자·밑줄이 몇 개든 이어질 수 있어요. 정규식으로 나타내면 이렇게 돼요.

^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$

클래스는 자기만의 상수, 변수(속성이라고 불러요), 함수(메서드라고 불러요)를 가질 수 있어요.

예제 #1 — 간단한 클래스 정의

<?php
class SimpleClass
{
    // property declaration
    public $var = 'a default value';

    // method declaration
    public function displayVar() {
        echo $this->var;
    }
}
?>

객체 컨텍스트 안에서 메서드를 호출하면 의사 변수(pseudo-variable) $this를 쓸 수 있어요. $this는 바로 그 호출을 하고 있는 객체 자신을 가리켜요.

⚠️ 경고

비정적(non-static) 메서드를 정적으로 호출하면 Error가 던져져요. PHP 8.0.0 이전에는 이 상황에서 deprecation 알림이 나오고 $this는 undefined가 됐었어요.

예제 #2 — $this 의사 변수의 여러 쓰임

<?php
class A
{
    function foo()
    {
        if (isset($this)) {
            echo '$this is defined (';
            echo get_class($this);
            echo ")\n";
        } else {
            echo "\$this is not defined.\n";
        }
    }
}

class B
{
    function bar()
    {
        A::foo();
    }
}

$a = new A();
$a->foo();

A::foo();

$b = new B();
$b->bar();

B::bar();
?>

위 예제의 PHP 7 기준 출력:


$this is defined (A)

Deprecated: Non-static method A::foo() should not be called statically in %s  on line 27
$this is not defined.

Deprecated: Non-static method A::foo() should not be called statically in %s  on line 20
$this is not defined.

Deprecated: Non-static method B::bar() should not be called statically in %s  on line 32

Deprecated: Non-static method A::foo() should not be called statically in %s  on line 20
$this is not defined.

위 예제의 PHP 8 기준 출력:


$this is defined (A)

Fatal error: Uncaught Error: Non-static method A::foo() cannot be called statically in %s :27
Stack trace:
#0 {main}
  thrown in %s  on line 27

읽기 전용 클래스 (Readonly classes)

PHP 8.2.0부터 클래스에 readonly 수정자를 붙일 수 있어요. 클래스를 readonly로 표시하면 선언된 모든 속성에 readonly 수정자가 붙고, 동적(dynamic) 속성은 만들 수 없게 돼요. 게다가 AllowDynamicProperties 속성을 써서 동적 속성을 지원하는 것도 불가능하고, 그렇게 시도하면 컴파일 타임 에러가 나요.

<?php
#[\AllowDynamicProperties]
readonly class Foo {
}

// Fatal error: Cannot apply #[AllowDynamicProperties] to readonly class Foo
?>

readonly 수정자는 타입 없는 속성이나 정적(static) 속성에는 붙일 수 없으니까, readonly 클래스도 그런 속성을 선언할 수 없어요.

<?php
readonly class Foo
{
    public $bar;
}

// Fatal error: Readonly property Foo::$bar must have type
?>
<?php
readonly class Foo
{
    public static int $bar;
}

// Fatal error: Readonly class Foo cannot declare static properties
?>

readonly 클래스는 자식 클래스도 역시 readonly 클래스일 때만 상속할 수 있어요.

new

클래스의 인스턴스를 만들려면 new 키워드를 써야 해요. 에러가 나면 예외를 던지는 생성자(constructor)가 정의되어 있지 않는 한, 객체는 항상 생성돼요. 클래스는 인스턴스화하기 전에 정의해 두는 게 좋고요(어떤 경우엔 필수예요).

클래스 이름이 담긴 문자열 변수를 new와 함께 쓰면 그 클래스의 새 인스턴스가 생성돼요. 클래스가 네임스페이스 안에 있다면 이때 완전한 이름(fully qualified name)을 써야 해요.

📌 참고

클래스 생성자에 넘길 인자가 없다면, 클래스 이름 뒤의 괄호는 생략해도 돼요.

예제 #3 — 인스턴스 만들기

<?php
class SimpleClass {
}

$instance = new SimpleClass();
var_dump($instance);

// This can also be done with a variable:
$className = 'SimpleClass';
$instance = new $className(); // new SimpleClass()
var_dump($instance);
?>

PHP 8.0.0부터는 new를 임의의 표현식(arbitrary expression)과 함께 쓸 수 있어요. 그 표현식이 문자열을 만들어 내면 더 복잡한 인스턴스화가 가능해지는데, 표현식은 괄호로 감싸줘야 해요.

예제 #4 — 임의의 표현식으로 인스턴스 만들기

여기서는 클래스 이름을 만들어 내는 여러 유효한 표현식 예시를 보여줘요. 함수 호출, 문자열 결합, ::class 상수를 쓰는 경우를 확인할 수 있어요.

<?php

class ClassA extends \stdClass {}
class ClassB extends \stdClass {}
class ClassC extends ClassB {}
class ClassD extends ClassA {}

function getSomeClass(): string
{
    return 'ClassA';
}

var_dump(new (getSomeClass()));
var_dump(new ('Class' . 'B'));
var_dump(new ('Class' . 'C'));
var_dump(new (ClassD::class));
?>

위 예제의 PHP 8 기준 출력:


object(ClassA)#1 (0) {
}
object(ClassB)#1 (0) {
}
object(ClassC)#1 (0) {
}
object(ClassD)#1 (0) {
}

클래스 컨텍스트 안에서는 new selfnew parent로도 새 객체를 만들 수 있어요.

이미 만든 인스턴스를 새 변수에 할당하면, 그 새 변수는 할당된 그 객체와 같은 인스턴스에 접근해요. 이 동작은 인스턴스를 함수에 넘길 때도 똑같아요. 이미 만들어진 객체의 복사본은 복제(clone)해서 만들 수 있고요.

예제 #5 — 객체 할당

<?php
class SimpleClass {
    public string $var;
}

$instance = new SimpleClass();

$assigned   =  $instance;
$reference  =& $instance;

$instance->var = '$assigned will have this value';

$instance = null; // $instance and $reference become null

var_dump($instance);
var_dump($reference);
var_dump($assigned);
?>

위 예제의 출력은 이렇게 돼요.


NULL
NULL
object(SimpleClass)#1 (1) {
  ["var"]=>
  string(30) "$assigned will have this value"
}

객체의 인스턴스를 만드는 방법은 몇 가지 더 있어요.

예제 #6 — 새 객체 만들기

<?php

class Test
{
    public static function getNew()
    {
        return new static();
    }
}

class Child extends Test {}

$obj1 = new Test(); // By the class name
$obj2 = new $obj1(); // Through the variable containing an object
var_dump($obj1 !== $obj2);

$obj3 = Test::getNew(); // By the class method
var_dump($obj3 instanceof Test);

$obj4 = Child::getNew(); // Through a child class method
var_dump($obj4 instanceof Child);

?>

위 예제의 출력은 이렇게 돼요.


bool(true)
bool(true)
bool(true)

새로 만든 객체의 멤버에 접근하는 것도 하나의 표현식으로 가능해요.

예제 #7 — 새로 만든 객체의 멤버 접근

<?php
echo (new DateTime())->format('Y'), PHP_EOL;

// surrounding parentheses are optional as of PHP 8.4.0
echo new DateTime()->format('Y'), PHP_EOL;
?>

위 예제의 출력은 이와 비슷한 형태예요.


2025
2025

📌 참고

PHP 7.1 이전에는 생성자 함수가 정의되어 있지 않으면 인자가 평가되지 않았어요.

속성과 메서드 (Properties and methods)

클래스의 속성과 메서드는 서로 다른 "네임스페이스"에 살아요. 그래서 같은 이름의 속성과 메서드를 둘 다 가질 수 있어요. 속성을 가리키는 표기와 메서드를 호출하는 표기가 같은데, 속성이 접근될지 메서드가 호출될지는 오로지 맥락에 달려 있어요. 즉 그 사용이 변수 접근인지 함수 호출인지에 따라 달라진다는 뜻이에요.

예제 #8 — 속성 접근과 메서드 호출

<?php
class Foo
{
    public $bar = 'property';
    
    public function bar() {
        return 'method';
    }
}

$obj = new Foo();
echo $obj->bar, PHP_EOL, $obj->bar(), PHP_EOL;

위 예제의 출력은 이렇게 돼요.


property
method

이 말은 즉, 속성에 할당해 둔 익명 함수를 직접 호출하는 것은 불가능하다는 뜻이에요. 대신 그 속성을 일단 변수에 할당해야 해요. 아니면 괄호로 감싸서 그런 속성을 직접 호출할 수도 있고요.

예제 #9 — 속성에 저장된 익명 함수 호출

<?php
class Foo
{
    public $bar;
    
    public function __construct() {
        $this->bar = function() {
            return 42;
        };
    }
}

$obj = new Foo();

echo ($obj->bar)(), PHP_EOL;

위 예제의 출력은 이렇게 돼요.


42

extends

클래스는 클래스 선언에서 extends 키워드를 쓰면 다른 클래스의 상수·메서드·속성을 상속받을 수 있어요. 여러 클래스를 한 번에 상속하는 건 불가능하고, 클래스는 오직 하나의 기반 클래스에서만 상속받을 수 있어요.

상속받은 상수·메서드·속성은 부모 클래스에 정의된 것과 같은 이름으로 다시 선언하면 오버라이드할 수 있어요. 다만 부모 클래스가 메서드나 상수를 final로 정의했다면 오버라이드할 수 없고요. 오버라이드된 메서드나 정적 속성은 parent::로 참조해서 접근할 수 있어요.

📌 참고

PHP 8.1.0부터 상수도 final로 선언할 수 있어요.

예제 #10 — 간단한 클래스 상속

<?php
class SimpleClass
{
    function displayVar()
    {
        echo "Parent class\n";
    }
}

class ExtendClass extends SimpleClass
{
    // Redefine the parent method
    function displayVar()
    {
        echo "Extending class\n";
        parent::displayVar();
    }
}

$extended = new ExtendClass();
$extended->displayVar();
?>

위 예제의 출력은 이렇게 돼요.


Extending class
Parent class

시그니처 호환 규칙 (Signature compatibility rules)

메서드를 오버라이드할 때 그 시그니처는 부모 메서드와 호환되어야 해요. 그렇지 않으면 치명적(fatal) 에러가 나거나, PHP 8.0.0 이전에는 E_WARNING 수준의 에러가 발생했어요. 시그니처가 호환된다는 건 변량(variance) 규칙을 지키고, 필수 매개변수를 선택적으로 바꾸고, 선택적인 새 매개변수만 추가하며, 접근성을 좁히지 않고 완화만 하는 경우를 말해요. 이걸 리스코프 치환 원칙(Liskov Substitution Principle, 줄여서 LSP)이라고 불러요. 생성자와 private 메서드는 이 시그니처 호환 규칙에서 제외되어서, 시그니처가 어긋나도 치명적 에러가 나지 않아요.

예제 #11 — 호환되는 자식 메서드

<?php

class Base
{
    public function foo(int $a) {
        echo "Valid\n";
    }
}

class Extend1 extends Base
{
    function foo(int $a = 5)
    {
        parent::foo($a);
    }
}

class Extend2 extends Base
{
    function foo(int $a, $b = 5)
    {
        parent::foo($a);
    }
}

$extended1 = new Extend1();
$extended1->foo();
$extended2 = new Extend2();
$extended2->foo(1);

위 예제의 출력은 이렇게 돼요.


Valid
Valid

아래 예시들은 자식 메서드가 매개변수를 제거하거나, 선택적 매개변수를 필수로 바꾸면 부모 메서드와 호환되지 않는다는 걸 보여줘요.

예제 #12 — 자식 메서드가 매개변수를 제거할 때의 치명적 에러

<?php

class Base
{
    public function foo(int $a = 5) {
        echo "Valid\n";
    }
}

class Extend extends Base
{
    function foo()
    {
        parent::foo(1);
    }
}

위 예제의 PHP 8 기준 출력은 이와 비슷해요.


Fatal error: Declaration of Extend::foo() must be compatible with Base::foo(int $a = 5) in /in/evtlq on line 13

예제 #13 — 자식 메서드가 선택적 매개변수를 필수로 바꿀 때의 치명적 에러

<?php

class Base
{
    public function foo(int $a = 5) {
        echo "Valid\n";
    }
}

class Extend extends Base
{
    function foo(int $a)
    {
        parent::foo($a);
    }
}

위 예제의 PHP 8 기준 출력은 이와 비슷해요.


Fatal error: Declaration of Extend::foo(int $a) must be compatible with Base::foo(int $a = 5) in /in/qJXVC on line 13

⚠️ 경고

자식 클래스에서 메서드 매개변수의 이름을 바꾸는 건 시그니처 불일치는 아니에요. 다만 그렇게 하면 명명된 인자(named arguments)를 쓸 때 런타임 Error가 나기 때문에 권장하지 않아요.

예제 #14 — 명명된 인자를 쓸 때 자식 클래스에서 매개변수 이름을 바꾼 경우의 에러

<?php

class A {
    public function test($foo, $bar) {}
}

class B extends A {
    public function test($a, $b) {}
}

$obj = new B;

// Pass parameters according to A::test() contract
$obj->test(foo: "foo", bar: "bar"); // ERROR!

위 예제의 출력은 이와 비슷해요.


Fatal error: Uncaught Error: Unknown named parameter $foo in /in/XaaeN:14
Stack trace:
#0 {main}
  thrown in /in/XaaeN on line 14

::class

class 키워드는 클래스 이름을 해석할 때도 쓰여요. 클래스 ClassName의 완전한 이름을 얻으려면 ClassName::class를 쓰면 되는데, 특히 네임스페이스를 쓰는 클래스에서 유용해요.

예제 #15 — 클래스 이름 해석

<?php
namespace NS {
    class ClassName {
    }
    
    echo ClassName::class;
}
?>

위 예제의 출력은 이렇게 돼요.


NS\ClassName

📌 참고

::class를 쓰는 클래스 이름 해석은 컴파일 타임 변환이에요. 즉 클래스 이름 문자열이 만들어지는 시점에는 아직 오토로딩이 일어나지 않았어요. 그래서 클래스가 존재하지 않아도 클래스 이름은 확장돼요. 그 경우에도 에러는 발생하지 않아요.

예제 #16 — 존재하지 않는 클래스 이름 해석

<?php
print Does\Not\Exist::class;
?>

위 예제의 출력은 이렇게 돼요.


Does\Not\Exist

PHP 8.0.0부터는 ::class를 객체에도 쓸 수 있어요. 이 해석은 컴파일 타임이 아니라 런타임에 일어나고, 효과는 객체에 get_class()를 호출한 것과 같아요.

예제 #17 — 객체 이름 해석

<?php
namespace NS {
    class ClassName {
    }

    $c = new ClassName();
    print $c::class;
}
?>

위 예제의 출력은 이렇게 돼요.


NS\ClassName

널세이프 메서드와 속성 (Nullsafe methods and properties)

PHP 8.0.0부터 대신 ?-> 라는 "nullsafe" 연산자로 속성과 메서드에 접근할 수도 있어요. nullsafe 연산자는 위에서 본 속성·메서드 접근과 똑같이 동작하되, 역참조하는 객체가 null이면 예외를 던지는 대신 null을 돌려줘요. 역참조가 체인의 일부라면 체인의 나머지 부분은 건너뛰어요.

이 효과는 각 접근을 is_null() 검사로 감싼 것과 비슷하지만 훨씬 간결해요.

예제 #18 — 널세이프 연산자

<?php

// As of PHP 8.0.0, this line:
$result = $repository?->getUser(5)?->name;

// Is equivalent to the following code block:
if (is_null($repository)) {
    $result = null;
} else {
    $user = $repository->getUser(5);
    if (is_null($user)) {
        $result = null;
    } else {
        $result = $user->name;
    }
}
?>

📌 참고

nullsafe 연산자는 null이 속성이나 메서드 반환값의 유효하고 예상 가능한 값으로 여겨질 때 가장 잘 어울려요. 에러를 나타내는 용도로는 예외를 던지는 쪽이 더 좋아요.