static 멤버는 클래스 자체에 속하는 값/함수로, 인스턴스 없이 ClassName::$prop 또는 ClassName::method()로 접근합니다. $this는 정적 메서드 내부에서 사용할 수 없습니다.
class Foo {
public static int $count = 0; // 정적(클래스) 프로퍼티 (타입 선언 가능)
protected static string $name = 'app';
public static function inc(): void {
self::$count++;
}
public static function info(): string {
// $this 불가 (인스턴스 컨텍스트 없음)
return self::$name . '(' . self::$count . ')';
}
}
// 접근
Foo::inc();
echo Foo::$count; // 1
echo Foo::info();
const(또는 public const)는 변경 불가능한 상수. static은 변경 가능한 클래스 레벨 값.
class C {
public const VERSION = '1.0';
public static int $counter = 0;
}
타입이 붙은 정적 프로퍼티(public static int $x;)는 초기화되지 않은 상태가 될 수 있고, 초기화 전에 읽으면 런타임 오류가 납니다. 생성자 또는 초기화 루틴에서 값을 설정하세요.
인스턴스 프로퍼티: 객체마다 별도 값.
정적 프로퍼티: 클래스(또는 선언된 클래스) 단위로 값 보유. 같은 클래스 내의 모든 인스턴스에서 동일한 정적 값을 참조합니다.
실용 예: 요청당 카운터나 전역 설정(읽기 전용)·유틸성 함수에 자주 사용됩니다.
부모 클래스에만 정적 프로퍼티가 선언되어 있다면, 부모가 선언한 “한 번의” 값이 공유되는 동작을 보일 수 있습니다(상황에 따라 공유처럼 보임).
부모와 자식 모두 같은 이름으로 static을 다시 선언하면 각 클래스는 서로 독립적인 정적 저장소를 가집니다(즉, 자식에 재선언하면 자식 클래스용 별도 정적 값이 됨).
예시(동작 차이):
class A {
public static int $x = 0;
}
class B extends A { /* 재선언 없음 */ }
class C extends A { public static int $x = 100; } // C는 별도 선언
A::$x = 1;
echo B::$x; // 1 (A에 선언된 값 참조)
echo C::$x; // 100 (C에서 재선언했으므로 C 전용)
핵심: “공유”처럼 보이는 경우(부모만 선언)와 “클래스별 고유 저장소”(자식에서 재선언) 동작을 구분해서 설계하세요.
self::는 정의된 클래스의 멤버를 가리키고, static::은 호출한(런타임) 클래스를 가리키는 늦은 정적 바인딩(Late Static Binding)입니다. 팩토리/템플릿 패턴에서 유용합니다.
예:
class Base {
public static function factory() { return new self(); } // self -> 항상 Base
public static function lateFactory() { return new static(); } // static -> 호출 클래스
}
class Child extends Base {}
var_dump(get_class(Base::factory())); // "Base"
var_dump(get_class(Child::factory())); // "Base" (self 바인딩)
var_dump(get_class(Child::lateFactory())); // "Child" (late static binding)
function counter() {
static $i = 0; // 함수 스코프에 영속적(해당 함수 내부 고정)
return ++$i;
}
이 static $i(함수 내부)는 그 함수 호출 횟수 등을 기억할 때 유용합니다. 클래스 static 프로퍼티는 클래스 범위의 값입니다. 둘은 개념은 비슷하지만 스코프가 다릅니다.
final class Singleton {
private static ?Singleton $instance = null;
private function __construct() {}
public static function instance(): Singleton {
return self::$instance ??= new self();
}
}
$s = Singleton::instance();
싱글턴은 전역 상태를 만드는 쉬운 방법이지만 테스트·유연성·의존성 관리에 문제를 일으킵니다. DI 컨테이너 사용을 권장합니다.
전역 상태는 최소화: 정적 프로퍼티로 전역 상태를 만들면 테스트와 재현성이 어려워집니다.
테스트성 저하: 모킹이 어렵고, 실행 순서/사이드 이펙트가 테스트에 영향을 줍니다.
장기 실행 프로세스 주의(워커/서버): PHP-FPM/Apache의 경우 프로세스가 재사용될 수 있으므로 정적 값이 요청 간에 남을 수 있습니다(특히 데몬/AMQP 워커 등에서는 주의).
초기화 방식: 정적 값이 복잡한 계산(파일 읽기, DB 조회 등)으로 초기화되어야 하면 “명시적 초기화 메서드”나 지연 초기화(lazy init) 패턴을 써서 예측 가능하게 만드세요.
예: getConfig() 내부에서 if (self::$cfg === null) self::$cfg = loadFromFile(); 처럼.
대안: 순수 유틸은 네임스페이스 함수(또는 final 클래스로 감싼 정적 메서드)로 만들고, 상태가 필요한 로직은 객체(DI)로 분리하세요.
트레이트는 정적 멤버를 가질 수 있습니다. 트레이트를 use한 클래스에 해당 멤버가 포함됩니다. 충돌 해결은 insteadof/as로 해야 합니다.
static 접근은 인스턴스 메서드 호출보다 미세한 차이는 있을 수 있으나, 일반적인 애플리케이션에서는 설계(아키텍처)가 성능보다 훨씬 중요합니다. 미세 최적화 목적만으로 static을 남용하지 마세요.
전역·가변 상태는 static로 만들지 말고 DI로 주입하라.
유틸·순수 함수는 static(혹은 namespaced function) 적절.
상속 환경에서 클래스별 고유 정적 값을 원하면 자식에서 재선언하라.
타입이 있는 static 프로퍼티는 초기화 확인(또는 nullable로 선언)하라.
팩토리용 정적 메서드와 static::(late static binding)을 함께 쓰면 하위 클래스에 맞는 인스턴스 생성에 편리하다.
원하시면 다음 중 한 가지를 바로 만들어 드리겠습니다.
static 기반의 설정 캐시 클래스(lazy init 포함) 예제
싱글턴 대체로 DI 컨테이너와 결합한 구현 예제
상속 상황에서 static 프로퍼티 동작을 보여주는 실습 코드 + 실행 결과