함수(Function): 입력을 받아 출력을 돌려주는 동작(로직) 자체. 보통 상태를 갖지 않으며 호출될 때마다 같은 입력이면 같은 출력(순수 함수)이 이상적입니다.
객체(Object): 상태(프로퍼티) + 행위(메서드) 묶음. 동일한 메서드라도 객체의 현재 상태에 따라 결과가 달라질 수 있습니다.
function add(int $a, int $b): int {
return $a + $b;
}
$sum = add(2, 3);
$mul = function (int $a, int $b): int { return $a * $b; };
$factor = 10;
$times = fn(int $x) => $x * $factor; // 외부 변수 캡처
$len = strlen(...); // Closure 반환
echo $len("hello"); // 5
class Greeter { public function hi(string $n){ return "Hi, $n"; } }
$g = new Greeter();
$call = $g->hi(...);
echo $call("Ada"); // "Hi, Ada"
class Counter {
private int $n = 0;
public function inc(int $step = 1): void {
$this->n += $step;
}
public function value(): int {
return $this->n;
}
}
$c = new Counter();
$c->inc(3);
echo $c->value(); // 3
class Adder {
public function __invoke(int $a, int $b): int { return $a + $b; }
}
$adder = new Adder();
echo $adder(2, 3); // 5 ← 함수처럼 호출
함수: 기본적으로 상태 없음. 다만
클로저는 외부 변수를 캡처하여 “약한 상태”를 가질 수 있습니다.
함수 내부 static 변수로 상태를 유지할 수 있지만 테스트/가독성 측면에서 권장되지 않습니다.
객체: 프로퍼티로 상태를 가지며, 메서드는 그 상태를 읽고/변경합니다.
PHP는 요청-응답형 환경에서는 보통 요청이 끝나면 객체도 소멸합니다(장기 실행 워커는 예외).
함수도 namespace에 둘 수 있으며 use function Vendor\pkg\util; 식으로 가져올 수 있습니다.
클래스/객체는 Composer의 PSR-4 오토로딩과 궁합이 좋습니다. 함수는 보통 composer.json의 autoload.files로 한 번에 로딩(또는 직접 include)합니다.
둘 다 매개변수·반환 타입 선언, 예외 던지기/처리가 가능합니다.
테스트/모킹:
순수 함수는 입력 → 출력 검증이 간단하고 빠릅니다.
외부 의존(예: DB, HTTP)이 있는 로직은 인터페이스를 구현한 객체로 분리하면 DI로 쉽게 모킹/스텁 가능. 함수는 전역 의존을 쓰면 모킹이 까다롭습니다.
함수 호출이 메서드 호출보다 보통 약간 가볍습니다.
OPcache 등으로 차이는 줄어드는 편이고, 대부분의 웹 앱에서는 설계 품질이 성능보다 훨씬 큰 영향을 미칩니다. 미세 최적화는 마지막에.
관점 |
함수 |
객체 |
---|---|---|
상태 |
기본 없음(클로저/정적변수로 예외) |
프로퍼티로 상태 보유 |
추상화 |
동작만 캡슐화 |
상태+행위 동시 캡슐화 |
재사용 |
범용 유틸, 순수 계산에 최적 |
도메인 모델, 규칙/불변식 보존에 최적 |
의존성 |
전달 인자 위주 |
DI 컨테이너/인터페이스/생성자 주입과 궁합 |
테스트 |
순수 함수 매우 쉬움 |
인터페이스 모킹으로 유연 |
오토로딩 |
files/include 필요 |
PSR-4로 자동 로딩 용이 |
성능 |
보통 더 가벼움 |
약간의 오버헤드(보통 무시 가능) |
함수 추천
수학적 계산, 변환, 파싱 등 입력→출력이 명확한 순수 로직
전역 상태/부작용이 없는 작은 유틸리티
고차 함수 스타일(콜백/매핑/필터링) 작성
객체 추천
상태를 안전하게 관리해야 할 때(예: 누산기, 세션 스코프 도메인 모델)
도메인 규칙/불변식을 타입으로 강제하고 싶을 때(값 객체, 엔티티)
다형성/확장성(전략, 템플릿 메서드, 플러그인 구조)이 중요한 경우
외부 자원(DB/HTTP/캐시) 의존을 인터페이스로 분리하고 테스트/교체 용이성 확보
/** @param callable(int):int $rule */
function applyDiscount(int $price, callable $rule): int {
return max(0, $rule($price));
}
$percent10 = fn(int $p) => (int) round($p * 0.9);
$vipFlat = fn(int $p) => max(0, $p - 5000);
echo applyDiscount(23000, $percent10); // 20700
echo applyDiscount(23000, $vipFlat); // 18000
interface DiscountRule { public function apply(int $price): int; }
final class Percent implements DiscountRule {
public function __construct(private float $rate) {}
public function apply(int $price): int { return (int) round($price * (1 - $this->rate)); }
}
final class Flat implements DiscountRule {
public function __construct(private int $amount) {}
public function apply(int $price): int { return max(0, $price - $this->amount); }
}
function applyDiscountO(int $price, DiscountRule $rule): int {
return max(0, $rule->apply($price));
}
echo applyDiscountO(23000, new Percent(0.1)); // 20700
echo applyDiscountO(23000, new Flat(5000)); // 18000
간단히 쓰려면 함수/클로저가 짧고 편합니다.
규칙이 많아지거나 검증/메타데이터/확장성이 필요하면 객체가 더 체계적입니다.
PHP의 많은 API는 callable을 받습니다. 가능한 형태:
문자열 "strlen", 배열 [$obj, 'method'], 정적 메서드 "Class::method", 클로저, __invoke 객체.
함수(클로저) 비교
같은 클로저 객체인지 확인은 === 또는 spl_object_id()로 합니다.
“동일 로직인지”를 일반적으로 비교할 방법은 없습니다.
객체 비교
===는 같은 인스턴스인지(동일 참조) 비교.
==는 보통 같은 클래스이고 모든 프로퍼티 값이 동일하면 참으로 취급됩니다.
객체 대입은 핸들(참조처럼) 전달입니다. 복제는 clone을 사용합니다.
작은, 순수한 계산은 전역 유틸 함수로 두되 네임스페이스로 충돌을 피하세요.
외부 자원 접근/부작용은 인터페이스 + 구현 객체로 감싸고, 애플리케이션 서비스에서 주입해 쓰세요.
**값 객체(Value Object)**를 적극 사용하면 도메인 불변식을 코드로 강제할 수 있습니다.
Composer 오토로딩을 쓴다면 클래스 중심 구조가 유지보수에 유리합니다.
과도한 static 메서드 남용은 테스트/확장성 저하의 원인이 됩니다. 상태가 없다면 순수 함수, 상태/폴리시가 있다면 인스턴스 메서드로 분리하세요.
call_user_func()는 직접 호출보다 느립니다. 가능하면 직접 호출이나 **일급 호출자(…)**를 사용하세요.
함수는 “작고 순수한 로직”을 빠르게 재사용하기에 최적.
객체는 “상태 + 규칙”을 안전하게 묶고, 확장성과 테스트 용이성을 확보하는 데 최적.
실제로는 둘을 함께 씁니다: 도메인 모델/서비스는 객체로, 작은 계산/유틸은 함수로.