Skip to content

Latest commit

 

History

History
437 lines (318 loc) · 22.1 KB

File metadata and controls

437 lines (318 loc) · 22.1 KB

DRAFT.md — 교내 시설 길찾기 시스템 설계 초안

이 문서는 검토용 초안입니다. 각 절의 [검토 필요] 표시와 마지막 "9. 결정이 필요한 사항"을 확인한 뒤 PLAN.md를 확정하시면 됩니다.


1. 개요

Spring Boot 기반의 캠퍼스 건물 시설 길찾기 시스템. 네이버 지도와 유사하게 (1) 검색어 입력 → 자동완성/매핑, (2) 출발지·도착지 선택 → 경로 안내를 제공한다.

설계의 핵심 난점은 두 가지로 정리된다.

  • 검색: 하나의 물리적 장소가 여러 표기('공학관 501호' = 'T동 501호' = 'T501')를 가지며, 이를 단일 DB 레코드로 매핑하면서도 자동완성을 빠르게 응답해야 한다.
  • 길찾기: 그래프 상의 모든 노드를 지나가지만, 사용자에게 보여줄 "지시(instruction)"는 의미 있는 지점에서만 생성되어야 한다. 동시에 노드의 성격을 표현하는 스키마가 개발 중 동적으로 변할 수 있다.

1.1 프로젝트 범위 (Scope)

본 프로젝트는 백엔드 전용이다. 다음을 명확히 구분한다.

  • 범위 내: 도메인 모델, DB 스키마, 검색/자동완성 로직, 다익스트라 길찾기, 경로 지시 추출, REST API 응답 계약(contract).
  • 범위 밖: 프론트엔드 구현, 지도 타일 렌더링, 좌표계 변환, 클라이언트 UI. 백엔드는 polyline·steps응답 계약만 책임진다. 응답에 좌표·층 정보가 포함되는 것은 클라이언트가 경로를 그릴 수 있도록 하는 계약 설계일 뿐이며, 렌더링 자체는 클라이언트의 몫이다.
  • 비대상 가정: 본 서비스는 실시간 사용자 위치(GPS 스트림)를 받지 않는다. 길찾기는 사용자가 지정한 출발 노드 → 도착 노드의 정적(static) 경로 계산이며, 응답은 전체 경로를 한 번에 반환한다. 경로 재탐색(rerouting), 현재 위치 기반 단계별 추적 등 실시간 요소는 설계에 포함하지 않는다.

2. 기술 스택 제안

영역 제안 이유
프레임워크 Spring Boot 3.x / Java 17+ 기본
ORM Spring Data JPA + Hibernate 표준
DB PostgreSQL JSONB 컬럼으로 동적 스키마 수용, 인덱싱 우수
위치 데이터 우선 double lat/lng, 필요 시 PostGIS 캠퍼스 규모에선 단순 좌표로 충분
자동완성 캐시 인메모리 Trie (+ 선택적 Redis) 캠퍼스 규모상 외부 검색엔진 불필요
마이그레이션 Flyway 또는 Liquibase 스키마 변경 이력 관리

[검토 필요] 규모가 "굉장히 크지는 않다"고 하셨으므로 Elasticsearch 같은 검색엔진과 PostGIS는 일단 배제하고 시작하는 것을 권장한다. 데이터가 수천 노드 이하라면 인메모리 처리로 충분하다.

2.1 DB 선택 — RDB vs NoSQL vs 그래프 DB

DB 후보를 명시적으로 비교하면 다음과 같다.

후보 적합한 경우 본 프로젝트 평가
RDB (PostgreSQL) 관계 중심, 조인 多, 정합성 중요 적합. 채택
문서형 NoSQL (MongoDB 등) 문서마다 스키마가 제각각, 수평 확장 필요, 조인 적음 부적합
그래프 DB (Neo4j 등) 수백만 노드 이상의 대규모 그래프 탐색 과잉

이 도메인의 데이터는 관계 중심이다. Place ↔ Building ↔ BuildingAlias, Node ↔ Edge는 전형적인 외래키·조인 관계이고, 그래프 편집 시 외래키 제약과 트랜잭션이 정합성을 보장한다. NoSQL이 유리한 조건 — (a) 문서별 상이한 스키마, (b) 대용량·고쓰기 수평 확장, (c) 조인 거의 없음 — 은 캠퍼스 규모 + 관계 중심 도메인에서 셋 다 해당하지 않는다.

