PHP용 json processor - jsonmapper
야크 털 깍기를 하다가 알게 된 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 인데 이 내용만 봐서는 다음 질문에 답하기가 어렵습니다.
- 필수 입력 항목은?
- duedate 에 허용 가능한 날자 형식은?
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",
}
위 내용을 확인하려면 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."
}
}
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 항목이 없거나 유효한 시간 값이 아닙니다");
}
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;
}
사용은 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);
JsonMapper는 PHP 용 json mapper 로 Java 의 Jackson 라이브러리 처럼 annotation 을 기반으로 편리하게 Json 을 다룰 수 있게 해주는 유용한 라이브러리로 docblock annotations 으로 데이타를 기술하면서 json 의 데이타 구조도 문서화를 할 수 있는 추가 장점이 있습니다.
설치
Pear 로도 설치할 수 있지만 composer 가 더 편리하며 두 가지 방법이 있습니다.
명령행에서 설치
composer require netresearch/jsonmapper
CODEcomposer.json 에 지정
{ "require": { "netresearch/jsonmapper": "~0.11" }, }
CODE설정이 완료되었으면 update 명령어로 변경 내역을 반영합니다.
composer update
CODE
사용
- Register an autoloader that can load PSR-0 compatible classes.
- Create a
JsonMapper
object instance - Call the
map
ormapArray
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
}
}
클래스 멤버로 다른 클래스를 포함할 경우 클래스의 네임스페이스를 기술(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);
결과
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"
}
}
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);
결과
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"
}
}
}
}
Property
@var
@var $type 형식으로 public 멤버에 기술하면 매핑
/**
* Full name
* @var string
*/
public $name;
type은 string, array, int 등 다양한 형식 지원
Array
phpdoc 에 [] 로 array 기술
/**
* @var int[]
*/
public $nums;
/**
* @var Address[]
*/
public $addresses;
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 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
또는 클로저를 작성하고 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()
);
Missing properties
필수 항목은 @required annotation 으로 지정 가능
/**
* @var string
* @required
*/
public $name;
bExceptionOnMissingData 프로퍼티를 true 로 설정할 경우
$jm = new JsonMapper();
$jm->bExceptionOnMissingData = true;
$jm->map(json_decode($jsonContact), new Contact());
결과
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
단점
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 로 구현한 프로젝트입니다.
같이 보기
- JSon Schema 의 PHP 구현물 - https://github.com/justinrainbow/json-schema
- JsonMapper 프로젝트 github - https://github.com/cweiske/jsonmapper
- Java Jackson annotation - https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations