테일러 오트웰이 사랑한 헬퍼, 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 



유료 구독자 전용 레터입니다.

한 달 1만원으로 매일 라라벨 관련 메일 받아보시고 과거 메일도 열람하세요. 일반 구독으로 공개글만 받아보실 수도 있습니다.

구독하기 버튼을 눌러주시면 구독과 동시에 xly에도 가입됩니다.

이현석

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