야크 털 깍기를 하다가 알게 된 PHP 라이브러리인데 너무 유용해서 널리 소개하려고 사용기를 작성해 봅니다.

 

필요성

2013년부터 버전 관리 시스템으로 git 을 도입하고 gitlab 설치형 community version을 사내에 설치해서 사용중이었습니다.

gitlab은 잘 만들어진 좋은 솔루션이었지만 한 가지 아쉬웠던 점은 이슈 관리로 쓰고 있던 community version은 JIRA 와 연동이 안 되는 점이었습니다.

 

즉 JIRA 에 등록한 이슈와 관련된 push 가 일어나면 JIRA 의 해당 이슈에 gitlab 의 커밋 URL을 댓글로 연결을 해주고 커밋 메시지에 "fixed TEST-3" 같이 이슈와 관련된 키워드가 있을 경우 TEST-3 번 이슈를 닫아주는 기능이 필요했지만 gitlab Enterprise 버전만 가능했습니다.

 

커뮤니티 버전과 상용 버전의 기능차를 두는 것은 개발사 입장에서는 지극히 당연하며(개발사도 돈을 벌어야 다음 버전을 내 놓을 수 있겠죠) 저도 예산이 있었다면 상용을 구매하고 싶었지만 gitlab enterprise 를 구매할 수 있게 책정된 예산이 없었습니다.

이슈 관리 시스템에서 관련 커밋을 확인하는 기능은 subversion 때부터 즐겨 썼던 기능이고 개별 커밋을 이슈 관점에서 볼 수 있게 해주는 중요한 기능이라고 생각하므로 이 기능이 꼭 필요했습니다. (git-flow 의 feature 브랜치 이름을 JIRA의 이슈 키로 생성하고 해당 feature 의 커밋만 살펴보는 것도 비슷한 결과를 내긴 하지만 이슈 관리 시스템에서 보는 것을 더 선호합니다.)

 

그렇다고 내년까지 기다렸다가 구매하면 시간이 너무 오래 걸릴 것 같아서 직접 만들어서 사용하기로 했습니다.

 

빨리 만들어서 사용하려고 스크립트 언어를 사용하기로 했고 개발 언어는 PHP(ruby와 python 으로 할 능력이 안 됩니다.!) 로 정하고 PHP 로 JIRA 와 연계할 수 있는 라이브러리를 찾아보았습니다.

 

https://packagist.org/ 에서 찾아보니 여러 개가 나오는데 다 업데이트가 오래 되었고 사용이 불편해서 썩 마음에 들지 않아서 직접 JIRA 연계 라이브러리를 만들기로 했습니다.

 

원래 이런 "야크 털 깍기"는 피하고 "적당한거 찾아서 갖다 쓰자" 주의지만 마땅한 라이브러리가 없어서 어쩔수가 없었습니다.

 

JSON 의 단점