"동적 스키마" 요구는 NoSQL의 근거가 될 수 있으나, PostgreSQL의 JSONB"안정적 핵심은 컬럼, 가변 속성은 문서" 라는 하이브리드를 단일 DB 안에서 제공하므로(5.2절 참조) NoSQL로 갈 필요가 없다. 그래프 DB 역시 노드가 수천 개 규모라면 그래프를 인메모리에 적재해 Dijkstra를 돌리는 것으로 충분하며, 별도 그래프 엔진의 운영 비용이 정당화되지 않는다. → PostgreSQL 채택.


3. 도메인 모델 개요

모델은 크게 검색/장소 영역그래프 영역으로 나뉜다. 두 영역은 Place.entranceNodeId로 연결된다(장소를 검색하면 그 장소의 대표 노드로 길찾기를 수행).

[검색 영역]                        [그래프 영역]
 Building ─< BuildingAlias          Node ─< Edge
    │                                ▲
    └─< Place ──(entranceNodeId)──────┘
          │
          └─< SearchToken (자동완성 색인)

4. 검색어 자동완성 및 동의어 매핑

4.1 문제 정리

  • '공학관 501호', 'T동 501호', 'T501'하나의 레코드다.
  • 사용자가 'T'만 입력해도 'T501', 'T401' 등이 자동완성되어야 하고, 동의어인 '공학관'도 떠야 한다.
  • 자동완성 결과에서 같은 레코드를 가리키는 여러 표기는 하나로 합쳐져 보여야 한다.
  • 어떤 표기로 검색하든 최종 응답은 단일 레코드다.

4.2 데이터 모델 — 정규 레코드 + 별칭(alias)

핵심 아이디어: 정규(canonical) 레코드는 하나, 표기(alias)는 여러 개. 표기는 두 계층에서 발생한다.

  1. 건물 계층 별칭'공학관''T동''T'
  2. 장소 계층 식별자 — 건물 별칭 × 호실 번호의 조합으로 파생
Building
  id            PK
  code          "T"              -- 건물 코드
  display_name  "공학관"          -- 정규 표시명

BuildingAlias
  id            PK
  building_id   FK -> Building
  alias         "공학관" | "T동" | "T" | "공학" ...

Place                            -- ★ 사용자에게 응답되는 정규 레코드
  id            PK
  building_id   FK -> Building
  floor         5
  room_number   "501"
  name          "전자공학과 사무실" (nullable)  -- 호실 자체의 고유명
  category      ROOM | OFFICE | LECTURE_HALL | FACILITY ...
  entrance_node_id  FK -> Node    -- 길찾기 시 이 장소의 대표 노드
  display_name  "공학관 501호"    -- 정규 표시명(파생 가능)

SearchToken                      -- 자동완성 색인(파생 데이터, 빌드로 생성)
  id            PK
  token         정규화된 검색 문자열
  place_id      FK -> Place
  weight        랭킹 가중치

SearchToken은 사용자가 직접 입력하지 않는 파생 테이블이다. Building/BuildingAlias/Place가 진실의 원천(source of truth)이고, SearchToken은 이들로부터 빌드된다.

4.3 검색 토큰 생성 규칙

Place(공학관 501호, 건물 별칭 = {공학관, T동, T}) 에 대해 다음 토큰을 생성한다.

패턴 예시
건물별칭 + 호실 + "호" 공학관 501호, T동 501호, T 501호
건물별칭 + 호실 (공백 변형) 공학관501호, t501, t 501
건물별칭 + 호실(번호만) 공학관 501, T501
호실 단독 (가중치 ↓) 501호, 501
장소 고유명 전자공학과 사무실 (name 필드가 있을 때)

모든 토큰은 place_id를 가리키므로, 어떤 토큰이 매칭되든 동일 레코드로 귀결된다. 토큰 생성은 데이터 입력/수정 시 트리거하거나 배치로 재빌드한다.

4.4 자동완성 자료구조 — 인메모리 Trie

