0Pricing
PHP Academy · 강의

사용 사례와 애플리케이션 서비스

프레임워크에 종속되지 않는 사용 사례로 비즈니스 동작을 표현합니다.

사용 사례와 애플리케이션 서비스은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

사용 사례란 실제로 무엇인가

사용 사례(애플리케이션 서비스 또는 인터랙터라고도 합니다)는 사용자 등록, 주문 생성, 구독 취소처럼 애플리케이션에 특화된 하나의 작업을 정확히 표현합니다. 하나의 의도를 수행하기 위해 엔터티와 포트를 조정합니다. 특히 프레임워크에 종속되지 않습니다. Request도, Response도, 전역 도우미도 없이 어디서든 호출할 수 있는 순수한 PHP 코드만 사용합니다.

명령 및 결과 DTO

사용 사례는 변경할 수 없는 명령 DTO를 입력으로 받고 결과 DTO를 반환합니다. DTO는 동작이나 형태 이상의 검증 로직이 없는 단순한 데이터 전달 객체입니다. 읽기 전용 속성(PHP 8.1 이상)을 사용하면 데이터를 변조할 수 없게 만들 수 있습니다.

<?php
final class RegisterUserCommand
{
    public function __construct(
        public readonly string $email,
        public readonly string $plainPassword,
    ) {}
}

final class RegisterUserResult
{
    public function __construct(public readonly string $userId) {}
}

애플리케이션 서비스의 본문

서비스는 명령을 도메인 작업으로 변환합니다. 고유성 확인, 저장, 식별자 반환과 같은 애플리케이션 수준의 조정을 수행하고, 규칙은 엔터티에 위임합니다.

<?php
final class RegisterUser
{
    public function __construct(
        private Users $users,
        private PasswordHasher $hasher,
    ) {}

    public function __invoke(RegisterUserCommand $c): RegisterUserResult {
        if ($this->users->existsByEmail($c->email)) {
            throw new EmailAlreadyRegistered($c->email);
        }
        $user = User::register(
            UserId::generate(),
            new Email($c->email),
            $this->hasher->hash($c->plainPassword),
        );
        $this->users->add($user);
        return new RegisterUserResult((string) $user->id());
    }
}

로직은 엔터티에 두기

빈약한 도메인 모델을 주의해야 합니다. 이는 엔터티가 접근자와 설정자로 축소되고 모든 로직이 서비스에 들어간 상태를 말합니다. 불변 조건은 엔터티에 있어야 합니다. 사용 사례는 비즈니스 규칙이 장황하게 늘어선 코드가 아니라 의도를 간결하게 나열한 짧은 대본처럼 읽혀야 합니다.

<?php
final class User
{
    private function __construct(
        private UserId $id,
        private Email $email,
        private string $passwordHash,
        private bool $active = false,
    ) {}

    public static function register(UserId $id, Email $e, string $hash): self {
        return new self($id, $e, $hash); // invariants enforced here
    }
    public function activate(): void {
        if ($this->active) throw new AlreadyActive();
        $this->active = true;
    }
    public function id(): UserId { return $this->id; }
}

트랜잭션 경계

사용 사례는 자연스러운 트랜잭션 경계입니다. 사용 사례 하나가 일관된 작업 단위 하나가 됩니다. 서비스 곳곳에 beginTransaction()을 흩뿌리는 대신 트랜잭션 데코레이터로 감싸면 핵심부가 저장 방식에 의존하지 않게 됩니다.

<?php
interface TransactionManager {
    public function transactional(callable $work): mixed;
}

final class TransactionalRegisterUser
{
    public function __construct(
        private RegisterUser $inner,
        private TransactionManager $tx,
    ) {}

    public function __invoke(RegisterUserCommand $c): RegisterUserResult {
        return $this->tx->transactional(fn() => ($this->inner)($c));
    }
}

검증: 어디에 두어야 하는가

검증을 두 계층으로 나누십시오.

  • 입력 검증(형식, 필수 필드)은 사용 사례가 실행되기 전에 구동 어댑터나 전용 검증기에서 수행합니다.
  • 도메인 검증(불변 조건, 비즈니스 규칙)은 값 객체와 엔터티에 두고 도메인 예외를 발생시킵니다.

사용 사례는 형식이 올바른 입력을 전제로 하며 의미를 보장합니다.

<?php
final class Email
{
    public function __construct(public readonly string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $value");
        }
    }
}

try { new Email('nope'); } catch (Throwable $e) { echo $e->getMessage(), PHP_EOL; }
echo (new Email('a@b.com'))->value, PHP_EOL;

HTTP 없이 출력 반환하기

프레임워크에 종속되지 않은 상태로 데이터를 반환하는 두 가지 방식은 다음과 같습니다.

  • 결과 DTO 반환(간단하고 동기식입니다).
  • 출력 포트 / 프레젠터 — 사용 사례가 주입된 출력 경계로 결과를 전달하면 어댑터가 형식(JSON, HTML, CLI)을 결정합니다. 이렇게 하면 응답 형태까지 핵심부 밖에 둘 수 있습니다.
