osm2pgsql로 OSM 데이터 넣기 — 테이블 구조는 도구가 정한다

15

osm2pgsql로 OSM 데이터 넣기 — 테이블 구조는 도구가 정한다 이미지

넣은 적 없는 좌표계가 붙어 있다

Shapefile을 데이터베이스에 넣을 땐 셈이 맞습니다. 251행짜리 파일을 넣으면 251행짜리 테이블이 나오죠. 그런데 OpenStreetMap 데이터는 다릅니다. 서울 한 도시분을 넣었더니 — 테이블이 네 개 생겨 있고, 좌표계를 지정한 적이 없는데 3857이 붙어 있고, 도형 칼럼 이름이 geomgeometry도 아닌 way 입니다. 아무것도 고른 적이 없는데요.

오늘은 osm2pgsql 로 OSM 데이터를 PostGIS에 적재하면서, 도구가 나 대신 내리는 결정들을 하나씩 확인합니다. 그리고 마지막에 그 결정권을 되찾아 옵니다.

이 글은 PostGIS가 설치된 데이터베이스 하나가 준비돼 있다고 가정합니다. 없다면 PostGIS 시작하기 편에 도커로 30초 만에 띄우는 방법이 있습니다.

측정 환경: osm2pgsql 2.3.1, PostGIS 3.5.2, 측정일 2026-09-02. 아래 편의점 대조표만 2026-09-03에 다시 쟀습니다.

왜 1:1이 안 되나

Shapefile은 사실상 표 한 장입니다. 속성이 담긴 .dbf의 행 하나가 테이블의 행 하나로 곧게 가죠. 그런데 OSM은 표가 아닙니다. 세상을 딱 세 가지 타입으로 그리거든요.

text
Node       좌표를 가진 점. 우체통일 수도 있고, 그냥 선을 그리기 위한 꼭짓점일 수도
Way        Node를 순서대로 가리키는 목록. 선일 수도 있고 면일 수도 있다
Relation   여러 요소를 역할(바깥/구멍 등)과 함께 묶은 것

여기엔 "행"이랄 게 없습니다. 그리고 결정적으로 — OSM엔 폴리곤이라는 타입이 아예 없습니다. 닫힌 Way가 건물(면)인지 순환도로(선)인지는 붙은 태그를 보고 판정해야 해요.

💡 이 데이터 모델을 처음 보신다면 OpenStreetMap 데이터 모델 편에서 자세히 다뤘습니다. 오늘 글은 위 세 줄만 알면 따라올 수 있어요.

그러니 누군가는 결정해야 해요.

그 결정을 내리는 게 osm2pgsql입니다. 오늘 글의 절반은 그 결정을 확인하는 일이고, 나머지 절반은 그걸 바꾸는 일이에요.

준비 — 도구와 데이터

데이터

OSM 데이터는 Geofabrik이 나라·지역 단위로 잘라 매일 배포합니다. 한국 전체를 받은 뒤 서울만 떼어내죠. 전국을 그대로 넣어도 되지만 적재가 길어집니다.

bash
curl -L -O https://download.geofabrik.de/asia/south-korea-latest.osm.pbf

osmium extract -b 126.76,37.42,127.18,37.70 \
  -o seoul.osm.pbf south-korea-latest.osm.pbf

osmium은 OSM 파일을 자르고 거르는 도구입니다. 설치는 OS마다 갈려요.

# macOS
brew install osmium-tool
# 데비안·우분투
sudo apt install osmium-tool
# 윈도우 — 공식 빌드가 없어 WSL이나 도커를 씁니다
wsl -e sudo apt install osmium-tool

⚠️ bbox 좌표 순서에 주의하세요. osmium왼쪽,아래,오른쪽,위(경도 먼저)입니다. Overpass API 같은 다른 도구는 위도를 먼저 받기도 해서, 같은 숫자 넷인데 순서가 뒤집히는 일이 흔합니다. 결과가 텅 비거나 엉뚱한 바다가 나오면 여기부터 의심하세요.

