최근에 트위터에서 final by default라는 주제에 대한 흥미로운 논쟁이 있었습니다. 클래스를 작성할 때 final로 선언하는 걸 기본으로 하는게 좋냐 그렇지 않냐는 문제입니다. 제가 흥미를 가진 시점은 라라벨의 직원인 드라이스 빈츠가 final로 선언하는게 좋다고 주장한 트윗을 봤을 때 입니다.
When writing packages, consider making classes final by default. This will help you make internal changes more easily in minor and patch releases.
패키지를 작성할 때는 클래스를 final로 만드는 걸 기본으로 해라. 그러면 마이너나 패치 릴리즈 때 내부를 쉽게 바꿀 수 있을 것이다.
그러자 누군가 테일러 오트웰은 반대의 의견을 가지고 있다는 것을 캡쳐해서 댓글로 답니다. 이에 대해 의견이 달라도 서로 존중하고 함께 일하는게 멋진 걸 만드는 최고의 방법 아니겠냐며 쿨하게 대답했습니다. 엄청 멋있네요 ㅎㅎ
테일러 오트웰이 댓글을 달았던 글은 스파티의 브렌트 루즈(Brent Roose)가 final by default를 주제로 설문을 한 글입니다. 현재 설문은 종료되었는데 최종적으로 655명이 투표를 했고 “final을 기본으로 한다”가 28%, “final을 기본으로 하지 않는다”가 72% 나왔습니다. 훨씬 더 많은 개발자가 final을 기본으로 삼지 않는 입장이었습니다.
저는 아직 쪼랩이라 뭐가 맞는지 잘 모르겠어요 :) 댓글 중에는 맥락이 고려되지 않은 것 같다며 글을 하나 추천하는 댓글이 있었습니다. 클래스에 final을 선언해야할 때는 언제인가?라는 글입니다. 오늘은 이 글을 간략히 정리해봤습니다.
가능하다면 언제나.
기존 솔루션의 서브 클래스를 만들어서 문제를 해결하려는 나쁜 버릇을 가진 개발자가 많다.
class Db { /* ... */ }
class Core extends Db { /* ... */ }
class User extends Core { /* ... */ }
class Admin extends User { /* ... */ }
class Bot extends Admin { /* ... */ }
class BotThatDoesSpecialThings extends Bot { /* ... */ }
class PatchedBot extends BotThatDoesSpecialThings { /* ... */ }
OOP를 잘못 이해한 개발자들이 주로 이런 접근을 취한다고..
상속을 강제로 막으면 구성으로 눈이 돌아가기 쉽다.
class RegistrationService implements RegistrationServiceInterface
{
public function registerUser(/* ... */) { /* ... */ }
}
class EmailingRegistrationService extends RegistrationService
{
public function registerUser(/* ... */)
{
$user = parent::registerUser(/* ... */);
$this->sendTheRegistrationMail($user);
return $user;
}
// ...
}
위의 예제에서는 가입한 유저에게 이메일을 보내는 기능을 추가하기 위해 RegistrationService
를 확장(상속)했다.
RegistrationService
클래스를 final
로 지정하면 이를 상속받아 EmailingRegsitrationService
를 만드는 실수를 손쉽게 방지할 수 있다.
final class EmailingRegistrationService implements RegistrationServiceInterface
{
public function __construct(RegistrationServiceInterface $mainRegistrationService)
{
$this->mainRegistrationService = $mainRegistrationService;
}
public function registerUser(/* ... */)
{
$user = $this->mainRegistrationService->registerUser (/* ... */);
$this->sendTheRegistrationMail($user);
return $user;
}
// ...
}
위의 코드는 RegistrationService
를 상속받는 대신 주입 받아서 사용한다. EmailingRegistrationService
는 RegistrationService
의 코드를 재활용하지만 상속 대신 인터페이스를 활용했다. OOP의 격언인 “상속보다 구성”에 딱 맞는 사례이다.
기존 클래스에 접근자나 API를 추가할 때 상속을 쓰려는 경향이 있다.
class RegistrationService implements RegistrationServiceInterface
{
protected $db;
public function __construct(DbConnectionInterface $db)
{
$this->db = $db;
}
public function registerUser(/* ... */)
{
// ...
$this->db->insert($userData);
// ...
}
}
class SwitchableDbRegistrationService extends RegistrationService
{
public function setDb(DbConnectionInterface $db)
{
$this->db = $db;
}
}
원문에서 설명한 결함들이 있는데 솔직히 잘 해석이 안됐습니다 ㅠ 일단, 위의 코드는 SOLID 원칙 중 리스코프 치환 원칙을 위반하고 있습니다. RegistrationService
에는 setDB()
메서드가 없기 때문에 이를 사용하기 위해서는 SwitchableDbRegsitrationServic
에 강하게 결합되어야 합니다. RegistrationServiceInterface
에 의존할 수 없기 때문에 SwitchableDbRegistrationServic
가 RegistrationService
의 하위 타입임에도 불구하고 치환할 수 없습니다.
단일 책임 원칙을 지킨답시고 많은 메서드를 가진 클래스를 상속 받아 특정 API를 오버라이드 하려는 경우가 있다. 모든걸 final로 만들다보면 새 API에 신중해지고, 되도록 작게 유지하도록 유도한다.
extends
는 캡슐화를 깨뜨린다final
을 반대하는 주된 의견은 유연성을 저해한다는 것인데, 그 유연성은 필요가 없다. 상속 대신 구성을 더 우선적으로 고려해라. 계속 final
을 제거해야하면 악취가 나는 코드가 포함되었기 때문일 수 있다.
final
로 클래스를 만들고 나면, 캡슐화가 보장되어 공개 API만 신경쓰면된다.
final
을 쓰지 말아야 할까파이널 클래스는 다음의 가정 하에서만 제대로 작동한다.
두 조건이 모두 만족하지 않으면 클래스를 확장가능하게 만들어야 한다.
===
8월의 첫 1일 1식 라라벨이 시작됐습니다~ 구독 신청해주신 모든 분들께 진심으로 감사드립니다. 7월호도 볼 수 있으면 좋겠다고 의견을 주신 분이 있었습니다. 1일 1식 라라벨에서는 다양한 신규 패키지들을 소개하는데, 이를 실제로 적용할만한 프로젝트가 있으면 좋겠단 생각이 들더라고요. 그래서 겸사겸사 조금씩 1일 1식 라라벨용 웹사이트를 만들어 볼까해요. 이 웹사이트를 통해 구독자들은 지난 글들도 볼 수 있도록 해보겠습니다. 금방 되진 않겠지만 암튼 :) 인내심을 갖고 기다려주세요~
1일 1식 라라벨 24호
2019년 8월 1일
메쉬 코리아 개발자. 바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 처음부터 제대로 배우는 라라벨(Laravel Up & Running 2nd Edition)을 번역했습니다.