𝗠𝗮𝘀𝘁𝗲𝗿𝗶𝗻𝗴 𝗘𝘃𝗲𝗻𝘁-𝗗𝗿𝗶𝘃𝗲𝗻 𝗗𝗲𝗰𝗼𝘂𝗽𝗹𝗶𝗻𝗴 𝗶𝗻 𝗟𝗮𝗿𝗮𝘃𝗲𝗹

Laravel 컨트롤러는 종종 비즈니스 로직이 마구잡이로 쌓이는 곳이 되곤 합니다.

단순한 회원가입 흐름으로 시작합니다. 곧 이메일 알림, Slack 알림, 감사 로그(audit logs), API 호출 등을 하나의 메서드에 추가하게 됩니다. 이렇게 되면 비대한 컨트롤러(Fat controller)가 만들어집니다.

비대한 컨트롤러는 코드를 취약하게 만듭니다. 테스트하기 어렵고, 단일 책임 원칙(Single Responsibility Principle)을 위반합니다.

이를 해결하기 위해 RabbitMQ와 같은 복잡한 도구가 필요하지는 않습니다. Laravel에는 대부분의 요구 사항을 충족할 수 있는 내장 이벤트 시스템이 있습니다.

강한 결합(Tight coupling)의 문제점: 뉴스레터 API가 느려지면 사용자 회원가입도 느려집니다. 메일 서비스가 실패하면 전체 요청이 실패합니다.

해결책: 이벤트 기반 아키텍처(Event-Driven Architecture).

이벤트는 중간 계층 역할을 합니다. 컨트롤러는 동작을 알리고, 리스너(Listener)는 해당 동작에 독립적으로 반응합니다.

가벼운 컨트롤러(Lean controller)는 다음과 같습니다:

public function register(RegisterRequest $request)
{
    $user = User::create($request->validated());

    UserRegistered::dispatch($user);

    return response()->json(['message' => 'Success'], 201);
}

이제 컨트롤러는 데이터 영속성(Data persistence)만 처리합니다. 부수 효과(Side effects)에 대해서는 신경 쓰지 않습니다.

세 가지 주요 이점을 얻을 수 있습니다:

  • 성능(Performance): 사용자에게 즉시 응답을 보냅니다. 무거운 작업은 ShouldQueue 인터페이스를 사용하여 백그라운드에서 실행됩니다.
  • 회복 탄력성(Resilience): 서비스가 다운되어도 리스너는 메인 애플리케이션을 중단시키지 않고 작업을 재시도할 수 있습니다.
  • 확장성(Extensibility): 새로운 리스너를 추가함으로써 푸시 알림과 같은 새로운 기능을 추가할 수 있습니다. 기존 컨트롤러를 수정할 필요가 없습니다.

준수해야 할 모범 사례:

  • 부수 효과에 집중하세요: 후처리(post-processing)를 위해 이벤트를 사용하세요. 즉시 실행되어야 하는 핵심 로직에는 사용하지 마세요.
  • 설명적인 이름을 사용하세요: OrderPlacedUserRegistered와 같이 과거형 이름을 사용하세요. 이는 동작이 이미 완료되었음을 나타냅니다.
  • 과도한 추상화를 피하세요: 코드가 단순하고 한 곳에서만 사용된다면, 이벤트보다는 함수 호출이 더 낫습니다.

데이터베이스 변경에는 Eloquent Observers를 사용하세요. 비즈니스 동작에는 Events를 사용하세요.

이벤트로 리팩터링하는 것은 내구성(durability)에 관한 것입니다. 이는 코드를 디버깅하기 쉽게 만들고 테스트 속도를 높여줍니다.

오늘 컨트롤러에 있는 복잡한 부수 효과 하나를 골라 리스너로 옮겨보세요.

이 샌드박스 예제를 확인해 보세요: https://onlinephp.io/c/1f7b2

비대한 컨트롤러를 넘어: Laravel에서 이벤트 기반 디커플링 마스터하기

애플리케이션이 성장함에 따라, 컨트롤러는 종종 "비대한 컨트롤러(Fat Controllers)"로 변질되곤 합니다. 컨트롤러가 비즈니스 로직, 데이터베이스 작업, 이메일 전송, 외부 API 호출 등 너무 많은 책임을 떠맡게 되면 코드는 읽기 어렵고, 테스트하기 힘들며, 유지보수가 매우 까다로워집니다.