<?php
interface RegisterUserOutput {
    public function present(RegisterUserResult $r): void;
}

final class RegisterUserWithPresenter {
    public function __construct(private Users $users, private PasswordHasher $h) {}
    public function __invoke(RegisterUserCommand $c, RegisterUserOutput $out): void {
        $user = User::register(UserId::generate(), new Email($c->email), $this->h->hash($c->plainPassword));
        $this->users->add($user);
        $out->present(new RegisterUserResult((string) $user->id()));
    }
}

사용 사례에서 도메인 이벤트 발생시키기

사용 사례는 엔터티가 발생시킨 도메인 이벤트를 기록한 다음 트랜잭션이 커밋된 후 전달하는 경우가 많습니다. 이를 통해 부수 효과(환영 이메일 전송, 조회 모델 업데이트)를 핵심 작업 흐름에서 분리할 수 있습니다.

<?php
trait RecordsEvents {
    private array $events = [];
    protected function record(object $e): void { $this->events[] = $e; }
    public function releaseEvents(): array {
        $e = $this->events; $this->events = []; return $e;
    }
}

final class UserRegistered {
    public function __construct(public readonly string $userId) {}
}

// Use case calls $user->releaseEvents() and hands them to a dispatcher
echo 'event recorded pattern', PHP_EOL;

사용 사례마다 하나의 클래스

메서드가 열 개 있는 비대한 서비스보다 단일 작업 클래스(공개 메서드 하나, 흔히 __invoke)를 사용하는 편이 좋습니다. 장점은 다음과 같습니다.

  • 명확한 단일 책임과 이름을 갖습니다(SubscriptionService::cancel이 아니라 CancelSubscription).
  • 생성자에는 이 작업에 필요한 항목만 주입합니다.
  • 데코레이터(트랜잭션, 로그 기록, 권한 부여)로 쉽게 감쌀 수 있습니다.

구성 루트에서 연결하기

사용 사례는 의존성을 직접 생성하지 않습니다. 구성 루트가 생성합니다. 다음은 DI 컨테이너 정의에 넣을 수 있는 수동 연결 예시입니다.

<?php
$pdo      = new PDO('sqlite::memory:');
$users    = new PdoUsers($pdo);
$hasher   = new BcryptHasher();
$register = new RegisterUser($users, $hasher);

// Decorate with a transaction boundary
$register = new TransactionalRegisterUser($register, new PdoTransactionManager($pdo));

// Driving adapter calls it
$result = $register(new RegisterUserCommand('dev@coddykit.com', 's3cret!'));
echo $result->userId, PHP_EOL;

데코레이터로 횡단 관심사 처리

로그 기록, 메트릭, 권한 부여는 횡단 관심사이므로 사용 사례 본문에서 분리하십시오. 서비스와 동일한 인터페이스를 공유하는 데코레이터로 서비스를 감싸면 핵심부는 작업 흐름에 집중하고 인프라 관심사는 주변에서 조합할 수 있습니다.

<?php
interface RegisterUserHandler {
    public function __invoke(RegisterUserCommand $c): RegisterUserResult;
}

final class LoggingRegisterUser implements RegisterUserHandler {
    public function __construct(
        private RegisterUserHandler $inner,
        private LoggerInterface $log,
    ) {}
    public function __invoke(RegisterUserCommand $c): RegisterUserResult {
        $this->log->info('register.start', ['email' => $c->email]);
        $r = ($this->inner)($c);
        $this->log->info('register.ok', ['id' => $r->userId]);
        return $r;
    }
}

빠른 확인

"이메일은 고유하고 형식이 올바라야 한다"는 규칙은 어디에 있어야 하나요?

복습

프레임워크에 종속되지 않은 사용 사례로 깔끔한 애플리케이션 계층을 만들 수 있습니다.

  • 작업마다 단일 작업 클래스 하나를 두고, 명령 DTO를 받아 결과 DTO를 반환하거나 출력 포트로 전달합니다.
  • 엔터티와 값 객체가 불변 조건을 소유하고, 서비스는 조정만 담당하게 하여 빈약한 모델을 피합니다.
  • 사용 사례는 트랜잭션 경계이며, 내부에 beginTransaction을 직접 넣는 대신 데코레이터로 감쌉니다.
  • 입력 검증(어댑터/VO)과 도메인 검증(엔터티)을 분리합니다.
  • 도메인 이벤트로 부수 효과를 분리하고, 구성 루트에서 의존성을 연결합니다.

자주 묻는 질문

“사용 사례와 애플리케이션 서비스” 강의는 무료인가요?

네 — “사용 사례와 애플리케이션 서비스” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“사용 사례와 애플리케이션 서비스”에서 뭘 배우나요?

프레임워크에 종속되지 않는 사용 사례로 비즈니스 동작을 표현합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

PHP Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“사용 사례와 애플리케이션 서비스” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 계층형 아키텍처에서 클린 아키텍처로
  2. 포트와 어댑터 설명
  3. 사용 사례와 애플리케이션 서비스
  4. 의존성 역전 실전
← PHP Academy(으)로 돌아가기