Laravel에서 PHPJasper로 JasperReports PDF 출력하기

12/21/2017 ·impact

Laravel에서 PHPJasper로 JasperReports PDF 출력하기

Laravel로 프로젝트를 하다 보면 화면이 아니라 인쇄용 리포트를 만들어야 할 때가 옵니다. 표 간격 하나 맞추려고 HTML과 CSS를 붙들고 있어도 원하는 모양이 잘 안 나옵니다. Java로 개발할 때 쓰던 JasperReports가 떠올라 PHP에서도 되는지 찾아봤더니, JasperReports jar를 명령어로 실행해 리포트를 만드는 방식이 가능했습니다. 이 글은 Laravel에 PHPJasper를 붙여 JSON 데이터를 PDF로 내보내는 과정을 정리한 것입니다.

왜 HTML 대신 JasperReports를 쓰는가

리포트는 웹 화면과 요구사항이 다릅니다. 여백, 페이지 나눔, 페이지마다 반복되는 머리글, 인쇄할 때 잘리는 문제까지 HTML로 하나씩 맞추려면 수작업이 계속 늘어납니다. JasperReports는 이런 출력물을 템플릿으로 미리 정의해두고 데이터만 갈아 끼우는 도구입니다. PHPJasper 저장소 README는 이 라이브러리를 PHP만으로 JasperReports의 .jrxml, .jasper 파일을 컴파일하고 처리하는 도구라고 설명합니다. 원문 메모를 쓸 당시 확장자를 .jxml로 적어뒀는데, README 기준 템플릿 확장자는 .jrxml입니다.

PHP가 맡는 일은 템플릿을 컴파일하고 데이터소스를 지정해 실행하는 데까지입니다. 실제로 레이아웃을 계산하고 PDF를 만드는 쪽은 Java입니다. Java로 같은 작업을 해본 적이 있다면 옮겨오는 데 오래 걸리지 않습니다. 예전에 Spring 4에서 JasperReports JsonDataSource로 PDF를 출력하는 글을 정리한 적이 있는데, 템플릿과 필드 매핑을 다루는 부분은 호출하는 언어가 바뀌어도 그대로 쓰입니다. Laravel에서는 Controller나 Service에서 데이터를 가공한 뒤 라이브러리를 호출하면 됩니다. Laravel 설정을 다룬 다른 글로는 Laravel 5.4 Passport 예제가 있습니다.

PHPJasper 설치와 실행 요건

설치는 Composer 한 줄입니다.

composer require geekcom/phpjasper

라이브러리는 PHP지만 요구사항에 Java가 들어갑니다. README의 Requirements 항목은 PHP 7.2 이상과 Java JDK 1.8을 필수로 적고 있고, JDBC 드라이버는 DB에서 데이터를 읽을 때만 필요한 선택 항목으로 분류합니다. Packagist의 geekcom/phpjasper 패키지 페이지를 보면 최신 릴리스는 3.4.0이고 PHP 제약은 ^7.2|^8.0입니다. 리포트를 돌릴 서버에 JDK가 설치돼 있는지부터 확인하는 게 순서입니다. 로컬에서 되던 코드가 배포 서버에서 실패한다면 이 부분을 먼저 봅니다.

패키지는 bin/jasperstarter 아래에 JasperStarter 바이너리를 함께 담고 있습니다. JasperStarter는 JasperReports를 명령행에서 돌리기 위한 런처로, JasperStarter 패키징 저장소 README는 이 도구를 JasperReports용 오픈소스 명령행 런처이자 배치 컴파일러라고 소개하며 Java 1.8 이상을 요구한다고 적고 있습니다. DB를 데이터소스로 쓸 때는 드라이버 jar를 설치 경로의 jdbc 디렉터리에 넣거나 --jdbc-dir로 다른 경로를 지정합니다. PHPJasper는 MySQL, PostgreSQL 커넥터를 함께 배포하고, MSSQL용 드라이버는 따로 받아야 합니다.

.jrxml 컴파일과 리포트 생성

템플릿은 .jrxml로 작성하고 이걸 컴파일해 .jasper 바이너리를 만듭니다. README에 실린 컴파일 예제는 다음과 같습니다.

$input = __DIR__ . '/vendor/geekcom/phpjasper/examples/hello_world.jrxml';
$jasper = new PHPJasper;
$jasper->compile($input)->execute();

컴파일은 템플릿이 바뀌지 않는 한 반복할 필요가 없습니다. 실제 출력물은 process()로 만듭니다. 아래도 README 예제 그대로입니다.

$input = __DIR__ . '/vendor/geekcom/phpjasper/examples/hello_world.jasper';
$output = __DIR__ . '/vendor/geekcom/phpjasper/examples';
$options = ['format' => ['pdf', 'rtf']];
$jasper = new PHPJasper;
$jasper->process($input, $output, $options)->execute();

format이 배열인 덕분에 한 번 실행으로 여러 포맷을 동시에 뽑을 수 있습니다. 그리고 예제의 $output은 확장자가 없는 경로라는 점을 눈여겨볼 만합니다. 템플릿에 어떤 파라미터가 선언돼 있는지 기억나지 않을 때는 listParameters()로 목록을 받아올 수 있습니다.

$output = $jasper->listParameters($input)->execute();

JSON 데이터소스로 PDF 뽑기

