최근 아키텍처에 관한 책을 읽고 있는데 레퍼런스로 나온 글 중 하나로 Hadi Hariri의 글 Refactoring to Functional–Why Class?을 번역했다. 이 글은 함수형으로 리펙토링하기라는 코틀린 연재 중 일부라서 그다지 공정한 느낌으로 쓰여진 글은 아니지만 객체지향이라는 패러다임에서 논쟁점이 되는 여러 부분을 잘 보여주고 있어 옮겨봤다.


함수형으로 리팩토링하기 – 왜 클래스죠?

대학에서

교수: 우린 실제 세계에서 객체로 둘러쌓여 있습니다. 이 객체는 자동차, 집, 기타 등등이 될 수 있죠. 그런 이유에서 객체 지향 프로그래밍에서 클래스를 통해 실제 세계에 존재하는 객체를 연결하는 방식이 매우 쉬운 이유입니다.

2주 후

제이크: 저 이 객체와 문제가 좀 있는데요. 도와주시겠어요?

교수: 물론이죠. 객체를 만드는데 도움이 되는 여러 일반적인 방법이 있는데 요약하자면 명사를 찾아요. 그리고 동사를 찾으면 클래스에서 사용할 수 있는 메소드가 될 수 있어요. 말하는 그대로죠.

제이크: 어 말씀한 내용이 합당하네요. 감사합니다.

신입 제이크

: 제이크 씨, 당신이 작성한 클래스를 확인했습니다. 좀 크기가 큰 것 같은데요.

제이크: 죄송합니다. 어떤 부분이 문제죠?

: 음… 너무 많은 책임을 갖는 게 문제에요. 너무 많은 일을 합니다.

제이크: 그리고요?

: 잘 생각해보세요. 하나에 너무 많은 책임이 있으면 이 부분 하나가 시스템의 많은 부분과 연결되어 있다는 뜻이에요. 즉 이 클래스를 변경할 가능성도 상당히 높다는 뜻이고 그건 무언가를 고장내게 될 가능성 또한 높다는 의미죠. 거기다 단일 클래스를 1000줄이 넘도록 작성하면 물론 30줄 짜리 코드에 비해 이해하기 어려울 것이고요.

제이크: 맞는 말이네요.

: 이 코드를 작은 클래스로 나누세요. 각각의 클래스는 한 가지 일만 하고 그 클래스 홀로 쓰여야 합니다.

1년 후

메리: 제이크 씨, 방금 당신이 작성한 클래스를 확인했는데요. 그다지 행동(behavior)이 많이 들어있지 않네요.

제이크: 네, 동작이 Customer 클래스에 속하는지 Accounts 클래스에 포함해야 하는지 확신이 없어서 CustomerService라는 클래스를 별도로 만들어 거기에 넣었습니다.

메리: 네, 적당한 방법이네요. 하지만 Customer 클래스를 더 이상 클래스라고 보기 어려워졌어요. DTO에 더 가까워요.

제이크: DTO요?

메리: 네, 데이터 전달 객체(Data Transfer Object)요. 클래스와 비슷하긴 하지만 행동이 없는 경우에요.

제이크: 음, 그럼 구조체나 레코드에 가깝다는 말씀이시죠?

메리: 네, 그런 느낌이에요. 클래스를 만들 때는 행동이 있어야 해요. 그러지 않고서는 클래스라고 하기 어려워요. DTO죠.

제이크: 알겠습니다.

2년 후

메튜: 제이크 씨, 이 클래스를 봤는데 특정 구현과 결합(coupled)이 상당히 강하군요.

제이크: 네?

메튜: 음, 지금 RepositoryController 내에서 생성하고 있어요. 이 부분은 어떻게 테스트하시겠어요.

제이크: 음… 시험용 데이터베이스를 사용하면 되지 않을까요?

메튜: 아뇨. 가장 먼저 해야 하는 부분은 프로그램을 클래스가 아닌 인터페이스를 사용하도록 하는 겁니다. 이 접근 방식이 특정 구현에 매여 있지 않은 코드를 장성하는 방법이에요. 그런 후에 의존성 주입을 사용해서 특정 구현을 전달해 사용하도록 하는겁니다. 그러면 구현을 언제든지 필요할 때 변경할 수 있게 되는 거죠.

제이크: 그렇군요.

메튜: 실무에서는 IoC 컨테이너를 사용해서 다른 클래스의 인스턴스를 연결하는 것이 가능할겁니다.

3년 후

프랜시스: 제이크 씨, 이 클래스에 너무 많은 의존성을 집어넣고 있군요.

제이크: 네, 그래도 IoC 컨테이너가 다 처리할겁니다.

프랜시스: 네, 저도 알고 있습니다. 하지만 가능하다고 해서 옳은 방법이라고 말하기는 어렵네요. 이 클래스는 여러 종류의 구현체를 사용할 수 있다고 하더라도 여전히 너무 많은 다른 클래스에 의존하고 있어요. 하나에서 최대 3개로 유지하도록 해요.