도구

osm2pgsql도 설치가 필요한데, 버전이 중요합니다. 2.0에서 설정 문법이 바뀌었거든요(뒤에 나올 Lua에서 add_row()insert()로). 배포판 패키지는 아직 1.x인 경우가 많으니 확인하세요.

bash
osm2pgsql --version    # 2.x 인지 확인

이 글은 도커로 갑니다. OS를 가리지 않고 버전도 맞출 수 있어서요.

bash
# PostGIS 컨테이너와 osm2pgsql 컨테이너를 같은 망에 둔다
docker network create gisnet
docker network connect gisnet gis          # gis = PostGIS 컨테이너 이름

💡 왜 네트워크를 만드나. PostGIS가 컨테이너 안에서 돌고 있다면, 다른 컨테이너에서 거기 붙으려면 같은 망에 있어야 합니다. 그러면 컨테이너 이름이 곧 호스트명이 되고, 포트는 호스트에 내보낸 번호가 아니라 컨테이너 내부 포트 5432 를 씁니다. PostGIS를 컨테이너가 아니라 로컬에 직접 설치했다면 이 단계는 건너뛰고, 아래 명령에서 -H gis 대신 -H host.docker.internal 을 쓰면 됩니다.

넣기 전에 — 원본에 뭐가 몇 개나 있나

나중에 대조하려면 출발점을 알아야 합니다.

bash
osmium fileinfo -e seoul.osm.pbf
text
  Name: seoul.osm.pbf
  Size: 30513661
  Bounding box: (126.6018192,37.29149,127.3420425,37.7857404)
  Number of nodes: 3012594
  Number of ways: 479707
  Number of relations: 12870

노드 301만 개, way 48만 개, relation 1.3만 개. 이 숫자를 기억해 두세요.

그냥 넣어본다

옵션을 최소한만 주고 실행합니다.

bash
docker run --rm --network gisnet -v "$PWD:/data" -e PGPASSWORD=postgres \
  iboates/osm2pgsql:2.3.1 \
  osm2pgsql -H gis -U postgres -d kor --create --slim --drop /data/seoul.osm.pbf

조각을 나눠 읽으면 이렇습니다.

text
-H gis            접속할 호스트 = 컨테이너 이름
-U postgres       사용자
-d kor            넣을 데이터베이스 이름 (미리 만들어 둔 것)
--create          새로 만든다 (기존 테이블이 있으면 갈아엎음)
--slim --drop     중간 테이블을 쓰되 끝나고 지운다 (메모리 절약)
-v "$PWD:/data"   현재 폴더를 컨테이너의 /data 로 보여줌

⚠️ Git Bash에서 실행하면 ERROR: Cannot detect file format for 'C:/Program'. 이 납니다. /data/... 를 윈도우 경로로 착각해 번역하기 때문이라, 앞에 MSYS_NO_PATHCONV=1 을 붙이면 됩니다.

text
osm2pgsql took 22s overall.

22초. 그리고 조회해 보면 —

sql
SELECT relname FROM pg_class
WHERE relkind='r' AND relname LIKE 'planet_osm%';
테이블행 수
planet_osm_point194,641
planet_osm_line200,742
planet_osm_polygon283,535
planet_osm_roads24,765

아까 osmium fileinfo로 세어둔 숫자와 나란히 놓아 봅시다.

원본 파일적재 후
Node3,012,594planet_osm_point 194,641
Way479,707line 200,742 + polygon 283,535
Relation12,870(polygon에 섞임)

노드 301만 개를 넣었는데 점 테이블엔 19만 개뿐입니다. 나머지 282만 개는 어디 갔을까요?

