접근 제어자는 클래스 내부 멤버(프로퍼티·메소드·상수)가 어디에서 접근 가능한지를 결정합니다. 주로 세 가지가 있습니다.
public : 어디서든 접근 가능 (클래스 내부, 자식, 외부 코드)
protected : 클래스 자신과 자식 클래스에서만 접근 가능
private : 해당 클래스 내부에서만 접근 가능 (자식도 접근 불가)
(참고) final, abstract, static, readonly 등은 접근 제어자와 별개입니다. readonly는 가시성은 아니고 쓰기 금지 속성입니다(PHP 8.1+).
프로퍼티(property) : 인스턴스 변수(또는 static) 선언 시 가시성 사용
메소드(method) : 인스턴스/정적 메소드 가시성
클래스 상수(const) : PHP 7.1부터 가시성(public/protected/private) 선언 가능
<?php
class A {
public int $pub = 1;
protected int $prot = 2;
private int $priv = 3;
public function publicMethod() { return $this->priv; } // OK
protected function protectedMethod() { return $this->prot; } // 내부/자식용
private function privateMethod() { return $this->priv; } // 오직 이 클래스 내부
}
$a = new A();
echo $a->pub; // OK
// echo $a->prot; // 오류: protected 접근 불가
// echo $a->priv; // 오류: private 접근 불가
오버라이드 시 접근성: 부모 메소드보다 더 제한적(더 숨김) 으로 바꿀 수 없습니다. 즉,
부모가 public이면 자식은 public만 가능(더 좁힐 수 없음).
부모가 protected이면 자식은 protected 또는 public으로 바꿀 수 있습니다(더 넓힐 수는 있음).
이유: Liskov Substitution Principle(리스코프 치환 원칙) — 서브타입은 슈퍼타입으로 대체 가능해야 합니다.
예:
class Base {
public function foo() {}
}
class ChildBad extends Base {
// public -> protected 로 변경하면 fatal error: access level must be public
// protected function foo() {} // 오류
}
private 프로퍼티/메소드는 정의된 클래스 내부에서만 접근 가능. 같은 명칭의 private 멤버를 부모/자식 클래스가 각각 선언하면 서로 다른 멤버로 취급됩니다(완전 분리).
때문에 부모 클래스의 private 메소드나 프로퍼티는 자식에서 덮어써도 부모의 것을 가리키지 않습니다.
public static, protected static, private static 동일하게 작동.
접근: ClassName::$prop 또는 self::$prop, static::$prop 등으로 접근.
self::은 정의된 클래스의 멤버를 가리키고, static::(늦은 정적 바인딩)은 호출한 클래스를 가리킵니다. 가시성은 동일 규칙 적용.
class X {
public const PUBLIC_C = 'pub';
protected const PROT_C = 'prot';
private const PRIV_C = 'priv';
}
public 상수는 클래스 외부에서 X::PUBLIC_C로 접근 가능.
protected/private 상수는 그 가시성 규칙대로 제한됩니다.
인터페이스: 메소드는 항상 public입니다. (인터페이스에 protected/private 메소드는 선언할 수 없습니다.)
트레이트: 트레이트에 선언된 멤버도 가시성을 가질 수 있으며, 클래스로 use할 때 insteadof, as로 충돌 해결 및 가시성 변경(별칭) 가능:
trait T {
protected function tMethod() {}
}
class C {
use T {
tMethod as public; // 가시성 변경 예
}
}
생성자를 private로 선언하면 외부에서 new로 만들 수 없고 정적 팩토리를 통해 인스턴스 생성 제어 가능:
class OnlyFactory {
private function __construct() {}
public static function create(): self { return new self(); }
}
Closure::bind 또는 $closure->call($obj)를 사용하면 클로저의 실행 스코프를 바꿔 private/protected 멤버에 접근할 수 있도록 할 수 있습니다(강력하지만 남용 위험).
예: Closure::bind(function(){ return $this->priv; }, $obj, $obj::class) — 객체의 private에 접근 가능.
선언되지 않은(또는 접근 불가) 프로퍼티 접근 시 __get/__set이 호출되므로 외부에서 private 멤버 접근을 흉내낼 수 있으나 이는 캡슐화 규칙을 우회하는 것이므로 신중히 사용.
public 프로퍼티를 남발하면 캡슐화·불변성·검증 책임이 깨집니다. 가능한 private + 게터/메소드 사용 권장.
부모의 private 멤버를 자식에서 동일 이름으로 사용하면 “덮어쓴다”기보다 서로 다른 멤버가 됩니다 — 혼동 주의.
생성자에서 오버라이드 가능한 메소드 호출(즉, 자식에서 재정의된 메소드 호출)은 위험. 자식 필드 초기화가 끝나기 전에 자식 메소드가 호출되어 부분 상태를 다룰 수 있음.
protected는 테스트/확장을 위해 유용하지만 남용 시 구현 세부 사항 노출(캡슐화 파손)로 이어질 수 있음. 가능한 private + 공용 API 권장.
접근 오류(예: Cannot access protected property)가 나오면 호출 위치를 먼저 확인 — 클래스 외부인지, 자식인지, 같은 클래스인지 확인.
리플렉션(Reflection)으로 접근 가능성 확인 가능(단, Reflection API 사용은 고급 디버깅/툴 용도).
프로퍼티는 기본 private. 필요한 경우에만 protected/public 사용.
외부 접근은 명확한 게터/메소드로 제공(검증/불변성 유지).
상속 API(자식이 사용할 것)는 protected로 설계하고 명확히 문서화.
불변 값은 readonly(PHP 8.1+)로 표현.
팩토리/DI를 활용하여 생성자 가시성(예: private 생성자 + static factory)로 인스턴스화 제어.
<?php
class ParentClass {
private function secret() { return "parent secret"; }
protected function doProtected() { return "ok"; }
public function publicAPI() { return $this->doProtected(); }
}
class ChildClass extends ParentClass {
// secret()는 부모의 private라서 여기서 다시 정의해도 부모 것과 분리된다
private function secret() { return "child secret"; }
// 부모 protected 메소드를 공개로 확장(허용)
public function doProtected() { return "child override"; }
public function show() {
// 부모의 private secret() 접근 불가: $this->secret() 호출 시 ChildClass의 secret() 호출
return $this->secret();
}
}
$c = new ChildClass();
echo $c->publicAPI(); // "child override"
echo $c->show(); // "child secret"
public / protected / private는 클래스 설계의 핵심 도구입니다.
원칙: 멤버는 가능한 한 더 좁은 범위(private) 로 만들고, 외부와의 계약은 메소드(혹은 readonly 게터)로 노출하세요.
상속·트레이트·팩토리 패턴과 결합해 올바른 캡슐화와 확장성을 확보하는 것이 중요합니다.