PROJ 라이브러리 개요
PROJ는 지리 공간 데이터의 좌표계를 서로 다른 기준 좌표계(CRS)로 변환하는 데 특화된 오픈소스 라이브러리입니다. 명령줄 인터페이스(CLI)뿐만 아니라 C/C++ API를 통해 애플리케이션에 직접 통합할 수 있습니다. 이 글에서는 C++ 환경에서 PROJ의 C API를 호출하여 좌표 변환 기능을 구현하고 활용하는 방법을 다룹니다.
CentOS 환경에서의 설치 및 검증
CentOS 7.9 환경을 기준으로 설명합니다. C++ 코드에서 API를 호출하려면 런타임 공유 라이브러리와 개발용 헤더 파일 및 정적 라이브러리가 모두 필요합니다.
sudo yum install proj proj-devel
설치가 정상적으로 완료되었는지 확인하기 위해 핵심 파일의 존재 여부를 검사합니다.
헤더 파일 확인:
ls /usr/include/proj_api.h
위 명령어 실행 시 proj_api.h 파일이 출력되면 개발 패키지가 성공적으로 설치된 것입니다.
라이브러리 파일 확인:
ls /usr/lib64/libproj.so*
공유 객체 파일이 존재하면 런타임 라이브러리 설치가 완료된 것입니다.
주요 API 및 자료형
참고: 본 구현은 PROJ 4.x 및 5.x 버전의 proj_api.h 헤더를 기반으로 합니다. PROJ 6.0 이상 버전에서는 proj.h를 사용하는 새로운 API로 마이그레이션되었으므로, 최신 환경에서는 공식 문서의 마이그레이션 가이드를 참고해야 합니다.
projPJ: 초기화된 투영(Projection) 객체를 나타내는 포인터 타입입니다.pj_init_plus(): PROJ4 형식의 정의 문자열을 파싱하여 투영 객체를 생성하고 초기화합니다.pj_free(): 사용이 끝난 투영 객체의 메모리를 해제합니다.pj_transform(): 소스 좌표계에서 대상 좌표계로 포인트 데이터를 변환합니다.
C++ 객체 지향 설계 및 코드 구현
C API를 직접 호출하면 메모리 누수나 리소스 관리 문제가 발생할 수 있습니다. 따라서 RAII(Resource Acquisition Is Initialization) 패턴을 적용하여 투영 객체를 관리하고, 좌표 구조체와 변환 함수를 캡슐화하여 안정성을 높입니다. 또한 pj_transform 함수가 경위도 데이터에 대해 라디안(Radian) 단위를 요구하므로, 이를 자동으로 처리하는 로직을 추가합니다.
// GeoSpatial.h
#pragma once
#include <iostream>
#include <string>
#include <stdexcept>
#include <proj_api.h>
#ifndef DEG_TO_RAD
#define DEG_TO_RAD (M_PI / 180.0)
#endif
#ifndef RAD_TO_DEG
#define RAD_TO_DEG (180.0 / M_PI)
#endif
struct GeoPoint {
double x;
double y;
double z;
GeoPoint(double px = 0.0, double py = 0.0, double pz = 0.0)
: x(px), y(py), z(pz) {}
};
class SpatialReference {
public:
explicit SpatialReference(const std::string& definition) {
m_handle = pj_init_plus(definition.c_str());
if (!m_handle) {
throw std::runtime_error("좌표계 초기화 실패: " + definition);
}
}
~SpatialReference() {
if (m_handle) {
pj_free(m_handle);
}
}
projPJ getHandle() const {
return m_handle;
}
SpatialReference(const SpatialReference&) = delete;
SpatialReference& operator=(const SpatialReference&) = delete;
private:
projPJ m_handle;
};
inline GeoPoint transformPoint(const SpatialReference& src, const SpatialReference& dst, GeoPoint pt) {
double tx = pt.x * DEG_TO_RAD;
double ty = pt.y * DEG_TO_RAD;
double tz = pt.z;
int err = pj_transform(src.getHandle(), dst.getHandle(), 1, 1, &tx, &ty, &tz);
if (err != 0) {
throw std::runtime_error("좌표 변환 중 오류 발생: 코드 " + std::to_string(err));
}
return GeoPoint(tx * RAD_TO_DEG, ty * RAD_TO_DEG, tz);
}
테스트 및 실행
WGS84 경위도 좌표를 Web Mercator 투영 좌표로 변환하는 테스트 코드를 작성하여 변환 로직을 검증합니다.
// main.cpp
#include "GeoSpatial.h"
int main() {
try {
const std::string wgs84Def = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs";
const std::string mercatorDef = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs";
SpatialReference srcCrs(wgs84Def);
SpatialReference dstCrs(mercatorDef);
// 서울 시청 대략적인 경위도
GeoPoint seoulCityHall(126.978, 37.566);
GeoPoint converted = transformPoint(srcCrs, dstCrs, seoulCityHall);
std::cout << "변환된 Web Mercator 좌표:" << std::endl;
std::cout << "X (Easting): " << converted.x << " m" << std::endl;
std::cout << "Y (Northing): " << converted.y << " m" << std::endl;
} catch (const std::exception& e) {
std::cerr << "오류: " << e.what() << std::endl;
return 1;
}
return 0;
}
자주 사용되는 PROJ4 좌표계 정의 문자열
다양한 좌표계 변환을 위해 아래와 같은 PROJ4 문자열을 활용할 수 있습니다.
- WGS84 (경위도):
+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs - UTM 52N 구역 (EPSG:32652):
+proj=utm +zone=52 +ellps=WGS84 +datum=WGS84 +units=m +no_defs - Web Mercator (EPSG:3857):
+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - Bessel 1841 기반 횡축 메르카토르 (TM):
+proj=tmerc +ellps=bessel +lon_0=127 +x_0=500000 +k=1.0
추가적인 좌표계 정의 문자열은 Spatial Reference 웹사이트나 EPSG 레지스트리에서 "proj4 string for [좌표계 이름]" 형식으로 검색하여 확인할 수 있습니다.