앞에서 본 그 성질 때문입니다 — OSM 노드 대부분은 "장소"가 아니라 선을 그리기 위한 꼭짓점이에요. 도로가 꺾이는 지점, 건물 모서리 하나하나가 다 노드거든요. 그런 노드는 점 테이블에 넣을 이유가 없습니다. Way의 모양을 만드는 데 쓰이고 사라져요.

반대로 Way 48만 개는 line과 polygon으로 갈리면서 48만 개보다 많아졌습니다. Relation이 풀려 폴리곤으로 들어가고, 일부 Way는 roads에도 복사되기 때문입니다.

네 테이블은 어떻게 갈렸나

text
Node  ─┬─ 태그 있음 ──────────────→ planet_osm_point
       └─ 태그 없음 ──────────────→ (버려짐, Way의 재료로만)

Way   ─┬─ 면으로 판정 ────────────→ planet_osm_polygon
       └─ 선으로 판정 ─┬──────────→ planet_osm_line
                        └ 주요 도로 → planet_osm_roads (사본)

Relation ─ multipolygon 등 ───────→ planet_osm_polygon

roadsline의 부분집합입니다. 별도 데이터가 아니라, 고속도로·국도처럼 지도를 멀리서 볼 때도 그려야 하는 것들만 골라 복사해 둔 테이블이에요. 저줌에서 20만 행을 훑지 않고 2.4만 행만 보면 되니까요.

line을 그냥 열어 보면 서울 같지가 않다

여기서 한 번쯤 겪는 일이 있습니다. QGIS 같은 뷰어로 planet_osm_line을 통째로 띄우면 도시가 아니라 털뭉치가 나옵니다. 서울의 형태가 안 보여요.

무엇이 들었는지 세어 보면 이유가 나옵니다.

sql
SELECT highway, count(*) FROM planet_osm_line
WHERE highway IS NOT NULL GROUP BY highway ORDER BY 2 DESC LIMIT 6;
text
   highway   | count
-------------+-------
 footway     | 46985   ← 인도·횡단보도
 residential | 44176   ← 골목
 service     | 42777   ← 주차장 통로, 건물 진입로
 path        |  6790
 primary     |  5265   ← 대로
 cycleway    |  4833

성격별로 묶으면 이렇습니다.

행 수
인도·골목·주차장 통로 등149,631
간선도로(motorway~tertiary)16,202
도로가 아닌 선(하천·철도·경계 등)21,512
합계201,073

우리가 "도로망"이라 부르는 건 전체의 8%뿐입니다. 나머지 92%가 같은 굵기로 겹쳐 그려지니 형태가 안 보이는 게 당연해요. 데이터가 잘못 들어간 게 아니라, OSM은 원래 인도 한 칸까지 다 그려 놓은 데이터입니다.

그래서 roads가 따로 있는 겁니다.

sql
SELECT count(*) FROM planet_osm_roads;   -- 24,765

이쪽을 열면 우리가 아는 서울이 나옵니다. 렌더링 목적이라면 줌 레벨에 따라 roadsline을 갈아 끼우는 것이 정석이고, 뷰어로 눈 검사만 할 거라면 WHERE highway IN ('motorway','trunk','primary','secondary') 같은 필터를 걸어도 됩니다.

면 판정은 도구가 들고 있다

"면으로 판정"이 앞에서 말한 그 문제입니다. 닫힌 Way에 building=yes가 붙었으면 면, highway=residential이면 선. 그 판정표를 도구가 들고 있습니다.

osm2pgsql로 OSM 데이터 넣기 — 테이블 구조는 도구가 정한다 이미지

도구가 대신 내린 결정 셋

① 좌표계를 안 물어봤다

sql
SELECT f_table_name, f_geometry_column, srid FROM geometry_columns
WHERE f_table_name LIKE 'planet_osm%';
text
 f_table_name       | f_geometry_column | srid
--------------------+-------------------+------
 planet_osm_point   | way               | 3857
 planet_osm_line    | way               | 3857
 planet_osm_polygon | way               | 3857
 planet_osm_roads   | way               | 3857