JIRA 는 버전 6부터 XML RPC 방식의 API 는 제공하지 않고 RESTFul 과 JSON 기반의 라이브러리만 제공하므로 JSON 처리가 빈번하게 발생합니다. (참고: https://docs.atlassian.com/jira/REST/6.4/)

 

JSON 은 무겁고 느리고 복잡한 XML 에 비해 가볍고 간단한 구조를 갖고 있으며 parser 를 만들기가 용이하며 네트워크를 통해 데이타 전송시 XML 기반의 SOAP 이나 RPC 에 대비해서 아주 빠른 장점이 있습니다.

 

하지만 이런 JSON 의 장점은 XML과 달리 스키마가 없어서 data validation 이 어렵고 문서화가 힘들다는 단점이 있습니다.

 

예로 다음은 JIRA의 이슈를 등록하는 JSON 인데 이 내용만 봐서는 다음 질문에 답하기가 어렵습니다.

  1. 필수 입력 항목은?
  2. duedate 에 허용 가능한 날자 형식은?
  3. issuetype Object에 사용할 수 있는 name/value 는? 

{
    "project": {
        "id": "10000"
    },
	"duedate": "2011-03-11",
    "summary": "something's wrong",
    "issuetype": {
        "id": "10000"
    },
    "assignee": {
        "name": "lesstif"
    },
    "labels": [
        "bugfix",
        "blitz_test"
    ],
    "description": "description",    
}
CODE

위 내용을 확인하려면 API Service Provider 의 문서를 확인하거나 아니면 직접 REST API 를 던져보며 테스트를 해 봐야 합니다.

PHP 에 내장된 JSON 데이타를 파싱하는 함수인 json_encode와 json_decode 는 간단하고 사용이 편리해서 위와 같이 API 를 사용하는 클라이언트를 구현하는 건 어렵지 않지만 데이타의 검증은 별도의 로직을 작성해야 합니다.

 

물론 잘 만든 REST API Provider 는 아래와 같이 잘못된 요청을 보냈을 경우 HTTP 400 에러와 함께 어떻게 처리해야 하는지를 알려 줍니다.

 Error Message : 
{
    "errorMessages": [],
    "errors": {
        "summary": "You must specify a summary of the issue.",
        "assignee": "Issues must be assigned."
    }
}
CODE

API 사용자라면 위와 같이 서버의 에러 메시지를 보면서 조치를 해도 되지만 만약 API 를 제공하는 서버를 구현한다고 가정해 보겠습니다.

 

$json = <<<JSONSTR
{
    "project" : "TEST",
    "summary" : "이슈 제목",
    "assignee": "user123",
    "duedate" : "2017-06-11 11:22:45"
}
JSONSTR;

$issue = json_decode($json, true);

// 비즈니스 로직 시작
if (empty($issue['summary'])) {
    print ("summary 항목은 필수입니다.");
}

if (empty($issue['assignee']) || FindUserByName($issue['assignee']) == null) {
    print ("assignee 항목이 없거나 사용자를 찾을수 없습니다.");
}

if (empty($issue['duedate']) || checkDateTimeValidation($issue['duedate']) === false) {
    print ("duedate 항목이 없거나 유효한 시간 값이 아닙니다");
}
CODE

JSON 내 항목이 추가/변경되거나 값이 바뀔 경우 위와 같이 비즈니스 로직을 매번 수정해야 하며 후임 개발자는 JSON 의 스키마를 확인해야 할 경우 비즈니스 로직의 소스 코드를 보고 이해해야 합니다.

 

해결책

위와 같은 문제는 JSON 데이타를 다음과 같은 클래스로 기술하고 annotation 을 사용하여 Property 의 특성과 필수 여부, 타입을 문서화하고 mapper library 는 annotation 을 확인해서 데이타의 검증까지 해주면 어느 정도 해결됩니다.

JSON 클래스

<?php

/**
 * JIRA 의 Issue 매핑하는 클래스.
 * @see https://docs.atlassian.com/jira/REST/6.4/#d2e4329
 */
class Issue
{
    /**
     * @var string
     * @required
     */
    private $project;

    /**
     * @var int
     * @required
     */
    private $id;

    /**
     * @var \DateTime
     */
    private $duedate;

    /**
     * @var string
     */
    private $summary;

    /**
     * @var string
     * @required
     */
    private $issuetype;
}
PHP

 

사용은 Json 과 매핑되는 객체를 생성하고 프로퍼티를 설정하면 바로 JSON 데이타로 Serialize하고 JSON 문자열을 DeSerialize 하여 객체를 생성하는 것이었습니다. 

$i = new Issue();
 
$i->summary = 'something's wrong';
$i->issuetype= '100000';
$i->assignee= 'lesstif';
$i->description= 'description';
 
 
// Json 문자열로 변환
$json = $i->toJsonString();
 
// 
$issue = $i->fromJsonString($json);
 
// lesstif 출력
print_r($issue->assignee);
PHP

 

 

JsonMapper는 PHP 용 json mapper 로 Java 의 Jackson 라이브러리 처럼 annotation 을 기반으로 편리하게 Json 을 다룰 수 있게 해주는 유용한 라이브러리로 docblock annotations 으로 데이타를 기술하면서 json 의 데이타 구조도 문서화를 할 수 있는 추가 장점이 있습니다.

 

설치

Pear 로도  설치할 수 있지만 composer 가 더 편리하며 두 가지 방법이 있습니다.

 

  1. 명령행에서 설치

    composer require netresearch/jsonmapper
    CODE
  2. composer.json 에 지정

    {
      "require": {
            "netresearch/jsonmapper": "~0.11"
      
        },
    }
    CODE
  3. 설정이 완료되었으면 update 명령어로 변경 내역을 반영합니다.

    composer update 
    CODE

사용 

  1. Register an autoloader that can load PSR-0 compatible classes.
  2. Create a JsonMapper object instance
  3. Call the map or mapArray method, depending on your data

예제 class

class Contact
{
    /**
     * Full name
     * @var string
     */
    public $name;

    /**
     * @var Address
     */
    public $address;
};

class Address
{
    public $street;
    public $city;

    public function getGeoCoords()
    {
        //do something with the $street and $city
    }
}
PHP

클래스 멤버로 다른 클래스를 포함할 경우 클래스의 네임스페이스를 기술(Ex: MyNameSpace\Address)

json 데이타를 class 객체로 매핑


$jsonContact = <<<JSONSTR
{
    "name":"Sheldon Cooper",
    "address": {
    "street": "2311 N. Los Robles Avenue",
        "city": "Pasadena"
    }
}
JSONSTR;
 
$mapper = new JsonMapper();
$contactObject = $mapper->map(
    json_decode($jsonContact), new Contact()
);

var_dump($contactObject);
CODE

 

결과

 

object(Contact)#5 (2) {
  ["name"]=>
  string(14) "Sheldon Cooper"
  ["address"]=>
  object(Address)#9 (2) {
    ["street"]=>
    string(25) "2311 N. Los Robles Avenue"
    ["city"]=>
    string(8) "Pasadena"
  }
}
CODE

 

json 데이타를 class array 로 매핑

 $jsonContacts = <<<JSONSTR