빠른 prefix 응답을 위해 애플리케이션 기동 시 SearchToken을 읽어 Trie를 메모리에 적재한다.

  • Trie의 각 노드/리프는 해당 prefix에 도달하는 place_id 집합을 누적한다.
  • prefix 질의 시 서브트리의 place_id를 모아 중복 제거 후 weight 순으로 정렬, 상위 N개 반환.
  • 데이터 변경 시 Trie를 재빌드(이벤트 또는 주기적). 캠퍼스 규모면 재빌드 비용이 무시할 만하다.

대안: PostgreSQL LIKE 'prefix%' + text_pattern_ops 인덱스, 또는 pg_trgm. 구현은 단순하나 동의어 합산·중복 제거 로직을 쿼리에 녹이기 번거롭다. 권장: Trie를 1차 경로로, DB는 진실의 원천 겸 폴백.

4.4.1 동의어 처리 — Trie 방식과 Elasticsearch 방식의 차이

두 방식은 동의어를 확장하는 시점(timing)이 다르다.

  • Elasticsearch: 검색 시점에 synonym filter가 질의어를 동의어로 확장한다. 동의어 사전만 등록하면 색인은 그대로 둔다.
  • 본 설계(Trie): 색인 생성 시점에 동의어를 미리 펼쳐 SearchToken으로 저장한다. '공학관 501호', 'T동 501호', 'T501'을 전부 토큰으로 만들어 모두 같은 place_id를 가리키게 한다(4.3절).

결과적으로 prefix 자동완성에 한해서는 동일한 사용자 경험이 나온다. 다만 ES가 추가 구현 없이 제공하는 것 중 Trie에는 없는 것이 있다.

기능 Elasticsearch Trie (본 설계)
prefix 자동완성 + 동의어 O O (색인 시점 확장)
오타 보정(fuzzy) O 별도 구현 필요
형태소 분석 O 별도 구현 필요
중간 단어 매칭('501호'로 검색) O prefix 본질상 어려움
랭킹 튜닝(BM25 등) O weight 수동 설계

오버헤드 측면에서 정리하면:

  • 운영 오버헤드: Trie가 훨씬 작다. 별도 클러스터·색인 동기화 파이프라인이 불필요하고 메모리만 사용한다.
  • 개발 오버헤드: 요구 기능 범위에 따라 갈린다. 단순 prefix 자동완성만이면 Trie가 더 가볍다. 반면 fuzzy·형태소·중간 매칭까지 필요하면 ES 도입이 오히려 적은 비용이 된다.

→ 따라서 검색에 필요한 기능 범위를 먼저 확정해야 선택이 명확해진다(9절 1번 항목). 1차 범위가 prefix 자동완성 + 동의어뿐이라면 Trie를 권장한다.

4.5 중복 제거

자동완성 응답의 단위는 토큰이 아니라 **place_id**다.

prefix "T" 매칭 토큰: ["T501", "T동 501호", "T401", "T동 401호", ...]
        → place_id 로 환원: [101, 101, 102, 102, ...]
        → distinct:        [101, 102, ...]
        → 각 place_id 의 display_name 으로 응답

따라서 'T501''T동 501호'가 동시에 매칭돼도 사용자에게는 공학관 501호 한 줄만 보인다.

4.6 입력 정규화 (한글 처리 포함)

토큰 저장 시와 질의 시 동일한 정규화 함수를 거쳐야 매칭이 일관된다.

  • 유니코드 NFC 정규화
  • 영문 소문자화 (Tt)
  • 공백 제거 또는 단일화 (T 501T501)
  • , 등 접미사 변형 흡수

[검토 필요] 초성 검색(ㄱㅎㄱ공학관)을 지원할지. 사용자 경험은 좋아지나 토큰/매칭 로직이 늘어난다. 1차 범위에서 제외하고 후속 개선으로 두는 것을 권장.

4.7 검색 API 초안

GET /api/search/autocomplete?q=T&limit=10
→ 200
{
  "suggestions": [
    { "placeId": 101, "displayName": "공학관 501호", "category": "OFFICE" },
    { "placeId": 102, "displayName": "공학관 401호", "category": "ROOM" }
  ]
}

