PHP로 HTTP 서버 구현하기 - 07 - 첫 테스트, Response 분리

연재글 전체 보기


테스트 주도로 시작하기

지금까지의 Request 분석 클래스는 아래와 같다.

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;
}
}

첫 테스트

의미 없는 코드지만 테스트가 존재하면 성공하는지나 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

use PHPUnit\Framework\TestCase;

class RequestTest extends TestCase
{
public function testCanBeCreated(): void
{
$this->assertInstanceOf(
Request::class,
new Request([])
);
}
}

결과는,

1
2
3
4
5
6
Testing started at PM 7:57 ...
PHPUnit 6.4.3 by Sebastian Bergmann and contributors.

Time: 59 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

이건 너무 당연한 테스트 코드라서 낭비다. 지워버리고 의미있는 테스트를 만들어보자.

다시 첫 테스트

server.php에서 Request.php으로 기능을 가져왔으니 뭔가 테스트할 거리가 있을 것 같다.

우선 __construct.

하는 일이라곤 생성자로 전달된 값을 private 변수에 담는 것 뿐인데, 이걸 테스트로 만들 필요가 있을까?

물론 테스트는 만들 수 있다.

굳이 굳이 Request 클래스에 private 변수를 꺼내올 수 있는 public method를 만들 수도 있고.

Request.php :

1
2
3
4
5
<?php
public function getRequestMessage()
{
return $this->requestMessage;
}

RequestTest.php :

1
2
3
4
5
6
7
8
9
10
11
12
public function testConstructByGetter() : void
{
$requestMessage = ['testKey' => 'testVal'];
$request = new Request($requestMessage);

$valueByGetter = $request->getRequestMessage();

$this->assertEquals(
serialize($requestMessage),
serialize($valueByGetter)
);
}

(하긴 막 짜려면 gettter도 필요없이 requestMessage 변수를 public으로 만들고 $request->requestMessage;처럼 막 갖다 써도..)

또한, 굳이 굳이 Reflection으로 private 변수를 들여다 볼 수 도 있다.

RequestTest.php :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function testConstructByReflection() : void
{
$requestMessage = ['testKey' => 'testVal'];
$request = new Request($requestMessage);
$reflectionClass = new \ReflectionClass($request);

$reflectionProperty = $reflectionClass->getProperty('requestMessage');
$reflectionProperty->setAccessible(true);
$privateVal = $reflectionProperty->getValue($request);

$this->assertEquals(
serialize($requestMessage),
serialize($privateVal)
);
}

다시 질문. 이걸 테스트로 만들 필요가 있을까?

생성자에 받은 녀석을 그대로 멤버 변수에 넣었는데 이게 잘 들어갔는지 확인할 필요는 없어보인다. 그 과정에서 이에 간섭하는 코드가 없기 때문이다.

그리고 private 변수의 접근 제한 수준을 억지로 공개해서 은닉성을 포기할 만큼 가치있는 일인가?

비슷한 관점으로 (그러나 명확하게 설명된) 이 글을 참고해도 좋겠다.

비공개 메서드를 테스트 해야 하는가?

테스트 케이스는 클라이언트 코드다

억지로 만든 테스트는 다 지워버리자.

역할 나누기

계속된 첫 테스트 작성 실패에 주눅들 것 없이 찬찬히 코드를 돌아보자.

진짜 진짜 첫 테스트를 작성 하려고 보니, Request 클래스 안에 getResponse() 메소드만 덩그러니 있는 것이 마음에 걸린다.

server.php에서 기능을 분리한 것은 좋았지만, 클래스 명이 Request인데 여기서 응답 문자열을 만들어주는 모양이 어색하다.

자연스럽게(서로 짝이 맞게) Response라는 클래스를 만들어야겠다는 생각이 든다.

어떤 기준으로 나눠야 할지 현재의 Request 클래스의 코드를 기준으로 각각의 임무를 적어보자.

Request

  • 요청된 문자열을 파싱해서 시작줄, 헤더(여러줄), 본문 등 필요한 포맷으로 정리한다
  • 해석할 수 없는 요청이라면 예외를 발생시킨다
  • 분석 결과, 클라이언트가 요청한 리소스 위치를 리턴한다
  • ‘/‘라는 요청이 오면 ‘index.html’이라는 파일을 읽어야 하는 것을 알고 있다

Response

  • 도큐먼트 root의 절대 경로를 알고 있다
  • 파일이 존재하는지 확인하고 없으면 예외를 발생한다
  • 파일을 읽는다
  • 처리 결과를 정리한다
  • 클라이언트에 전달할 HTTP 응답 메시지를 리턴한다

나중에 여기서 더 더 분리가 되겠지만, 현재 코드에서 예상할 수 있는 기능을 크게 둘로 나오면 이 정도라고 생각한다.

그럼 이런 흐름이 될 것이다.

  1. server.php에선 클라이언트가 보낸 raw 메시지를 Request 클래스에 전달하고,
  2. Request 클래스는 이를 파싱해서 내부에 저장하고,
  3. Response 클래스는 여기서 리소스 경로를 받아 읽고 server.php에 최종 결과물을 리턴한다.

이제 Request 클래스는 raw 메시지를 받아 특정 경로를 반환하도록 고치고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Request
{
private $requestMessage;

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

public function getResourcePath()
{
return '/index.html';
}
}

서버는 이걸 받아 Response에게 던지고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ... 생략 ...
while (true) {
$client = @stream_socket_accept($server);

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

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

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

Response는 클라이언트에 보낼 메시지를 조합한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Response
{
private $docRoot = PROJECT_ROOT . '/public';

public function getResponse($resourcePath)
{
$filename = $this->docRoot . $resourcePath;
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;
}
}

이제 브라우저에서 http://127.0.0.1:1337/에 접근해보고 문제없이 동작하는 것을 확인한다.

드디어 진짜 진짜 첫 테스트를 작성할 차례다.

진짜 진짜 첫 테스트

진짜 진짜 첫 테스트의 대상은 RequestgetResourcePath() 메소드로 정했다.

사실 TDD의 정석대로 하자면, 메소드를 추가하기 전에

  • Red : 실패하는 테스트를 작성하고, 실패하는 것을 확인
  • Green : 이를 통과하는 최소한의 코드를 작성하고
  • Refactor : 리팩토링하는 과정

을 겪어야 했지만, 이미 잘 돌아가는 코드가 존재했고 Request-Response로 분리하는 과정에서 믿을 수 있는 건 수동 테스트 밖에 없었기 때문에,

수동 테스트가 성공하는 범위 안에서 최소한의 변경으로 코드로 분리하고자 했다.

좀 늦긴 했지만 getResourcePath() 메소드에 대한 테스트를 작성해보자.

tests/RequestTest.php :

1
2
3
4
5
6
7
8
9
10
11
<?php
use PHPUnit\Framework\TestCase;

class RequestTest extends TestCase
{
public function testGetResourcePath(): void
{
$request = new Request([]);
$this->assertEquals('/index.html', $request->getResourcePath());
}
}

1
OK (1 test, 1 assertion)

결론

7화 만에 진짜 진짜 첫 테스트를 만들다니…

다음 시간에는 Request를 파싱해서 실제 동작하는(잊지 않았겠지! 클라이언트는 html과 image와 favicon을 요청할 것이다) HTTP 요청 메시지 파서를 만들 것이다. 당연히 최소한으로 동작하는..