> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 패키지의 응답 Macro와 HTTP 계약

> Laravel 13의 ResponseFactory와 Macroable을 읽고, 패키지 고유의 JSON 응답, 이름 충돌, 등록 상태의 수명, HTTP 계약의 테스트와 유지보수를 해설합니다.

패키지 사용자가 여러 컨트롤러에서 같은 형식의 오류 JSON을 반환할 수 있도록 하고 싶을 때가 있습니다. 응답 Macro를 공개하면 컨트롤러에 상속을 요구하지 않고도 `response()->acmeFailure(...)` 같은 생성 API를 추가할 수 있습니다.

이 페이지는 Laravel 공식 문서의 기본 브랜치 `13.x`와, 2026년 11월 8일에 확인한 Framework 최신 릴리스 `v13.40.0`의 공식 구현을 참조합니다. [응답](https://kawax.biz/ko/responses.md)과 [Macroable 트레이트](https://kawax.biz/ko/advanced/macroable.md)의 기본을 바탕으로, 배포 패키지의 공개 API로서 유지보수할 때의 경계를 다룹니다.

## 등록 대상은 응답이 아니라 Factory

`Response`라는 이름에는 생성을 의뢰하는 파사드와, 생성된 HTTP 응답 클래스가 있습니다. 공식 문서의 `Response::macro()`가 확장하는 것은 파사드의 해석 대상인 `ResponseFactory`입니다.

| 대상 | 역할 | 이 페이지에서의 취급 |
| - | - | - |
| `Illuminate\Support\Facades\Response` | `ResponseFactory` 계약을 해석해 호출하는 파사드 | Macro 등록과 호출 |
| `Illuminate\Routing\ResponseFactory` | `json()`·`view()`·`noContent()` 등의 생성 API | Macro를 보유하는 클래스 |
| `Illuminate\Http\Response` | 생성된 HTTP 응답 | Factory와는 별도의 Macro 등록 대상 |
| `Illuminate\Http\JsonResponse` | JSON 응답 | 이번 Macro의 반환값 |

인수 없는 `response()`는 Factory를 반환합니다. 반면 `response('본문')`이나 `response(null)`은 `make()`로 생성한 응답을 반환하므로 같은 호출 대상이 아닙니다.

> [!WARNING]
>
> `Illuminate\Http\Response::macro()`에 등록해도 `response()->acmeFailure()`의 Macro가 되지 않습니다. 파사드와 응답 본체의 같은 이름 클래스를 `use` 문에서 혼동하지 마세요.

## 오류 JSON 생성 API 공개하기

### Service Provider에서 등록하기

`src/HttpServiceProvider.php`의 `boot()`에 패키지 접두사가 붙은 Macro를 등록합니다. 프로바이더 로딩은 [패키지 자동 검색](https://kawax.biz/ko/advanced/package-discovery.md)을 참조하세요.

이 예의 공개 계약은 `error.code`·`error.message`를 가진 JSON과 400\~599의 HTTP 상태입니다. 이 상태 제한은 Laravel Macro 메커니즘의 제약이 아니라, 오류 전용 API로서 패키지가 정한 것입니다.

```php theme={null}
<?php

namespace Acme\Http;

use Illuminate\Http\JsonResponse;
use Illuminate\Routing\ResponseFactory;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;

final class HttpServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Response::macro('acmeFailure', function (
            string $code,
            string $message,
            int $status = 422,
            array $headers = [],
        ): JsonResponse {
            if ($status < 400 || $status > 599) {
                throw new InvalidArgumentException(
                    'acmeFailure requires a status between 400 and 599.'
                );
            }

            /** @var ResponseFactory $this */
            return $this->json([
                'error' => [
                    'code' => $code,
                    'message' => $message,
                ],
            ], $status, $headers);
        });
    }
}
```

Macro를 사용하기 전에 이 프로바이더가 부팅되어야 합니다. 등록을 HTTP 라우트 로딩 파일이나 `runningInConsole()`의 부정 조건 안에 두지 마세요. HTTP 기능 테스트도 CLI에서 실행됩니다.

이 등록만 담당하는 프로바이더를 서비스 해석을 기다리는 지연 프로바이더로 만들 필요도 없습니다. 지연 등록의 구조는 [지연 서비스 프로바이더](https://kawax.biz/ko/advanced/deferred-provider.md)를 참조하세요.

### 컨트롤러에서 호출하기

비즈니스 로직에서 충돌을 판정한 뒤, 정해 둔 오류 코드와 사용자용 메시지를 전달합니다.

```php theme={null}
return response()->acmeFailure(
    code: 'order_conflict',
    message: '注文を確定できませんでした。',
    status: 409,
    headers: ['X-Acme-Error' => 'order_conflict'],
);
```

HTTP 상태는 `409`이고, JSON은 다음과 같은 형태가 됩니다. 일반적인 `json()`과 마찬가지로 `Content-Type`은 `application/json`입니다.

```json theme={null}
{
  "error": {
    "code": "order_conflict",
    "message": "注文を確定できませんでした。"
  }
}
```

파사드를 통해서도 같은 Factory의 Macro를 호출할 수 있습니다.

```php theme={null}
use Illuminate\Support\Facades\Response;

return Response::acmeFailure(
    'order_conflict', '注文を確定できませんでした。', 409
);
```

> [!NOTE]
>
> Macro는 이 메서드를 호출한 응답만 생성합니다. 유효성 검사 예외나 인증 오류를 자동으로 이 형식으로 바꾸지 않습니다. 또한 `Accept` 헤더에 따라 HTML과 JSON을 전환하는 처리도 추가하지 않습니다.

## Closure의 `$this`와 호출 경로

Factory는 `Macroable`을 사용합니다. 인스턴스의 정의되지 않은 메서드를 호출하면 `__call()`이 등록된 Macro를 찾고, 일반 Closure를 호출 대상 인스턴스에 다시 바인딩합니다.

```mermaid theme={null}
flowchart TD
    A["response()->acmeFailure(...)<br>또는 Response::acmeFailure(...)"] --> B["ResponseFactory 인스턴스"]
    B --> C["Macroable::__call()<br>Closure의 $this를 다시 바인딩"]
    C --> D["ResponseFactory::json()"]
    D --> E["새 JsonResponse"]
```

등록 코드가 Service Provider의 메서드 안에 있어도, 실행 시 `$this`는 Service Provider가 아니라 Factory입니다. 그래서 예의 `$this->json()`은 Factory의 일반 메서드를 호출합니다.

* `$this`를 사용하는 Macro는 일반 `function (...) { ... }`로 정의합니다. `static function`에는 인스턴스의 `$this`가 없습니다.
* invokable 객체를 등록한 경우 Closure처럼 다시 바인딩되지 않습니다. 필요한 의존성은 그 객체 자신의 생성자 등으로 받습니다.
* Macro의 인수는 호출 측이 전달한 인수입니다. `Request` 등을 타입 선언해도 Macroable이 컨테이너에서 자동 주입하지는 않습니다.

파사드의 `Response::acmeFailure()`는 겉보기에는 정적이지만 내부적으로 Factory 인스턴스로 전달됩니다. 반면 구체 클래스의 `Illuminate\Routing\ResponseFactory::acmeFailure()`는 진짜 정적 호출이 되며, `__callStatic()`은 Closure를 인스턴스 없이 바인딩합니다. 이번처럼 `$this->json()`을 사용하는 Macro는 구체 클래스의 정적 호출을 지원하지 않습니다.

## 이름 충돌을 호환성 문제로 다루기

`macro()`는 이름을 키로 하는 정적 배열에 대입할 뿐입니다. 기존 이름을 등록해도 경고가 나오지 않습니다.

| 상황 | 확인한 구현의 동작 |
| - | - |
| 같은 이름의 Macro를 다시 등록 | 나중 등록이 이전 Macro를 덮어쓴다 |
| `json()` 등 클래스에 실제로 존재하는 공개 메서드와 같은 이름으로 등록 | 일반 메서드가 호출되고 Macro에는 도달하지 않는다 |
| `hasMacro($name)`가 `true` | 등록되어 있다는 의미. 실제 메서드와의 충돌이 없다는 보장은 아니다 |
| `mixin($object, replace: false)` | 등록된 Macro의 교체를 피한다. 실제 메서드와의 충돌 검사는 아니다 |

`failure` 같은 범용 이름보다 `acmeFailure`처럼 패키지 이름을 식별할 수 있는 이름을 선택합니다. Laravel 업데이트 시에는 `method_exists(ResponseFactory::class, 'acmeFailure')`로도 실제 메서드와의 충돌을 확인합니다.

`hasMacro()`를 보고 무조건 등록을 건너뛰면, 다른 패키지의 구현을 자신의 API라고 생각하고 사용할 가능성이 있습니다. "앱 측의 사전 등록으로 덮어쓸 수 있다"는 것을 사양으로 삼는 경우에만, 그 우선순위와 필요한 인수·반환값을 공개하세요. 위 예는 프로바이더가 부팅될 때마다 패키지 자신의 정의를 등록하는 방침입니다.

## 등록 상태와 응답의 수명을 분리하기

Macro 등록표는 Factory의 **정적 프로퍼티**입니다. Factory를 다시 만들거나 파사드의 해석된 인스턴스를 지우는 것만으로는 이 표가 사라지지 않습니다. 같은 PHP 프로세스 안에서 애플리케이션을 다시 생성하는 테스트에서도 등록 상태가 남을 수 있습니다.

반면 예의 Macro는 호출할 때마다 `json()`이 새 `JsonResponse`를 생성합니다. 공유하는 것은 생성 처리이며, 응답의 본문이나 헤더 자체가 아닙니다.

장수명 워커에서 지켜야 할 경계는 다음과 같습니다.

1. 등록 시에 현재 Request·인증 사용자·테넌트를 `use (...)`로 캡처하지 않는다. 이 값들은 호출 시 인수로 전달한다.
2. 등록 시에 생성한 응답을 재사용하지 않는다. 매번 새 응답을 반환한다.
3. 설정을 등록 시에 값으로 캡처하는 경우, 그 값이 프로세스 안에서 유지된다는 것을 사양에 포함한다.
4. 패키지 업데이트 후에는 상주 프로세스를 재시작해 새 등록 코드를 반영한다.

[패키지 서비스의 라이프사이클 설계](https://kawax.biz/ko/advanced/package-service-lifetimes.md)에서는 컨테이너의 공유 상태와 요청 단위 상태를 더 자세히 다룹니다.

> [!WARNING]
>
> `Response::flushMacros()`는 Factory에 등록된 **모든 Macro**를 삭제합니다. 다른 패키지의 등록도 사라지므로, 프로덕션의 요청 처리나 공유 테스트 환경의 안이한 후처리에 사용하지 마세요. 충돌·리셋 검증은 분리된 테스트 프로세스에서 수행합니다.

## HTTP 계약을 실제 반환값으로 테스트하기

패키지를 Composer로 로드할 수 있는 Laravel 13 검증용 앱에 다음 `tests/Feature/ResponseMacroTest.php`를 둡니다. `Tests\TestCase`와 HTTP 테스트의 기본은 [HTTP 테스트](https://kawax.biz/ko/http-tests.md)를, 패키지 단독 환경 구축은 [패키지 테스트](https://kawax.biz/ko/advanced/package-testing.md)를 참조하세요.

```php theme={null}
<?php

namespace Tests\Feature;

use Acme\Http\HttpServiceProvider;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Route;
use InvalidArgumentException;
use Tests\TestCase;

class ResponseMacroTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->app->register(HttpServiceProvider::class);
    }

    public function test_http_response_keeps_the_public_contract(): void
    {
        Route::get('/_acme/failure', function (): JsonResponse {
            return response()->acmeFailure(
                'order_conflict',
                '注文を確定できませんでした。',
                409,
                ['X-Acme-Error' => 'order_conflict'],
            );
        });

        $this->getJson('/_acme/failure')
            ->assertStatus(409)
            ->assertHeader('Content-Type', 'application/json')
            ->assertHeader('X-Acme-Error', 'order_conflict')
            ->assertExactJson([
                'error' => [
                    'code' => 'order_conflict',
                    'message' => '注文を確定できませんでした。',
                ],
            ]);
    }

    public function test_facade_returns_a_fresh_json_response(): void
    {
        $first = Response::acmeFailure('first', '最初のエラー');
        $second = response()->acmeFailure('second', '次のエラー', 503);

        $this->assertInstanceOf(JsonResponse::class, $first);
        $this->assertNotSame($first, $second);
        $this->assertSame(422, $first->getStatusCode());

        $first->setData(['changed' => true]);

        $this->assertSame(503, $second->getStatusCode());
        $this->assertSame('second', $second->getData(true)['error']['code']);
    }

    public function test_success_status_is_rejected(): void
    {
        $this->expectException(InvalidArgumentException::class);

        response()->acmeFailure('invalid_status', '設定を確認してください。', 200);
    }
}
```

`Response::shouldReceive('acmeFailure')`처럼 생성 API 자체를 모킹하면 Macro 등록·Closure 바인딩·JSON 생성이 실행되지 않습니다. 호출 횟수 검증과는 분리해서, 위와 같이 실제 HTTP 응답을 검증합니다.

또한 사용자가 받는 본문·상태·헤더를 호환성의 대상으로 다룹니다.

* `error.code`는 기계 판정용으로 안정시키고, 표시 메시지 변경과 구별한다.
* 인수 이름은 명명된 인수로 사용되므로, 기본값이나 순서와 마찬가지로 변경의 영향을 확인한다.
* `204` 같은 본문 없는 응답은 오류 JSON이 아니라 `noContent()` 등 별도의 API로 반환한다.
* `Content-Type`을 독자 헤더로 변경하는 확장을 허용하는 경우에도 본문 형식과 일치시킨다.
* 데이터 직렬화나 페이지네이션이 목적이라면 Macro만으로 재구현하지 말고 [Eloquent API 리소스](https://kawax.biz/ko/eloquent-resources.md)도 검토한다.

## 장기 유지보수 체크리스트

* [ ] Laravel 13의 지원 대상 조합에서 프로바이더가 부팅되고, 헬퍼와 파사드 양쪽에서 호출할 수 있다.
* [ ] Macro 이름이 다른 등록이나 Factory의 실제 메서드와 충돌하지 않는다.
* [ ] JSON 키, HTTP 상태, 헤더, 기본값, 명명된 인수의 호환성을 테스트하고 있다.
* [ ] 연속 호출에서도 응답을 공유하지 않으며, Request나 사용자 정보를 등록 시에 보유하지 않는다.
* [ ] 등록을 리셋하는 테스트를 다른 패키지와 분리하고 있다.
* [ ] 상주 프로세스 재시작을 업데이트 절차에 포함하고 있다.

## 참조한 1차 정보

* [Laravel 공식 문서: Response macros·JSON responses (13.x)](https://github.com/laravel/docs/blob/13.x/responses.md#response-macros)
* [Laravel 공식 문서: Package development (13.x)](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Response 파사드와 해석 대상 (v13.40.0)](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/Facades/Response.php)
* [Facade의 인스턴스 전달 (v13.40.0)](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/Facades/Facade.php)
* [response() 헬퍼 (v13.40.0)](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Foundation/helpers.php)
* [ResponseFactory의 생성 처리 (v13.40.0)](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Routing/ResponseFactory.php)
* [Macroable의 등록·바인딩·리셋 (v13.40.0)](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Macroable/Traits/Macroable.php)
* [Macroable의 공식 테스트 (v13.40.0)](https://github.com/laravel/framework/blob/v13.40.0/tests/Support/SupportMacroableTest.php)


## Related topics

- [Macroable 트레이트](https://kawax.biz/ko/advanced/macroable.md)
- [고급 주제](https://kawax.biz/ko/advanced/index.md)
- [패키지의 커스텀 세션 드라이버](https://kawax.biz/ko/advanced/package-session-drivers.md)
- [패키지의 HTTP 클라이언트와 테스트](https://kawax.biz/ko/advanced/package-http-client.md)
- [패키지의 커스텀 캐시 스토어와 계약](https://kawax.biz/ko/advanced/package-cache-drivers.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.