GET /api/places/{placeId}
→ 200  단일 레코드 상세

5. 길찾기

5.1 그래프 모델

Node
  id            PK
  lat, lng      double            -- 모든 노드는 geocode 보유
  floor         int               -- 실내 층수 (실외는 0 또는 null)
  building_id   FK -> Building (nullable, 실외면 null)
  is_indoor     boolean
  node_type     enum              -- 안정적인 상위 분류 (5.2 참조)
  properties    JSONB             -- 동적 속성 (5.2 참조)

Edge
  id            PK
  from_node_id  FK -> Node
  to_node_id    FK -> Node
  weight        double            -- 거리(또는 소요시간) 기반
  edge_type     enum              -- CORRIDOR | OUTDOOR_PATH | STAIR
                                  --  | ELEVATOR | DOOR
  bidirectional boolean
  properties    JSONB

층 간 이동은 STAIR/ELEVATOR 타입의 Edge가 floor=N 노드와 floor=N+1 노드를 연결하는 식으로 모델링한다. 실내/실외 경계는 DOOR 타입 Edge 또는 is_indoor가 바뀌는 인접 노드 쌍으로 표현한다.

5.2 노드 성격 표현과 동적 스키마 — JSONB 하이브리드

요구사항: "각 노드의 성격이 스키마에 나타나야 하고, 개발 중 스키마가 동적으로 변할 수 있다."

순수 enum 컬럼은 변경 때마다 마이그레이션이 필요해 동적 변화에 약하고, 순수 JSONB는 핵심 질의가 불편하다. 하이브리드를 권장한다.

  • 안정적 핵심은 컬럼으로: lat, lng, floor, building_id, is_indoor, node_type.
    • node_type은 거의 바뀌지 않는 상위 분류만: WAYPOINT(단순 경유), JUNCTION(분기), DOOR(출입구), VERTICAL_LINK(계단/엘리베이터 접점), POI(목적지 후보).
  • 자주 바뀌는 세부 속성properties JSONB로: isStair, isElevator, isEntrance, landmark, accessibility 등. 새 속성 추가 시 마이그레이션 불필요.

Hibernate는 @JdbcTypeCode(SqlTypes.JSON) 또는 hypersistence-utils로 JSONB를 매핑할 수 있다.

[검토 필요] node_type enum의 초기 값 목록을 PLAN.md에서 확정할 것. 어디까지를 안정 컬럼으로, 어디부터를 JSONB로 둘지의 경계가 핵심 설계 결정이다.

5.3 다익스트라

  • 캠퍼스 규모상 그래프를 기동 시 메모리에 인접 리스트로 적재하고, DB 변경 시 갱신.
  • 표준 우선순위 큐 기반 Dijkstra. weight는 거리(미터) 기반을 기본으로 하되 가중치 정책을 둔다.
    • 계단 패널티, 실외 경로 선호/비선호 등은 edge_type별 배수로 조정.
    • 접근성 모드: STAIR 간선을 제외하면 휠체어 경로가 된다.
입력:  startNodeId, endNodeId, (선택) mode = NORMAL | ACCESSIBLE
출력:  Node 의 순서 리스트 (경로상 전체 노드)

5.4 경로 지시(instruction) 추출 — 2단계 처리

Dijkstra의 결과는 경로상 모든 노드다. 여기서 곧바로 응답하지 않고, 지시 추출(post-processing) 단계를 거쳐 의미 있는 지점만 "step"으로 묶는다.

핵심 원칙: 곡선 경로는 여러 노드·간선을 포함하지만 하나의 step으로 표현한다. 즉 지시 추출은 노드열에 대한 일종의 run-length 압축이다.

지시가 발생하는 이벤트

이벤트 트리거 조건 생성되는 지시 예
출발 경로 시작 "출발합니다"
회전 연속 간선의 방위각 변화가 임계치 초과 "좌회전 / 우회전 하세요"
실내↔실외 전환 인접 노드의 is_indoor 변화 (또는 DOOR 간선) "건물로 들어가세요 / 나가세요"
층 변경 인접 노드의 floor 변화 "6층까지 계단으로 올라가세요"
도착 경로 끝 (목적지 노드) "도착했습니다"

