테일러 오트웰이 사랑한 헬퍼, tap() free

2019-07-09

러 오트웰이 예전에 자신의 블로그에 Tap, Tap, Tap는 글을 올리면서 자신이 벨에서 가장 좋아하는 헬퍼 메소드가 tap고 소개한 바 있다. 대체 뭐길래 그랬을까? 오늘은 tap이 무엇이고 어떻게 동작하는지 알아보자.


질문. 다음의 리턴값은?


return $user->update([
'name' => $name,
'age' => $age
]);

아래 코드는 Model.phpupdate() 메소드이다. 주석 중 @return 항목에 bool 되어 있다. 따서 정답은 bool, 즉 true 아니면 false이다.


/**
* Update the model in the database.
*
* @param array $attributes
* @param array $options
* @return bool
*/
public function update(array $attributes = [], array $options = [])
{
if (! $this->exists) {
return false;
}

return $this->fill($attributes)->save($options);
}

업데이트한 $user를 리턴하고 싶으면 어떻게 해야할까? 뭐 쉽다. $user를 리턴하면 된다.


$user->update([
'name' => $name,
'age' => $age
]);

return $user;

tap을 쓰면 한 줄로 처리할 수 있다.


return tap($user)->update([
'name' => $name,
'age' => $age
]);

위 코드는 update()의 결과가 아닌 update()를 실행한 $user를 반환한다. 이렇게만 보면 굉장히 소소해 보이는 기능인데, 자주쓰는 사람은 꽤 유용한가보다. 용법만 알고 쓰면 되긴 하지만, 동작원리가 궁금한 사람은 코드도 한 번 읽어보자.


옛 버전 tap


tap은 7줄 짜리 짧은 메소드이다. 그런데, 이전에는 3줄 짜리 메소드였다.


function tap($value, $callback)
{
$callback($value);

return $value;
}

더 단순하면 더 이해하기 쉬우니 옛 버전으로 먼저 이해해보자. 설명을 보면 주어진 $value를 가지고 클로저($callback)를 호출한 다음 $value를 리턴한다.고 되어 있다. 앞서 살펴본 $user->update() 예제를 옛 버전으로 처리하려면 아래와 같이 하면 됐다.


return tap($user, function($user) {
$user->update([
'name' => $name,
'age' => $age
]);
});

새 버전 tap


조금 더 불편하면 되는데, 한 줄이도 더 줄이고 싶었는지 다음과 같이 클로저를 안 받을 수 있게 메소드를 변경했다.


/**
* Call the given Closure with the given value then return the value.
*
* @param mixed $value
* @param callable|null $callback
* @return mixed
*/
function tap($value, $callback = null)
{
if (is_null($callback)) {
return new HigherOrderTapProxy($value);
}

$callback($value);

return $value;
}

이제 클로저가 주어지지 않을 수 있고, 그 경우 HigherOrderTapProxy를 리턴한다.


HigherOrderTapProxy


class HigherOrderTapProxy
{
public $target;

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

public function __call($method, $parameters)
{
$this->target->{$method}(...$parameters);

return $this->target;
}
}

HigherOrderTapProxy 클래스는 생성자와 __call 매직 메소드 하나만 가지고 있다.


__call


__call 매직 메소드는 미리 정의되지 않은 메소드를 실행한다.


$HigherOrderTapProxy = new HigherOrderTapProxy($target);

$HigherOrderTapProxy->update([
'name' => $name,
'age' => $age
])

HigherOrderTapProxy 클래스에 update는 메소드가 없기 때문에 에러가 나야하지만, __call이 선언되어 있기 때문에 __call가 이를 처리한다. 호출한 메소드인 ‘update’가 __call 메소드의 첫번째 인자로, update 메소드가 받은 인자들이 배열로 변환되어 __call 메소드의 두번째 인자로 전달된다.


가변 길이 인수 목록 …


__call 메소드의 내용을 보면 아래와 같은 구문이 보인다.


$this->target->{$method}(...$parameters);

...는 PHP 5.6에 추가된 기능으로 인수를 넘길 때 ...를 붙이면 배열을 풀어서 전달해준다.


$args = [1, 3, 5, 7, 9];
MyClass::someMethod(...$args);

위 코드와 아래 코드가 같다.

MyClass::someMethod(1, 3, 4, 7, 9);

HigherOrderTapProxy 정리


HigherOrderTapProxy는 객체를 받아서 그 객체의 메소드를 실행한 뒤 객체 자체를 반환하는 역할을 한다.


tap($user)new HigherOrderTapProxy($user)를 반환하므로 tap($user)->update()는 (new HigherOrderTapProxy($user))->update()와 같다. HigherOrderTapProxy에 update 메소드가 없으므로 __call에서 처리된다. $this->target은 $user이고, $method는 ‘update’, $parameters는 [[‘name’ => $name, ‘age’ => $age]] 이므로


$this->target->{$method}(...$parameters);

return $this->target;

는 아래와 같은 뜻이 된다.


$user->update([
'name' => $name,
'age' => $age
]);

return $user;

아까 봤던 익숙한 코드가 나왔다 ^^


마치며


이번 글에서는 tap 메소드의 사용법을 알아보고, 어떻게 구현된 것인지 코드를 살펴봤다. 코드 자체는 매우 짧지만 초보자들이 읽기엔 조금 어려웠을 수 있을 것 같다. 그래서 관련된 PHP 문법과 기능인 __call 매직 메소드, 가변 길이 인수 목록에 대해서도 간단히 알아보았다.


===

오늘은 메 발송이 좀 늦었습니다. 죄송합니다. ㅠㅠ

부터는 늦지 않게 더 신경쓰겠습니다!

1 1 벨 6호

2019년 7월 8 




이현석

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