PHP로 HTTP 서버 구현하기 - 06 - Request 분리, autoload, PHPUnit

연재글 전체 보기


Request 분리

autoload

우선 클라이언트로부터 받은 request 메시를 분석하는 클래스부터 만들기로 했다.

Project root에 libs 디렉토리를 만들어 놓고, 여기에 Request라는 클래스를 하나 만들어야겠다.

그럼 autoload도 적용해야 하니 composer를 이용해야겠다.

대충 composer init해서 파일을 만들고, libs 디렉토리를 psr-4 기반으로 autoload 하도록 설정해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "youngiggy/phttp",
"type": "project",
"license": "MIT",
"authors": [
{
"name": "youngiggy",
"email": "youngiggy@gmail.com"
}
],
"require": {},
"autoload": {
"psr-4": { "": "libs/" }
}
}

이제 libs 디렉토리 아래 Request 클래스를 하나 만들고

1
2
3
4
5
6
<?php

class Request
{

}

after composer init

server.php 상단에 autoload용 코드를 심고, Request를 로드해보자.

1
2
3
4
5
6
7
8
9
10
11
12
<?php
require __DIR__ . '/../vendor/autoload.php';

//todo 지울 것 s
$req = new Request();
echo ($req instanceof Request);
exit;
//todo 지울 것 e

$server = stream_socket_server("tcp://127.0.0.1:1337", $errno, $errorMessage);

// ...생략...

일단 autoload는 잘 동작한다. OK.

server.php

테스트용으로 넣었던 쓸데 없는 코드는 이제 지우고,

Request 클래스에 사용자 입력을 분석하고 응답을 만들어내는 역할을 맡기려고 한다.

서버 소스는 아래와 같이 바뀌었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
require __DIR__ . '/../vendor/autoload.php';

defined('PROJECT_ROOT') or define('PROJECT_ROOT', __DIR__ . '/..');

$server = stream_socket_server("tcp://127.0.0.1:1337", $errno, $errorMessage);

if ($server === false) {
throw new UnexpectedValueException("Could not bind to socket: $errorMessage");
}

while (true) {
$client = @stream_socket_accept($server);

if ($client) {
$clientSentData = fread($client, 1024);

$response = (new Request($clientSentData))->getResponse();

fwrite($client, $response, strlen($response));
fclose($client);
}
}

이전 소스에서 index.html을 읽기 위해 아래와 같이 상대 위치로 하드코딩해서 썼었다.