저는 JSON을 데이터소스로 쓰고 있어서 JSON adapter 기준으로 정리합니다. Controller나 Service에서 데이터를 가공한 뒤 아래처럼 호출하면 됩니다.

use PHPJasper\PHPJasper;

$input = '/your_input_path/your_report.jasper'; // .jrxml, .jasper 파일 경로
$output = '/your_output_path'; // Report가 생성될 경로

$data_file = __DIR__ . '/your_data_files_path/your_json_file.json'; // JSON 파일

$options = [
 'format' => ['pdf'], // html, xls 등 다른 포맷도 지정 가능
 'params' => [], // 템플릿에 설정한 param 값이 있으면 여기로 넘긴다
 'locale' => 'en', // 다국어 지원 여부
 'db_connection' => [
 'driver' => 'json', // sql, json 등 데이터 드라이버 설정
 'data_file' => $data_file, // 데이터 소스 경로
 'json_query' => 'your_json_query' // json을 읽기 시작할 키값
 ]
];

$jasper = new PHPJasper;

$jasper->process(
 $input,
 $output,
 $options
)->execute();

locale은 다국어를 자동으로 번역해주는 옵션이 아닙니다. 템플릿에 한글을 써넣고 en을 넘긴다고 영어로 바뀌지 않습니다. 해당 로캘일 때 쓸 문구를 템플릿 쪽에 같이 정의해둬야 합니다. JasperStarter 문서의 --locale 설명도 두 자리 ISO-639 코드나 de_DE 같은 ISO-639와 ISO-3166 조합을 지정하는 옵션이라고만 적고 있습니다.

데이터를 파일로 미리 만들어둘 필요는 없습니다. PHP 배열을 JSON 파일로 떨어뜨린 다음 그 경로를 $data_file로 넘겨도 됩니다.

$jsonArry = array('data' => $jsonData);
$jsonTmpfilePath = $output . '.json';
$jsonTmpfile = fopen($jsonTmpfilePath, 'w');
fwrite($jsonTmpfile, json_encode($jsonArry));
fclose($jsonTmpfile);
$data_file = $jsonTmpfilePath;

이렇게 data 키 아래에 배열을 담았다면 json_query에는 data를 넘깁니다. JasperReports의 JSON Data Source 샘플 문서는 JSON 객체 안의 속성을 마침표 표기법으로 접근한다고 설명하며 Northwind.Orders 같은 예를 듭니다. 즉 중첩이 깊으면 contacts.person처럼 경로를 이어 쓰면 됩니다. 필드는 템플릿에서 net.sf.jasperreports.json.field.expression property로 매핑합니다.

JSON Data Source 샘플json_query 경로 표기법과 필드 매핑 property를 확인할 때

options 배열에서 자주 막히는 지점

PHPJasper는 동봉된 JasperStarter 바이너리를 실행하는 구조라, options 배열의 키 이름이 JasperStarter 명령행 옵션과 그대로 대응합니다. data_file--data-file, json_query--json-query, locale--locale입니다. JasperStarter 문서에서 --data-file은 파일 기반 데이터소스의 입력 파일이고 -를 주면 표준 입력을 쓴다고 설명하고, --json-query는 JSON Datasource용 질의 문자열이라고 설명합니다. 옵션 이름이 헷갈릴 때는 JasperStarter 쪽 설명을 찾아보는 편이 빠릅니다.

출력 포맷은 pdf와 html 말고도 선택지가 넓습니다. JasperStarter 문서가 process 명령의 -f에 허용하는 값으로 적어둔 목록은 view, print, pdf, rtf, xls, xlsMeta, xlsx, docx, odt, ods, pptx, csv, csvMeta, html, xhtml, xml, jrprint입니다. 데이터소스 타입 -t에 올 수 있는 값은 none, csv, xml, json, jsonql, mysql, postgres, oracle, generic입니다. 위 코드의 'driver' => 'json'은 이 목록의 json에 해당합니다.

JSON 대신 다른 소스를 붙일 때 db_connection의 모양이 바뀝니다. README는 XML을 쓸 때 driver를 xml로 두고 data_filexml_xpath를 지정하는 예를 보여주고, PostgreSQL을 쓸 때는 driver, username, password, host, database, port를 채우는 예를 보여줍니다. MSSQL처럼 드라이버를 직접 얹어야 하는 경우에는 jdbc_driver, jdbc_url, jdbc_dir을 함께 씁니다. 키 구성만 갈아 끼우는 식이라 데이터소스를 바꿔도 호출부는 거의 그대로입니다.

이 방식이 맞는 서버

JDK 1.8을 올릴 수 있는 서버인지가 첫 번째 판단 기준입니다. PHP 7.2 이상이어도 JVM을 못 띄우면 후보에서 빠집니다. 양식이 자주 바뀌지 않는다면 .jrxml 컴파일은 배포 단계로 빼고 요청 시점에는 .jasper 실행만 남기는 편이 낫고, 데이터가 DB가 아니라 API 응답이라면 db_connectiondriver를 json으로 두면 그대로 들어맞습니다.

결정에 남는 변수는 포맷과 로캘 값입니다. 뽑으려는 출력 포맷이나 쓰려는 로캘이 실제로 통하는지는 JasperStarter Usage 의 옵션 목록에서 직접 확인해야 알 수 있습니다.

출처: PHPJasper README · geekcom/phpjasper on Packagist · JasperStarter Usage · JasperReports JSON Data Source Sample · JasperStarter 패키징 저장소