3857. 우리는 좌표계를 고른 적이 없습니다. Shapefile을 넣을 땐 .prj 파일을 뜯어 좌표계를 알아낸 뒤 -s 5174 처럼 손으로 알려줘야 했는데, 여기선 아예 물어보지도 않았어요.

3857은 웹 메르카토르입니다. 웹 지도 타일이 쓰는 좌표계죠(자세히는 지도 투영법 편에서).

그런데 여기서 짚고 갈 게 있습니다. OSM 원본은 3857이 아닙니다.

text
OSM 원본        노드마다 lat/lon 을 소수점 7자리로 저장  →  언제나 위경도(4326)
   └─ osm2pgsql 이 적재하면서 3857로 변환해 저장

즉 3857은 "원래 그랬던 것"이 아니라 도구가 바꿔 놓은 결과입니다. 실제로 값을 꺼내 보면 미터 단위 큰 수가 그대로 들어 있어요.

sql
SELECT name, ST_AsText(way) FROM planet_osm_point
WHERE shop = 'convenience' AND name IS NOT NULL LIMIT 1;
text
 GS25 | POINT(14156431.327409102 4503373.365285658)

osm2pgsql이 원래 지도 렌더링용 도구로 태어났으니 자연스러운 기본값입니다. 타일은 전부 3857로 그리니 미리 변환해 두면 렌더러가 매번 계산할 일이 없죠.

💡 이 설정은 default.style과 무관합니다. 조금 뒤에 볼 그 파일은 태그를 칼럼으로 만드는 규칙만 담고 있고, 좌표계 얘기는 한 줄도 없어요. 좌표계는 명령행 옵션 소관입니다.

text
-l, --latlong     위경도(4326)로 저장
-m, --merc        웹 메르카토르(3857)로 저장  ← 아무것도 안 주면 이것
-E, --proj SRID   지정한 좌표계로 저장

그리고 이 변환은 적재할 때 한 번 일어나 디스크에 그대로 저장됩니다. 조회할 때마다 바뀌는 게 아니에요.

분석이 목적이라면 3857이 불편할 수 있습니다. 좌표가 눈으로 안 읽히고, 메르카토르는 위도가 높을수록 거리가 부풀어 ST_Distance 결과가 실제 거리와 다르거든요. 그럴 땐 처음부터 다르게 넣으면 됩니다.

bash
osm2pgsql -d kor -l seoul.osm.pbf          # 위경도 그대로
osm2pgsql -d kor -E 5179 seoul.osm.pbf     # 한국 미터 좌표계로

② 칼럼 이름이 way

geomgeometry도 아닌 way입니다. 매뉴얼도 이렇게 적어놨어요.