층 변경 지시에서 "계단/엘리베이터" 구분은 해당 구간 Edgeedge_type으로 결정한다.

추출 알고리즘 (의사코드)

steps = []
current = 새 step (시작 노드)
for i in 1 .. path.length-1:
    event = 노드 path[i] 에서 발생하는 이벤트 판정
            (회전? 층변경? 실내외전환? 도착?)
    if event != NONE:
        current.endIndex = i
        steps.add(current)
        current = 새 step (i, type = event)
steps.add(current)

각 step은 path 상의 **노드 인덱스 구간 [startIndex, endIndex]**을 보유한다. 직진 구간 내부에 노드가 100개여도 step은 1개다.

5.5 회전 감지와 곡선 처리

회전은 연속한 두 간선의 방위각(bearing) 차이로 판정한다.

θ1 = bearing(path[i-1] → path[i])
θ2 = bearing(path[i]   → path[i+1])
Δ  = normalize(θ2 - θ1)   // (-180°, 180°]
Δ > 0 → 우회전 / Δ < 0 → 좌회전 (좌표계 기준에 따라 조정)
  • 직진: |Δ| ≤ ε (예: 15°) → 지시 없음, 직진 step에 흡수.
  • 회전: |Δ| ≥ τ (예: 35°) → 회전 지시 생성.
  • 완만한 곡선: 곡선은 여러 노드로 그려지지만 각 노드의 |Δ|가 작다(< τ). 따라서 단일 노드 임계치만 쓰면 곡선은 자연히 회전 지시를 만들지 않고 직진으로 병합된다. → 1차 구현으로 권장.
  • 개선안(선택): 곡선이 급해서 중간 크기의 Δ가 연속될 경우, 슬라이딩 윈도우로 누적각을 계산해 윈도우 중심에 회전 지시 하나만 생성. 1차에서는 보류.

[검토 필요] ε, τ 임계값은 실제 노드 배치 밀도에 따라 튜닝이 필요하다. PLAN.md에서 초기값을 정하고 추후 조정.

5.6 Response 설계

참고: 본 절은 응답 계층 분리의 설계 배경이다. 확정된 /api/route 응답 계약(필드명·summary.bound/features·step별 context/distanceMeters/durationSeconds/road/landmark 등)은 .claude/API.md §2를 따른다.

응답은 두 계층으로 구성한다. 지도 렌더링용 전체 좌표와, 화면 안내용 step 목록을 분리한다.

GET /api/route?from=PLACE_101&to=PLACE_250&mode=NORMAL
→ 200
{
  "summary": { "distanceMeters": 320, "estimatedSeconds": 280, "floorChanges": 1 },

  // (1) 지도에 경로를 그리기 위한 전체 노드열 — 곡선 포함 모든 점
  "polyline": [
    { "lat": 37.0001, "lng": 127.0001, "floor": 1, "indoor": false },
    { "lat": 37.0002, "lng": 127.0002, "floor": 1, "indoor": false }
    // ... 모든 노드
  ],

  // (2) 사용자에게 보여줄 지시 목록 — 의미 있는 지점만
  "steps": [
    {
      "type": "DEPART",
      "instruction": "공학관 정문에서 출발합니다",
      "distanceMeters": 0,
      "location": { "lat": 37.0001, "lng": 127.0001, "floor": 1 },
      "polylineRange": [0, 0]
    },
    {
      "type": "TURN_LEFT",
      "instruction": "좌회전 후 직진하세요",
      "distanceMeters": 80,
      "location": { "lat": 37.0005, "lng": 127.0004, "floor": 1 },
      "polylineRange": [0, 12]        // 이 step 이 덮는 노드 구간 (곡선 흡수)
    },
    {
      "type": "ENTER_BUILDING",
      "instruction": "공학관 안으로 들어가세요",
      "location": { "lat": 37.0008, "lng": 127.0007, "floor": 1 },
      "polylineRange": [12, 13]
    },
    {
      "type": "FLOOR_CHANGE",
      "instruction": "엘리베이터로 5층까지 올라가세요",
      "meta": { "fromFloor": 1, "toFloor": 5, "transport": "ELEVATOR" },
      "polylineRange": [13, 14]
    },
    {
      "type": "ARRIVE",
      "instruction": "공학관 501호에 도착했습니다",
      "location": { "lat": 37.0009, "lng": 127.0008, "floor": 5 },
      "polylineRange": [14, 14]
    }
  ]
}

