액션으로 리팩토링하기 free

2019-08-07

Fat Model, Skinny Controller 는 말을 들어봤을 겁니다. 아마도 루비 온 레즈가 개발자 커뮤니티에 크게 영향을 미친 만트 인것으로 알고 있는데요. 컨트롤러를 최대한 가볍게 유지하는 로직은 모델에서 처리하고 컨트롤러는 최소한의 만 하게 하는 것이죠.


요즘에는 모델도 가볍게 하는 얘기가 종종 들립니다. 클린 아키텍처 인 PHP에도 MVC 패턴을 잘못 쓰는 쓰는 현상의 하나로 “비만 모델”이는 표현이 나옵니다.


스파티의 프릭 반 더 허르텐이 “액션으로 리팩토링하기(Refactoring to Actions)는 글로 컨트롤러와 모델 모두 가볍게 유지할 수 있는 좋은 기법을 소개했습니다.


스파티에서는 단 하나의 로직을 처리할 목적으로 클래스를 만들며, 이 클래스를 “액션”이고 부른다고 합니다. 액션 클래스는 execute()는 단 하나의 공개 메서드를 갖게 한다고 하네요. “액션”이고 부르는 것도 메서드 이름을 “execute”고 한 것도 단지 스파티의 정책 뿐입니다. 따서 다른 이름으로 부르고 다른 메서드명을 사용해도 상관없습니다. 실제로 댓글에 보면 “도메인 서비스”는 더 보편적인 용어가 있다는 지적이 있습니다.


프릭 반 더 허르텐 글의 예제 코드를 보면 이해에 도움이 될 것입니다. 블로그에 ‘공개하기’ 기능이 필요한 경우 흔히 아래와 같이처럼 컨트롤러에 메서드를 추가합니다.


class PostsController
{
public function create()
{
// ...
}

public function store()
{
// ...
}

public function edit()
{
// ...
}

public function update()
{
// ...
}

public function delete()
{
// ...
}

public function publish(Post $post, TwitterApi $twitterApi)
{
$post->markAsPublished();

$twitterApi->tweet($post->title . PHP_EOL . $post->url);

flash()->success('Your post has been published!');

return back();
}
}

컨트롤러에는 기본 메서드 이외의 메서드를 추가하지 않는 것을 권장합니다. 대신 컨트롤러를 새로 만듭니다. 벨 제작자가 추천한 벨 코드 깔끔하게 짜는 방법 초간단 요약을 참고하세요.


공개하기를 처리하는 컨트롤러를 추가하고, “액션”을 활용한 최종 코드는 아래와 같습니다.


namespace App\Actions;

use App\Services\TwitterApi;

class PublishPostAction
{
/** @var \App\Services\TwitterApi */
private $twitter;

public function __construct(TwitterApi $twitter)
{
$this->twitter = $twitter;
}

public function execute(Post $post)
{
$this->markAsPublished($post);

$this->tweet($post->title . PHP_EOL . $post->url);
}

private function markAsPublished(Post $post)
{
$post->published_at = now();

$post->save();
}

private function tweet(string $text)
{
$this->twitter->tweet($text);
}

}

namespace App\Http\Controllers;

use App\Actions\PublishPostAction;

class PublishPostController
{
public function __invoke(Post $post, PublishPostAction $publishPostAction)
{
$publishPostAction->execute($post);

flash()->success('Hurray, your post has been published!');

return back();
}
}

PostController에 있던 공개하기 로직이 PublishPostAction 클래스로 빠졌습니다. 컨트롤러에서는 메서드 인젝션으로 PublishPostAction을 주입받아 사용합니다. Post 모델에 있던 markAsPublished() 메서드도 PublishPostAction으로 뺐습니다. 컨트롤러와 모델 모두 더 가벼워졌고, 공개하기 관련 로직은 PublishPostAction으로 모였습니다.


로직이 별도의 컨트롤러에 있지 않고 별도의 클래스에 있기 때문에 아티즌 커맨드에서도 사용할 수 있습니다.


namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Actions\PublishPostAction;
use App\Models\Post;

class PublishPostCommand extends Command
{
protected $signature = 'blog:publish-post {postId}';

protected $description = 'Publish a post';

public function handle(PublishPostAction $publishPostAction)
{
$post = Post::findOrFail($this->argument('postId'));

$publishPostAction->execute($post);

$this->comment('The post has been published!');
}
}

이렇듯 로직을 액션으로 빼내면 애플리케이션 여기저기에서 사용할 수 있고, 테스트하기 쉬워집니다. 액션이 커지면 더 작은 액션들로 쪼개서 클래스를 작게 유지할 수 있습니다.


원문에서는 코드를 점진적으로 개선하고 위에 소개한 코드 이후에도 테스트하는 코드, 액션을 더 작은 액션들로 쪼개는 코드 예제도 나와있습니다. 이 기법에 관심이 있으시다면 원문을 한 번 읽어보시고 적용하면 더 좋을 것 같아요.

1 1 벨 28호
2019년 8월 7



이현석

메쉬 코리아 개발자. 바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.