All tables contain a geometry column named way (which is not the most intuitive name, but it can't be changed now because of backwards compatibility).

"직관적인 이름은 아니지만 하위 호환 때문에 이제 못 바꾼다." 도구가 15년 넘게 쓰이면서 굳어버린 이름입니다. 다른 적재 도구들은 대개 칼럼명을 옵션으로 받는데(ogr2ogr이라면 -lco GEOMETRY_NAME=geom), 여기선 선택지가 없어요.

③ 태그 1,801종 중 69개만 칼럼이 된다

여기가 오늘의 핵심입니다. 먼저 원본에 태그가 몇 종이나 있는지 세어 봅니다.

bash
osmium tags-count seoul.osm.pbf | wc -l
text
1801

서울 한 도시에만 1,801종입니다. (gis-19에서 봤듯 전 세계로 치면 113,052종이고요.) 그럼 테이블엔 칼럼이 몇 개 생겼을까요?

sql
SELECT count(*) FROM information_schema.columns
WHERE table_name = 'planet_osm_point';
text
69

69개. 나머지는 어떻게 됐을까요. 서울에서 가장 많이 쓰인 태그 40개를 칼럼 목록과 대조해 봤습니다.

text
Top 40 중 칼럼이 없는 태그: 24개

  addr:city        (199,786회 쓰임)
  addr:street      (170,861회)
  addr:district    (154,033회)
  addr:subdistrict (120,790회)
  name:ko
  addr:postcode
  name:en
  building:levels
  height

17만 번 쓰인 addr:street이 통째로 사라졌습니다. 주소 관련 칼럼을 찾아보면 이게 전부예요.

sql
SELECT column_name FROM information_schema.columns
WHERE table_name='planet_osm_point' AND column_name LIKE 'addr%';
text
 addr:housename
 addr:housenumber
 addr:interpolation

번지수는 남았는데 도로명이 없습니다. 주소가 반쪽만 들어온 셈이죠. name:ko가 없는 것도 한국 데이터를 다루는 입장에선 아프고요.

이 결정은 default.style이라는 파일에 적혀 있습니다.

text
node,way   access              text   linear
node,way   addr:housenumber    text   linear
node,way   amenity             text   polygon
node,way   note                text   delete

객체타입 / 태그키 / 자료형 / 플래그 네 칸이고, 규칙이 106줄뿐입니다.

플래그개수
linear · polygon69칼럼이 된다
delete31아예 버린다 (note, KSJ2:* 같은 수입 흔적)
polygon,nocolumn6면 판정에만 쓰고 칼럼은 안 만든다

목록에 없는 태그는 그냥 사라집니다. 1,801종 중 1,732종이 그렇게 없어진 거예요.

osm2pgsql로 OSM 데이터 넣기 — 테이블 구조는 도구가 정한다 이미지

그런데 이 출력은 곧 사라진다

여기까지가 osm2pgsql기본 동작이고, 검색하면 나오는 튜토리얼 대부분이 이걸 보여줍니다. 그런데 매뉴얼을 보면 이렇게 적혀 있어요.

The pgsql output is marked as deprecated now and does not get any of the new features. The pgsql output will be removed at some point, so you should think about migrating.

(flex 출력에 대해) If you are starting a new project, use this output.

폐기 예정입니다. 그런데도 아직 기본값이라, 옵션 없이 실행하면 방금 본 결과가 나옵니다.

왜 버려질까요? 지금까지 본 게 답입니다 — 결정이 전부 하드코딩돼 있어서요. 테이블은 네 개로 고정, 칼럼명은 way로 고정, 살아남는 태그는 106줄짜리 파일로 고정. 새 기능을 붙일 자리가 없습니다.

그래서 나온 게 flex 출력입니다.

flex — 결정권을 되찾는다

매뉴얼이 Flex Output이라 부르는 출력 모드입니다. flexible을 줄인 이름이고, 매뉴얼도 그렇게 밝혀 뒀어요.

The flex output, as the name suggests, allows for a flexible configuration. This is the most modern and most flexible output.

무엇이 유연하다는 걸까요. 테이블 구조를 Lua 파일로 직접 쓴다는 점입니다. "이런 테이블을 만들고, 이런 객체를 이렇게 넣어라"를 내가 정하는 거죠. 방금 본 결정 셋 — 좌표계, 칼럼 이름, 살아남을 태그 — 이 전부 내 손으로 돌아옵니다.

잠깐, Lua 파일이 뭐죠

Lua는 프로그래밍 언어입니다. 다른 프로그램 안에 심어 쓰라고 만들어진 작고 가벼운 언어예요. osm2pgsql --version을 찍어보면 해석기를 통째로 품고 있는 게 보입니다.

text
Lua 5.1.4 (LuaJIT 2.1.1761786044)

같은 이유로 여러 곳에서 씁니다 — Roblox와 World of Warcraft의 스크립트, nginx 설정, Redis 스크립트, Neovim 설정이 다 Lua예요.

왜 JSON이나 YAML이 아닐까요? 설정할 내용이 값이 아니라 판단이라서입니다.

text
"닫힌 way인데 building 태그가 있으면 면 테이블에,
 없으면 선 테이블에, shop=convenience면 내 테이블에도"

JSON으로는 이런 조건 분기를 쓸 수 없습니다. 그래서 설정을 코드로 받는 길을 택한 거예요.

문법은 자바스크립트와 느낌이 비슷합니다. 이 표만 있으면 아래 설정을 읽고 고칠 수 있어요.

LuaJavaScript
local x = 1const x = 1
-- 주석// 주석
a ~= ba !== b
if … then … endif (…) { … }
function f() … endfunction f() { … }
obj:method()obj.method():은 자기 자신을 넘긴다

osm2pgsql이 정해둔 이름은 몇 개뿐입니다. define_table(테이블 정의), process_node·process_way(객체가 하나씩 들어올 때마다 호출), insert(행 추가). 나머지는 평범한 Lua 문법이에요.

편의점만 뽑아내기

예제로 서울의 편의점만 골라 테이블 하나로 만들어 보겠습니다. 같은 .osm.pbf를 다시 읽되, 이번엔 내가 설계한 모양으로요.

lua
-- convenience.lua
local stores = osm2pgsql.define_table({
    name = 'convenience',
    ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },
    columns = {
        { column = 'name',   type = 'text' },
        { column = 'brand',  type = 'text' },
        { column = 'street', type = 'text' },   -- 기본 출력이 버리던 것
        { column = 'tags',   type = 'jsonb' },  -- 나머지 태그는 통째로 보관
        { column = 'geom',   type = 'point', projection = 4326 },
    }
})

