Developers building Laravel applications are being urged to ditch the new keyword inside their controllers and switch to the Factory Method pattern. By moving object creation into dedicated factories, code becomes easier to extend, test, and maintain—benefits that matter as projects grow beyond a handful of endpoints.
new 키워드가 숨겨진 위험 요소인 이유
컨트롤러에 다음과 같은 코드가 포함되어 있다면,
$processor = new StripePaymentProcessor($config);
해당 컨트롤러는 이제 StripePaymentProcessor 클래스와 강하게 결합(tightly coupled)됩니다. 결제 프로세서가 필요한 모든 곳에서 해당 라인을 반복하게 되어, 생성 로직이 코드베이스 전체에 흩어지게 됩니다. 만약 나중에 프로세서에 로거(logger), 캐시 매니저(cache manager) 또는 다른 설정 형식이 필요해진다면, 해당되는 모든 지점을 업데이트해야 합니다. 그 결과, 변경에 취약하고 단위 테스트에서 모킹(mock)하기 어려운 코드가 됩니다.
Factory Method 패턴의 등장
Factory Method 패턴은 직접적인 인스턴스화 대신 특정 인터페이스의 인스턴스를 반환하는 메서드로 대체합니다. 컨트롤러는 인터페이스에 의존하고, 팩토리 클래스는 어떤 구체적인 클래스(concrete class)를 생성할지, 그리고 그 의존성을 어떻게 조립할지를 알고 있습니다. 모든 생성 로직이 한 곳에 모여 있으므로, 새로운 의존성을 추가하거나 구현체를 교체할 때 팩토리만 수정하면 됩니다.
핵심 장점
- 느슨한 결합(Loose coupling) – 컨트롤러는 구체적인 클래스가 아닌 추상화된 인터페이스를 대상으로 동작합니다.
- 중앙 집중식 생성 – 빌드 프로세스를 변경하려면 단 하나의 파일만 수정하면 됩니다.
- 테스트 용이성 – 팩토리를 스텁(stub)으로 만들거나 모크(mock)로 교체할 수 있어, 컨트롤러를 독립적으로 테스트할 수 있습니다.
- 개방-폐쇄 원칙(Open/Closed Principle) – 기존 컨트롤러 코드를 건드리지 않고도 새로운 기능(예: 새로운 결제 게이트웨이)을 추가할 수 있습니다.
커피숍 비유
모든 주문에 대해 긴 if-else 문을 사용하여 직접 원두를 갈고, 우유를 데우고, 물을 붓는 바리스타를 상상해 보세요. 라떼 레시피가 바뀌면 모든 바리스타가 그 단계를 다시 배워야 합니다. 주문 유형을 받아 내부적으로 준비 과정을 처리하는 커피 머신은 이 문제를 해결합니다. 바리스타는 머신에 무엇이 필요한지만 말하면 되고, 머신이 모든 단계를 캡슐화합니다. 이제 레시피를 업데이트하려면 모든 바리스타가 아닌 머신만 조정하면 됩니다.
Laravel은 이미 팩토리에 의존하고 있습니다
Laravel의 핵심 컴포넌트들은 이 패턴이 실제로 어떻게 작동하는지 잘 보여줍니다.
- Database –
ConnectionFactory가 MySQL 또는 PostgreSQL 연결을 생성할지 결정합니다. - Queues –
QueueManager가 Redis, SQS 또는 기타 백엔드용 드라이버를 생성합니다. - Filesystem –
FilesystemManager가 로컬 또는 S3 디스크 인스턴스를 생성합니다. - Mail –
MailManager가 SMTP, Mailgun 또는 기타 메일 드라이버를 해결(resolve)합니다.
프레임워크가 이러한 중요한 서비스에 대해 팩토리를 신뢰한다면, 커스텀 코드도 그 뒤를 따라야 합니다.
팩토리를 도입해야 할 때
다음과 같은 경우에 팩토리를 사용하세요:
- 객체 생성이 여러 설정 단계나 외부 서비스를 포함하는 경우.
- 교체 가능한 여러 구현체가 존재하는 경우 (다양한 결제 게이트웨이, 스토리지 제공업체 등).
- 가능한 구현체의 목록이 늘어날 것으로 예상되는 경우.
다음과 같은 경우에는 팩토리를 피하세요:
- 추가 설정 없이 단일하고 불변하는
new호출로 생성이 끝나는 경우. - 단 하나의 구현체만 사용될 예정이라 추가적인 추상화가 불필요한 오버헤드가 되는 경우.
빠른 실습: 결제 컨트롤러 리팩터링하기
- 인터페이스 정의 –
process(array $data)메서드를 가진PaymentProcessorInterface를 만듭니다. - 구체적인 클래스 구현 – 인터페이스를 충족하는
StripePaymentProcessor,PayPalPaymentProcessor를 각각 구현합니다. - 팩토리 생성 –
make(string $driver): PaymentProcessorInterface메서드를 가진PaymentProcessorFactory를 만듭니다. 내부에서는switch문이나 맵(map)을 사용하여 적절한 클래스를 반환하고, Laravel의 서비스 컨테이너에서 필요한 서비스를 주입합니다. - 팩토리 주입 – 컨트롤러의 생성자에서
PaymentProcessorFactory를 타입 힌트(type-hint)합니다. Laravel이 이를 자동으로 해결(resolve)합니다. - 팩토리 사용 –
$processor = $this->processorFactory->make('stripe'); $processor->process($request->all());
이제 컨트롤러는 new나 구체적인 프로세서를 전혀 언급하지 않습니다. 새로운 게이트웨이를 추가하려면 클래스를 생성하고 팩토리의 맵을 확장하기만 하면 됩니다. 컨트롤러는 수정할 필요가 없습니다.
잠재적인 단점 및 완화 방법
팩토리의 주요 비판점은 불필요한 간접 참조(indirection)가 추가되어 사소한 객체의 경우 코드가 장황해질 수 있다는 점입니다. 단순한 서비스를 과도하게 설계(over-engineering)하면 실질적인 이득 없이 코드베이스만 비대해질 수 있습니다. 핵심은 생성 복잡도를 평가하는 것입니다. 만약 객체가 의존성이 없는 단순한 데이터 홀더라면, 직접 new를 사용하는 것도 괜찮을 수 있습니다. 또한, 팩토리가 관련 없는 로직을 모아두는 쓰레기통이 될 수도 있습니다. 팩토리가 객체 생성에만 집중하도록 유지하고, 설정은 전용 서비스 프로바이더(service provider)에 위임함으로써 명확성을 유지할 수 있습니다.
다음에 주의할 점
- 서비스 컨테이너 바인딩(Service container bindings) – 서비스 프로바이더에서 인터페이스를 팩토리 클래스에 바인딩하면 Laravel의 컨테이너가 팩토리를 자동으로 해결(resolve)할 수 있습니다.
- 자동 검색(Auto-discovery) – 일부 패키지는 자체 팩토리를 노출하므로, 이름 충돌에 주의해야 합니다.
- 테스트 전략 – 컨트롤러를 단위 테스트할 때는 팩토리를 스텁(stubbed)된 프로세서를 반환하는 모크(mock)로 교체하여, 테스트가 빠르고 격리된 상태를 유지하도록 하세요.
요약
여기저기 흩어져 있는 new 호출을 적절한 위치의 팩토리로 교체하면 생성을 중앙 집중화하고 결합도를 낮추며, 커스텀 코드를 Laravel의 설계 철학과 일치시킬 수 있습니다. 새로운 결제 제공업체, 스토리지 백엔드 또는 플러그인 스타일의 컴포넌트 등 확장을 예상하는 팀에게 Factory Method 패턴은 유연성과 확신이라는 보상으로 돌아오는 저비용 투자입니다. 미래의 자신과 이 코드를 물려받을 모든 이들이 당신에게 고마워할 것입니다.