이러한 문제를 해결하고 코드의 결합도를 낮추는 가장 효과적인 방법 중 하나는 **이벤트 기반 아키텍처(Event-Driven Architecture)**를 도입하는 것입니다. 이 글에서는 Laravel에서 이벤트를 사용하여 어떻게 컨트롤러를 가볍게 유지하고 시스템을 디커플링(decoupling)할 수 있는지 알아보겠습니다.

비대한 컨트롤러의 문제점

비대한 컨트롤러는 단일 책임 원칙(Single Responsibility Principle, SRP)을 위반합니다. 컨트롤러의 주된 역할은 HTTP 요청을 받고, 적절한 모델을 호출하여 응답을 반환하는 것입니다. 하지만 다음과 같은 로직이 컨트롤러에 포함되기 시작하면 문제가 발생합니다:

  • 사용자 등록 후 환영 이메일 발송
  • 활동 로그 기록
  • 결제 완료 후 인보이스 생성
  • 외부 서비스(예: Stripe, Mailchimp)와의 통신

이러한 로직이 컨트롤러에 섞여 있으면, 특정 기능을 수정할 때 컨트롤러 전체를 건드려야 하며, 이는 예상치 못한 부작용(side effects)을 초래할 수 있습니다.

해결책: 이벤트와 리스너 (Events & Listeners)

Laravel의 이벤트 시스템은 관찰자 패턴(Observer Pattern)을 구현하여 구성 요소 간의 결합도를 낮춰줍니다. 핵심 개념은 다음과 같습니다:

  1. 이벤트 (Event): "무슨 일이 일어났음"을 나타내는 단순한 데이터 객체입니다. (예: OrderPlaced)
  2. 리스너 (Listener): 이벤트가 발생했을 때 실행될 작업입니다. (예: SendOrderConfirmationEmail)

구현 단계

1. 이벤트 생성하기

먼저, OrderPlaced 이벤트를 생성합니다.

php artisan make:event OrderPlaced

이벤트 클래스는 발생한 사건에 필요한 데이터를 담습니다.

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPlaced
{
    use Dispatchable, SerializesModels;

    public $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

2. 리스너 생성하기

이제 주문이 발생했을 때 이메일을 보내는 리스너를 생성합니다.

php artisan make:listener SendOrderConfirmationEmail --event=OrderPlaced

리스너의 handle 메서드에 실제 로직을 작성합니다.

namespace App\Listeners;

use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmationMail;

class SendOrderConfirmationEmail
{
    public function handle(OrderPlaced $event)
    {
        Mail::to($event->order->user->email)
            ->send(new OrderConfirmationMail($event->order));
    }
}

3. 이벤트를 컨트롤러에서 발생시키기 (Dispatching)

이제 컨트롤러는 주문을 처리하고 이벤트를 던지기만 하면 됩니다.

public function store(Request $request)
{
    // 주문 생성 로직...
    $order = Order::create($request->all());

    // 이벤트 발생!
    OrderPlaced::dispatch($order);

    return response()->json(['message' => 'Order placed successfully!'], 201);
}

이제 컨트롤러는 이메일이 어떻게 발송되는지, 로그가 어떻게 남는지 알 필요가 없습니다. 오직 주문을 생성하는 데만 집중합니다.

옵저버 (Observers) 활용하기

이벤트와 리스너가 특정 "행위"에 집중한다면, Eloquent Observers는 모델의 생명주기(Lifecycle)에 집중합니다. 모델이 생성(created), 업데이트(updated), 삭제(deleted)될 때 자동으로 특정 로직을 실행하고 싶을 때 유용합니다.

php artisan make:observer UserObserver --model=User

예를 들어, 사용자가 생성될 때 프로필 이미지를 기본값으로 설정하는 로직을 옵저버에 넣을 수 있습니다.

결론

이벤트 기반 디커플링을 사용하면 다음과 같은 이점을 얻을 수 있습니다:

  • 유지보수성 향상: 각 클래스가 하나의 책임만 가집니다.
  • 확장성: 새로운 기능을 추가할 때(예: 주문 시 SMS 발송) 기존 컨트롤러 코드를 수정할 필요 없이 새로운 리스너만 추가하면 됩니다.
  • 테스트 용이성: 비즈니스 로직이 분리되어 있어 단위 테스트가 훨씬 쉬워집니다.

Laravel의 강력한 이벤트 시스템을 활용하여 더 깨끗하고, 확장 가능한 코드를 작성해 보세요!