local function save(object, geom)
    if object.tags.shop ~= 'convenience' then
        return
    end
    stores:insert({
        name   = object.tags.name,
        brand  = object.tags.brand,
        street = object.tags['addr:street'],
        tags   = object.tags,
        geom   = geom,
    })
end

function osm2pgsql.process_node(object)
    save(object, object:as_point())
end

function osm2pgsql.process_way(object)
    if object.is_closed then
        save(object, object:as_polygon():centroid())
    end
end

Lua를 몰라도 읽힙니다. 세 부분이에요.

process_way 쪽을 눈여겨보세요. 닫힌 Way면 면으로 보고 중심점을 뽑습니다. 편의점이라고 다 점으로 찍혀 있는 게 아니라, 누군가는 건물 외곽선을 그리고 거기에 shop 태그를 붙여 두거든요. 그런 것들을 점으로 환산해 한 테이블에 모으는 겁니다.

bash
docker run --rm --network gisnet -v "$PWD:/data" -e PGPASSWORD=postgres \
  iboates/osm2pgsql:2.3.1 \
  osm2pgsql -H gis -U postgres -d kor \
  --output=flex --style=/data/convenience.lua --create /data/seoul.osm.pbf
text
osm2pgsql took 8s overall.

제대로 들어갔는지 검산해 봅시다. 같은 질문을 완전히 다른 경로로도 물어볼 수 있거든요 — OSM에는 Overpass API라는 원격 조회 서비스가 있어서, 파일을 내려받지 않고도 "이 범위의 편의점 개수"를 물을 수 있습니다.

같은 지역을 두 방법으로 같은 날 세어 봤습니다(2026-09-03 측정).

Overpass API (원격 조회)flex (로컬 적재)
전체7,1007,100
점(Node)6,9876,987
면(Way)113113

한 행도 다르지 않습니다. 원본이 같은 OSM 데이터이니 맞아떨어지는 게 정상이고, 실제로 정확히 맞았습니다. 파일을 받아 로컬에 적재한 경로와 원격 API에 물은 경로가 같은 답을 냈으니, Lua로 짠 필터가 의도한 대로 걸렀다는 뜻이에요.