[
    {
        "name": "KwangSeob Jeong",
        "address": {
            "street": "namyangjoo",
            "city": "Gyonggi"
        }
    },
    {
        "name": "Sheldon Cooper",
        "address": {
            "street": "2311 N. Los Robles Avenue",
            "city": "Pasadena"
        }
    }
]
JSONSTR;

$mapper = new JsonMapper();
$contactsArray = $mapper->mapArray(
    json_decode($jsonContacts, false), new \ArrayObject(), 'Contact'
);

var_dump($contactsArray);
CODE

 

결과

object(ArrayObject)#7 (1) {
  ["storage":"ArrayObject":private]=>
  array(2) {
    [0]=>
    object(Contact)#8 (2) {
      ["name"]=>
      string(15) "KwangSeob Jeong"
      ["address"]=>
      object(Address)#12 (2) {
        ["street"]=>
        string(10) "namyangjoo"
        ["city"]=>
        string(7) "Gyonggi"
      }
    }
    [1]=>
    object(Contact)#9 (2) {
      ["name"]=>
      string(14) "Sheldon Cooper"
      ["address"]=>
      object(Address)#16 (2) {
        ["street"]=>
        string(25) "2311 N. Los Robles Avenue"
        ["city"]=>
        string(8) "Pasadena"
      }
    }
  }
}
CODE

Property 

 

@var

@var $type 형식으로 public 멤버에 기술하면 매핑

/**
     * Full name
     * @var string
     */
    public $name;
CODE

type은 string, array, int 등 다양한 형식 지원

 

Array

phpdoc 에 [] 로 array 기술

/**
 * @var int[]
 */
public $nums;

/**
 * @var Address[]
 */
public $addresses;
CODE

 

Handling invalid or missing data

Unknown properties

PHP 클래스에 프로퍼티가 없을 경우 JsonMapper 클래스의 bExceptionOnUndefinedProperty 를 true 로 설정하면 예외 발생가능

$jsonContact = <<<JSONSTR
{
    "name": "KwangSeob Jeong",
    "unknown_property" : "1234"
}
JSONSTR;
 
$jm = new JsonMapper();
$jm->bExceptionOnUndefinedProperty = true;
$contactObject = $jm->map(
    json_decode($jsonContact), new Contact()
);
PHP

 

결과

PHP Fatal error:  Uncaught JsonMapper_Exception: JSON property "unknown_property" does not exist in object of type Contact in vendor/netresearch/jsonmapper/src/JsonMapper.php:111
CODE

 

또는 클로저를 작성하고  undefinedPropertyHandler 로 지정 가능

/**
 * Handle undefined properties during JsonMapper::map()
 *
 * @param object $object    Object that is being filled
 * @param string $propName  Name of the unknown JSON property
 * @param mixed  $jsonValue JSON value of the property
 *
 * @return void
 */
function setUndefinedProperty($object, $propName, $jsonValue)
{
    $object->{'UNDEF' . $propName} = $jsonValue;
}

$jm = new JsonMapper();
$jm->undefinedPropertyHandler = 'setUndefinedProperty';
$contactObject = $jm->map(
    json_decode($jsonContact), new Contact()
);
PHP


Missing properties

필수 항목은 @required annotation 으로 지정 가능

/**
 * @var string
 * @required
 */
public $name;
CODE


bExceptionOnMissingData 프로퍼티를 true 로 설정할 경우

$jm = new JsonMapper();
$jm->bExceptionOnMissingData = true;
$jm->map(json_decode($jsonContact), new Contact());
CODE

 

결과

PHP Fatal error:  Uncaught JsonMapper_Exception: Required property "name" of class Contact is missing in JSON data in vendor/netresearch/jsonmapper/src/JsonMapper.php:257
CODE

 

단점

public 멤버 변수

JsonMapper 라이브러리의 문제는 아니고 PHP 언어 차원에서 annotation 을 지원하지 않으므로 JsonMapper 라이브러리는 PHPDoc 기반의 annotation 을 보고 클래스의 프로퍼티에 접근해야 합니다.

그러므로 모든 property 는 public 으로 선언해야 하며 private 일 경우 json mapper 에서 처리할 수 없으므로 public 으로 만들거나 별도의 setter 메서드를 구현해야 합니다

Model class 수작업 생성

JSON schema 등 schema 정보로부터 모델을 생성하는 기능이 없으므로 클래스는 수작업으로 작성해야 합니다. 

 

적용 프로젝트

php-jira-rest-client

유명한 이슈 및 프로젝트 관리 제품인 JIRA 의 REST API 구현 프로젝트로 이를 구현하려고 json mapper 라이브러리를 찾게 되었습니다.

OpsGenie-php

서비스 모니터링과 알림의 중요성을 설명한  "DevOps – 내일 새벽에는 누가 일어날까" 라는 글에서는 "Alert & On-Call" 을 제공하는 서비스인 Opsgenie 라는 서비스를 사용하고 있습니다.

이 서비스의 REST API룰 PHP 로 구현한 프로젝트입니다.

같이 보기