1
2
3
4
//...생략...
$filename = '../public/index.html';
if (!($fp = fopen($filename, 'r'))) {
//...생략...

처리 로직이 서버 소스를 떠나면 index.html 파일을 여는 fopen을 어디서 처리할 지 아직 확신이 없다. 계속 바뀔지 모른다.

그래서 프로젝트의 최상위 경로를 PROJECT_ROOT라는 상수로 지정했다.

그리고 주요 처리 로직을 들어내고 Request 클래스에 클라이언트가 보낸 문자열을 전달하고,

getResponse() 메소드로 받은 문자열을 그대로 클라이언트에 전달한다.

이제 당분간 이쪽 소스를 건드릴 일은 없어 보인다.

Request.php

이 클래스를 사용하는 곳(server.php)을 보면 Request 클래스의 역할은 크게 두가지다.

  1. 클라이언트가 보낸 문자열을 생성자로 받는 것
  2. getResponse() 메소드로 처리 결과를 리턴하는 것

server.php의 소스를 가능한 건드리지 않고 조심히 들고와보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

class Request
{
private $requestMessage;

public function __construct($requestMessage)
{
//하지만 아직 쓰진 않지
$this->requestMessage = $requestMessage;
}

public function getResponse()
{
$filename = PROJECT_ROOT . '/public/index.html';
if (!($fp = fopen($filename, 'r'))) {
return 'HTTP/1.0 404 파일 없음' . PHP_EOL;
}
$responseBody = fread($fp, filesize($filename));

$response = 'HTTP/1.0 200 OK' . PHP_EOL;
$response .= 'Content-Type: text/html' . PHP_EOL;
$response .= PHP_EOL;
$response .= $responseBody . PHP_EOL;

return $response;
}
}

거의 동일하다.

어려울 게 없으니 서버 실행!

테스트

이전의 파일 하나짜리 서버와 동일하게 동작한다.

잘 돌아는 가는데 어쩐지 마음이 헛헛하다.
(가을인가)

계속 이렇게 테스트를 해야하나?

매번 뭔가 수정하고 php ./app/server.php를 다시 실행해야 하나?

이제 개발 시작할 분석 로직은 그냥 스트링을 던져주고 결과만 잘 나오면 되는데.

소파에 반쯤 누워 맥주 한잔 들이키며 이 막장 드라마를 구경하는 중급 개발자라면 ‘유닛 테스트를 걸라고, 멍청아’라고 소리칠 것만 같다.

넣어주지 뭐.

PHPUnit

PHPUnit을 가져와야 할텐데, composer.json에 추가하기 보다는 composer require을 사용하기로 한다.

콘솔을 열고

composer require --dev phpunit/phpunit ^6.4

  • “개발에서 쓰려고 하는데 대충 6.4 정도면 될 것 같아요”

최신 버전이 6.4라서 명시했고, 7.0 전까지는 써도 별 탈 없을 것이다.

업데이트가 완료되면 PHPStorm에 실행 환경을 설정한다.

테스트 범위 설정이나 그룹화를 위해 XML로 설정하기로 한다.

프로젝트 root에 phpunit.xml 파일을 만들고 아래와 같이 입력.

1
2
3
4
5
6
7
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Request">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

테스트를 돌리기 전 특별히 bootstrap할 게 없으므로 vendor/autoload.php를 bootstrap에 명시했고,

testsuite 명은 나중에 바뀌겠지만 일단 대충 넣고,

tests라는 디렉토리에 있는 Test.php로 끝나는 파일(예: parseTest.php)을 모두 검사하도록 했다.

PHPUnit with PHPStorm

다른 에디터를 쓰는 분들은 다른 각자 알아서 찾아보면 될 것이고, 여기서는 간단히 PHPStorm에서 설정하는 법만 적어본다.

Run | Edit Configurations 메뉴에서 + 버튼을 눌러 PHPUnit 선택.

Run > Edit Configurations

Test Runner > Test Scope을 `Definded in the configuration file’을 선택한다.

바로 아랫줄 제일 오른쪽에 보이는 설정 버튼을 누르면 Test Frameworks 설정 창이 나온다.
(만약 처음 설정/실행한다면 창 아래쪽에서 configuration file에 문제가 있으니 수정하라고 Fix 버튼이 보일 수도 있다. 이걸 눌러도 같은 창이 나온다.)

Run > Edit Configurations > Test Frameworks

+ 버튼을 눌러 Configuration type을 PHPUnit Local로 추가한 후,

PHPUnit library > Use Composer autoloader 선택하고 Path to script는 버튼을 눌러 자신의 vendor/autoload.php을 선택하면 된다.

그 아래 Test Runner 옵션에서는 Default configuration file을 체크하고 앞서 만든 phpunit.xml을 선택한다.

그리고 실행!

테스트를 작성하지 않았으므로 No tests executed! 같은 메시지가 보이면 정상이다.

실행창에서 Toggle auto-test를 눌러 놓으면 뭔가 수정이 될 때마다 테스트가 실행된다.

하지만 개발하다보면, 주석을 넣는다거나 공백을 삽입하는데도 test가 실행되므로 종종 눈엣가시가 된다.

이럴 때는 AutoTest Delay값을 늘려주면 좀 낫다.

Run > Set AutoTest Delay

나중에 테스트가 엄청 많아지면 현재 개발하는 모듈 이외의 클래스도 단위테스트가 돌게 될텐데, 이 때는 phpunit.xml에서 테스트 단위를 나누면 된다. 하지만 예상컨대 이 프로젝트에선 그렇게까지 테스트가 많아질 것 같지 않다.

결론

이제부터는 뭔가 추가/수정할 때마다 미리 테스트를 만들고 실패하는 것을 확인하고 이를 만족하는 기능을 개발하게 될 것이다.

현재 추가/수정하는 코드가 시스템을 망가뜨리지 않는다는 최소한의 보장을 받을 수 있다.

하지만 서버 코드의 많은 부분이 file이나 directory를 읽어야 하는데, 이런 부분은 unit test 만으로 해결 안될 수 있다.

뭐가 어려운지는 이런 글을 보고 예습을 하는 것도 좋다.

참고

http://www.php-fig.org/psr/psr-4/
https://getcomposer.org/doc/01-basic-usage.md
http://xpressengine.github.io/Composer-korean-docs/
https://getcomposer.org/doc/articles/versions.md#caret-version-range-
https://phpunit.de/index.html
https://www.jetbrains.com/help/phpstorm/testing-with-phpunit.html
http://jwchung.github.io/testing-oh-my