제이크: 네, 알겠습니다. 감사합니다.

4년 후

안나: 제이크 씨, 이 클래스 이름은 왜 Utils인가요?

제이크: 음. 그 코드는 정말 어디에 놔야 할 지 알 수 없어서 그렇게 이름 붙였어요.

안나: 그래요. 이미 그런 코드를 위한 클래스가 있어요. RandomStuff라는 이름이에요.

맥주 마시며

제이크: 피터, 내가 생각해봤는데 말이지. 학교에서 배울 땐 객체로 생각하고 명사를 분석하라는 등 기법을 얘기했는데 말야. 그러고 나서는 이름을 잘 붙였는지, 작게 작성했는지, 단일 책임으로 작성했는지, 너무 많은 의존성을 주입하고 있는 것은 아닌지 생각해야 한단 말이야. 이제 와서는 동시성에 좋지 않다고 상태를 갖지 않는 코드를 작성해야 한다고 말하지. 처음부터 궁금했는데 이럴거면 도대체 왜 클래스를 사용하는 걸까?

피터: 헛소리 하지 마 제이크. 만약 클래스가 없다면 어디에 함수를 선언할 수 있겠어? 맥주나 한 잔 더 마실래?

다음 시간에 계속.

객체 지향 프로그래밍에 익숙한 개발자라면 하나의 파일에 하나의 클래스를 작성하는 방식에 익숙할 것이다. 다만 php는 다른 언어와 같이 라이브러리를 일괄적으로 불러오는 방법이 없어 위와 같은 접근 방법으로는 require 또는 include를 이용해 수많은 단일 파일을 불러들여야만 했었다.

PHP5에서는 클래스 또는 인터페이스 등을 호출했을 때 해당 파일을 자동으로 불러올 수 있도록 여러 함수를 제공한다. 먼저 __autoload 함수를 이용한 예제다.

<?php
function __autoload($className){
  include $className . '.php';
}

$foo = new Foo();
$bar = new Bar();
?>

위와 같이 함수를 선언하면 new Foo()와 같이 클래스를 사용하는 순간 해당 클래스명으로 __autoload 함수가 실행, Foo.php 파일을 include한다.

다만 __autoload 함수는 spl_autoload_register 함수를 통해 대체될 수 있기 때문에 권장되지 않는다. spl_autoload_register는 다음과 같이 사용한다.

<?php
function my_autoloader($className){
  include 'classes/' . $className . '.class.php';
}

spl_autoload_register('my_autoloader');
?>

PSR-0 Autoloading Standard

위 함수를 통해 모듈화가 가능하도록 PHP Framework Interop Group(PHP-FIG)에서 PSR-0 Autoloading Standard가 제안되었다. 해당 제안은 다음의 규약을 포함하고 있다.

  • 네임스페이스와 클래스명으로의 자격을 갖추기 위해서는 다음의 구조를 따라야 한다. \<Vendor Name>\(<Namespace>\)*<Class Name>
  • 각 네임스페이스는 최상위 네임스페이스를 가져야 한다. (“Vendor Name”)
  • 각 네임스페이스는 필요에 따라 서브 네임스페이스를 가질 수 있다.
  • 각 네임스페이스 구분자는 파일 시스템에서 해당 파일을 불러오기 위한 디렉토리 구분자로 사용된다.
  • 클래스명에 들어있는 _ 글자도 디렉토리 구분자로 사용된다. 네임스페이스에서의 _는 특별한 의미가 없다.(PEAR 구현을 포함)
  • 완전한 네임스페이스와 클래스는 파일 시스템에서 불러올 때 .php를 접미어로 붙여 불러온다.
  • 알파벳으로 구성된 벤더명, 네임스페이스, 클래스명은 대소문자를 구분한다.

위 규약에 따른 예제는 PSR-0 문서에서 제공되고 있으며 SplClassLoader 구현도 해당 문서에서 확인할 수 있다.

다음은 문서에서 제공되는 autoload 함수 예제다.

<?php
function autoload($className) {
  $className = ltrim($className, '\\');
  $fileName = '';
  $namespace = '';
  if($lastNsPos = strpos($className, '\\')) {
    $namespace = substr($className, 0, $lastNsPos);
    $className = substr($className, $lastNsPos + 1);
    $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
  }
  $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
  require $fileName;
  }
}
?>

Composer 활용하기

Composer는 PSR-4 Autoloader 제안과 함께 위에서 살펴본 PSR-0를 준수하고 있어서 간단한 설정으로 PSR-0 방식을 사용할 수 있다. composer.json에 autoload 경로를 등록하면 composer의 ClassLoader와 맵핑되어 자동으로 불러온다.

{
  ...
  "autoload": {
    "psr-0": {"": "<path>/"}
  },
  ...
}

위 내용을 추가한 후 composer update 등을 통해 갱신하면 vendor/composer/autoload_namespaces.php 파일 안에 composer.json에서 작성한 경로가 추가된 것을 확인할 수 있다.

더 읽을 거리

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.