💡 이 숫자는 시간이 지나면 달라집니다. OpenStreetMap 데이터 모델 편에서 같은 쿼리를 2026년 8월 7일에 돌렸을 때는 Node 6,986개 + Way 114였어요. 합계는 7,100으로 같은데 점이 하나 늘고 면이 하나 줄었습니다. 면으로 그려져 있던 편의점 하나가 점으로 다시 그려지면 이렇게 됩니다.

그 편에서 "같은 편의점인데 어떤 건 점이고 어떤 건 건물 폴리곤" 이라는 걸 발견했었는데, 오늘 process_way에서 닫힌 Way의 중심점을 뽑은 게 정확히 그 면들을 처리한 것이었어요.

⚠️ overpass-api.de가 아예 응답하지 않을 때가 있습니다. 공용 인스턴스라 부하가 걸리면 HTTP 에러도 아니고 연결 자체가 안 됩니다(이 표를 다시 재는 동안에도 그랬어요). 쿼리를 의심하기 전에 다른 인스턴스로 같은 쿼리를 보내 보세요 — 같은 OSM 데이터베이스를 보니 결과는 같습니다. 가용한 목록은 OSM 위키의 Overpass API 인스턴스 문서에 정리돼 있고, 위 숫자는 그중 하나로 받았습니다.

그리고 버려졌던 것들이 살아 있습니다.

sql
SELECT count(street) FROM convenience;
text
 4634
sql
SELECT jsonb_pretty(tags) FROM convenience WHERE name IS NOT NULL LIMIT 1;
text
{
    "name": "세븐일레븐 도봉산로점",
    "shop": "convenience",
    "addr:city": "서울특별시",
    "addr:street": "도봉산4길",
    "addr:district": "도봉구",
    "addr:postcode": "01301",
    "addr:housenumber": "15"
}

jsonb 칼럼 하나로 태그를 통째로 가져왔더니 주소가 온전합니다. 칼럼을 미리 정하지 않아도 되고, 나중에 tags->>'addr:street'로 꺼내 쓰면 되죠.

용량 차이도 큽니다.

크기
planet_osm_* 네 테이블175 MB
convenience 한 테이블2.8 MB

당연합니다 — 필요한 것만 넣었으니까요. "무엇을 버릴지"를 도구가 아니라 내가 정한 결과입니다.

osm2pgsql로 OSM 데이터 넣기 — 테이블 구조는 도구가 정한다 이미지

💡 그럼 기본 출력은 쓸모없나? 아닙니다. 지도를 통째로 렌더링할 거라면 네 테이블이 그대로 필요하고, 그 스키마를 전제로 만들어진 지도 스타일(CartoCSS 등)이 잔뜩 있어요. 다만 새로 시작하는 프로젝트라면 flex가 매뉴얼의 권고이고, 특정 목적이 뚜렷할수록 이득이 큽니다.

정리 — 한 문장으로

OSM은 표가 아니라서 "행 하나가 행 하나로" 들어갈 수 없다. 누군가는 점·선·면을 가르고 어떤 태그를 남길지 정해야 하는데, 기본 출력에선 그 결정을 도구가 내리고(테이블 4개·SRID 3857·칼럼명 way·태그 69개), flex에선 내가 내린다.

ShapefileOSM 기본 출력OSM flex
행 대응251 → 251300만 노드 → 4테이블내가 정함
테이블내가 이름 지정고정 4개내가 설계
SRID-s 5174 지정3857 고정projection = 4326
도형 칼럼geom 지정way 고정내가 지정
속성.dbf 그대로69개만내가 고름 + jsonb
상태deprecated권장

이제 데이터베이스에 서울 OSM이 통째로 들어 있습니다. 파일에서 데이터베이스로 옮기는 일은 여기까지예요.

다음 편부터는 질문하는 법입니다. ST_Intersects·ST_Within·ST_Contains… 이름이 비슷비슷한 함수들이 실은 9칸짜리 행렬 하나로 전부 정의된다는 걸 보게 됩니다. DE-9IM, 공간 관계의 문법이에요.

참고