polylineRange가 곡선 문제를 해결한다. 곡선의 모든 점은 polyline에 남아 지도에 매끄럽게 그려지고, steps에서는 그 구간을 인덱스 범위로만 참조하는 단일 지시가 된다.

step type 후보(초안): DEPART, STRAIGHT, TURN_LEFT, TURN_RIGHT, ENTER_BUILDING, EXIT_BUILDING, FLOOR_CHANGE, ARRIVE.

5.7 길찾기 API 초안

GET /api/route?from={placeId|nodeId}&to={placeId|nodeId}&mode=NORMAL|ACCESSIBLE

placeId로 들어오면 Place.entranceNodeId로 환원한 뒤 노드-투-노드 Dijkstra를 수행한다. 사용자는 장소를 고르지만 내부적으로는 그래프 노드로 길을 찾는다.


6. 검색 영역과 그래프 영역의 연결

  • 사용자는 **장소(Place)**를 검색/선택한다.
  • Place는 그래프 상의 대표 노드 entranceNodeId(보통 그 호실의 출입문 노드)를 가진다.
  • 길찾기 요청은 Place → Node로 변환되어 처리된다.

이 분리 덕분에 검색 모델과 그래프 모델을 독립적으로 진화시킬 수 있다.


7. 데이터 입력(authoring)

노드·간선·장소 데이터를 어떻게 채워 넣을지가 별도 과제다. 후보:

  • 관리자용 지도 편집 화면(노드 찍기, 간선 잇기, 속성 입력)
  • GeoJSON 등 외부 포맷 임포트
  • 초기에는 SQL/CSV 시드 + Flyway

[검토 필요] 1차 범위에 편집 도구를 포함할지, 시드 데이터로 시작할지 결정 필요.


8. 성능 및 기타 고려

  • 그래프와 Trie는 인메모리 적재 → Dijkstra와 자동완성 모두 ms 단위 응답 기대.
  • 데이터 변경 시 인메모리 구조 갱신 전략 필요(이벤트 기반 재빌드 vs 주기적).
  • SearchToken·polyline 등 파생 데이터의 재생성 책임을 한 곳에 모을 것(서비스 계층).

9. 결정이 필요한 사항 (PLAN.md에서 확정)

  1. 검색엔진 범위 — 인메모리 Trie로 충분한지, Redis/Elasticsearch 도입 여부. 결정의 기준은 필요 기능 범위다(4.4.1절 참조): prefix 자동완성 + 동의어뿐이면 Trie, fuzzy·형태소·중간 매칭이 필요하면 ES.
  2. 초성 검색 지원 여부 (1차 제외 권장).
  3. node_type enum 초기 목록과 안정 컬럼 / JSONB 경계.
  4. 층 간 연결 모델링 방식 확정 (VERTICAL_LINK 노드 + STAIR/ELEVATOR 간선).
  5. 가중치 정책 — 거리 기반인가 소요시간 기반인가, 계단 패널티 수치.
  6. 접근성 라우팅(ACCESSIBLE 모드) 1차 포함 여부.
  7. 회전 임계값 ε, τ 초기값.
  8. 좌표계 / PostGIS 도입 여부.
  9. 데이터 입력 도구 1차 범위 포함 여부.
  10. 별칭 범위 — 건물 별칭 외에 학과명·시설명까지 alias로 둘지.
  11. 다국어(영문 명칭) 지원 여부.

10. 권장 진행 순서

  1. 도메인 모델 확정 (Building / Place / Node / Edge 스키마, JSONB 경계).
  2. 시드 데이터로 작은 그래프 + 장소 셋 구성.
  3. Dijkstra → 노드열 출력까지 구현·검증.
  4. 지시 추출(step 변환) + Response 설계 구현.
  5. SearchToken 생성 로직 + Trie 자동완성 구현.
  6. 정규화/중복 제거 엣지 케이스 보강.
  7. (선택) 접근성 모드, 데이터 편집 도구.