클래스에 final을 선언해야할 때는 언제인가? free

2019-08-01

클래스에 final을 선언해야할 때는 언제인가?


최근에 트위터에서 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로 만드는 걸 기본으로 해. 그러면 마이너나 패치 릴리즈 때 내부를 쉽게 바꿀 수 있을 것이다.



그러자 누군가 테러 오트웰은 반대의 의견을 가지고 있다는 것을 캡쳐해서 댓글로 답니다. 이에 대해 의견이 달도 서로 존중하고 함께 하는게 멋진 걸 만드는 최고의 방법 아니겠냐며 쿨하게 대답했습니다. 엄청 멋있네요 ㅎㅎ


EArRP3mU4AARF-h.jpeg


러 오트웰이 댓글을 달았던 글은 스파티의 브렌트 루즈(Brent Roose)가 final by default를 주제로 설문을 한 글입니다. 현재 설문은 종료되었는데 최종적으로 655명이 투표를 했고 “final을 기본으로 한다”가 28%, “final을 기본으로 하지 않는다”가 72% 나왔습니다. 훨씬 더 많은 개발자가 final을 기본으로 삼지 않는 입장이었습니다.


저는 아직 쪼랩이 뭐가 맞는지 잘 모르겠어요 :) 댓글 중에는 맥락이 고려되지 않은 것 같다며 글을 하나 추천하는 댓글이 있었습니다. 클래스에 final을 선언해야할 때는 언제인가?는 글입니다. 오늘은 이 글을 간략히 정리해봤습니다.


“final”을 써야할 때는 언제인가?


가능하다면 언제나.


왜 final을 써야 하는가?


1. 파멸의 연쇄 상속 방지 (Preventing massive inheritance chain of doom)


기존 솔루션의 서브 클래스를 만들어서 문제를 해결하려는 나쁜 버릇을 가진 개발자가 많다.


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를 잘못 이해한 개발자들이 주로 이런 접근을 취한다고..


2. 구성을 쓰게끔 한다 (Encouraging composition)


상속을 강제로 막으면 구성으로 눈이 돌아가기 쉽다.


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를 상속받는 대신 주입 받아서 사용한다. EmailingRegistrationServiceRegistrationService의 코드를 재활용하지만 상속 대신 인터페이스를 활용했다. OOP의 격언인 “상속보다 구성”에 딱 맞는 사례이다.


3. 사용자 공개 API에 대해 생각하도록 한다


기존 클래스에 접근자나 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() 메서드가 없기 때문에 이를 사용하기 위해서는 SwitchableDbRegsitrationService에 강하게 결합되어야 합니다. RegistrationServiceInterface에 의존할 수 없기 때문에 SwitchableDbRegistrationServiceRegistrationService의 하위 타입임에도 불구하고 치환할 수 없습니다.


4. 객체의 공개 API를 줄이게 한다


책임 원칙을 지킨답시고 많은 메서드를 가진 클래스를 상속 받아 특정 API를 오버이드 하려는 경우가 있다. 모든걸 final로 만들다보면 새 API에 신중해지고, 되도록 작게 유지하도록 유도한다.


5. final은 나중에 언제든 풀 수 있다


6. extends는 캡슐화를 깨뜨린다


7. 유연성은 사실 필요치 않다


final을 반대하는 주된 의견은 유연성을 저해한다는 것인데, 그 유연성은 필요가 없다. 상속 대신 구성을 더 우선적으로 고려해. 계속 final을 제거해야하면 악취가 나는 코드가 포함되었기 때문 수 있다.


8. 자유롭게 코드를 변경할 수 있다


final로 클래스를 만들고 나면, 캡슐화가 보장되어 공개 API만 신경쓰면된다.


언제 final을 쓰지 말아야 할까


파이널 클래스는 다음의 가정 하에서만 제대로 작동한다.



  1. 클래스가 구현하는 추상화(인터페이스)가 있다.

  2. 파이널 클래스의 모든 공개 API가 인터페이스를 구현한 것이다.


두 조건이 모두 만족하지 않으면 클래스를 확장가능하게 만들어야 한다.

===

8월의 첫 1 1 벨이 시작됐습니다~ 구독 신청해주신 모든 분들께 진심으로 감사드립니다. 7월호도 볼 수 있으면 좋겠다고 의견을 주신 분이 있었습니다. 1 1 벨에서는 다양한 신규 패키지들을 소개하는데, 이를 실제로 적용할만한 프로젝트가 있으면 좋겠단 생각이 들더고요. 그래서 겸사겸사 조금씩 1 1 벨용 웹사이트를 만들어 볼까해요. 이 웹사이트를 통해 구독자들은 지난 글들도 볼 수 있도록 해보겠습니다. 금방 되진 않겠지만 암튼 :) 인내심을 갖고 기다려주세요~

1 1 벨 24호
2019년 8월 1 







이현석

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