확률은 갈아끼우고 판정은 코드에 — nestly를 지은 얼개
1편에서 nestly의 목적을 공간과 가구의 치수 재사용으로 정리했다. 이번에는 그 목적을 코드로 옮기며 나눈 책임을 다룬다. 모델이 이미지를 읽고, 사용자가 형상과 치수를 확인하고, 편집기가 배치를 계산하는 과정은 서로 다른 입력과 검증을 요구했다.
처음에는 이를 “확률은 인터페이스 뒤에, 판정은 코드에”라고 설명했다. 하지만 모델이 만든 숫자를 코드로 합산하는 것만으로는 그 숫자가 맞는지 알 수 없다. 검수 시점인 2026년 9월 22일의 구현을 기준으로, 초기 구상과 현재 동작을 구분해 살펴본다.
웹·서버·데이터베이스, 그리고 별도 저장소
구현된 주요 구성은 Vite·React·TypeScript 기반 웹, Spring Boot·Java 기반 API 서버, PostgreSQL 데이터베이스다. 모바일 폴더에는 React Native 앱을 만들겠다는 README만 있다. 따라서 웹·API·모바일 세 개를 동작하는 3계층이라고 부르는 것은 맞지 않는다. 모바일과의 컴포넌트 공유도 아직 구현된 기능이 아니다.
저장소도 하나가 아니다. 상위 폴더는 문서 저장소이고 웹·API·모바일은 각자 독립된 Git 저장소다. 문서 저장소의 .gitignore는 세 코드 폴더를 제외한다. 로컬에서는 같은 상위 폴더 아래 보이지만, 문서 저장소만 내려받으면 웹과 API 코드는 따라오지 않는다. 이 차이는 뒤에서 CI 실행 경로의 문제로 이어진다.
언어보다 구체적인 책임을 정한다
Java 서버는 공간과 가구를 저장하고, 사용자 소유권과 요청 형식·참조 관계를 검사한다. 배치 저장 서비스는 좌표와 가구 참조를 검증하지만 초록·노랑·빨강의 기하 판정을 다시 수행하지 않는다. 요청에 포함된 판정 스냅샷도 표시용으로 저장한다. “서버가 저장했다”와 “서버가 배치의 적합성을 확인했다”는 서로 다른 상태다.
웹에서는 사용자가 가구를 옮길 때 위치와 회전을 반영해 기하 함수를 호출한다. 판정에 쓰는 좌표는 cm로 맞추지만 인식 결과와 편집용 형상에는 정규화 좌표도 존재한다. 이 둘을 변환하는 경계가 필요하다. TypeScript를 선택한 것만으로 단위 혼동이 막히지는 않는다.
실제 Rect의 좌표와 크기는 모두 number다. 같은 필드 구조를 가진 정규화 좌표도 이 타입에 대입할 수 있다. TypeScript는 구조를 기준으로 타입의 호환성을 판단하므로, 단위를 구분하려면 별도 타입 설계나 변환 함수의 검사·테스트가 필요하다. 이번 검수에서도 정규화 값을 Rect에 대입하는 코드는 컴파일됐고, 좌표에 문자열을 넣은 코드는 거부됐다. (TypeScript 타입 호환성 문서)
상태 관리는 서버 응답에 TanStack Query를 쓰고 화면 상태에는 React의 상태와 Context를 쓴다. 별도의 범용 상태 관리 도구를 추가하지 않은 선택이지, 상태 관리 라이브러리를 전혀 쓰지 않는 구조는 아니다.
모델을 교체할 때 함께 바뀌는 것
평면도 인식에는 시각·언어 모델(Vision-Language Model, VLM)을 사용한다. 로컬 모델 호출은 VlmService 뒤에 있고 호스팅 경로에는 VlmProvider 계약이 있다. 다만 현재 조정 서비스는 NvidiaVlmProvider 구현체를 직접 참조해 분기한다. 인터페이스 하나만 구현하면 모든 모델이 자동 등록되는 구조는 아니다.
응답 형식도 같지 않다. 호스팅 어댑터는 모델이 반환한 방의 위치를 공통 결과 형식으로 옮기고 방 식별자를 만든다. 이 경로에서 문과 창은 빈 배열로 반환한다. 공통 타입으로 변환됐다는 이유만으로 로컬 경로와 같은 정보를 얻었다고 볼 수 없다. 모델을 바꿀 때는 인식 품질뿐 아니라 누락되는 필드와 후속 화면의 처리도 확인해야 한다.
비용을 고려해 로컬 모델과 호스팅 경로를 선택할 수 있게 했지만, 무료 사용 가능 여부를 이 구조의 고정된 전제로 삼지는 않는다. 현재 설정의 기본값과 키 주입 방식은 다음과 같다.
vlm:
provider: ${VLM_PROVIDER:ollama}
nvidia:
api-key: ${NVIDIA_API_KEY:}
VLM_PROVIDER=nvidia로 설정해도 요청의 외부 전송 동의가 있어야 호스팅 제공자를 호출한다. 키가 비어 있으면 호스팅 호출을 건너뛴다. 인식 실패 처리는 경로마다 다르다.
| 선택 경로 | 실패 시 동작 |
|---|---|
nvidia + 동의 있음 |
호스팅 호출 실패 → 로컬 시도 → 로컬도 실패하면 빈 결과 |
nvidia + 동의 없음 |
호스팅 제공자는 호출하지 않고 로컬 시도 → 실패하면 빈 결과 |
기본 ollama |
로컬 호출 실패를 예외로 전달 |
여기서 로컬이라는 설명은 기본 호출 주소가 localhost일 때를 뜻한다. 주소를 외부 서버로 바꿨다면 실제 전송 대상도 달라진다. 빈 결과 역시 인식 성공이 아니라 사용자가 템플릿이나 직접 입력으로 이어갈 수 있게 하는 신호다. 이미지 파일을 읽거나 결과를 저장하는 과정의 실패까지 이 폴백이 처리하지는 않는다.
상태 계산과 기하 검증은 다른 일이다
초기 백엔드에는 모델이 배치 설명을 생성하고 또 다른 모델 호출이 위반 개수를 세는 흐름이 있었다. 서버는 모델이 반환한 합격 문구 대신 위반 개수로 상태를 정했다. 다음은 2026년 7월 19일 코드에 있던 메서드다.
private static VerificationStatus deriveStatus(Map<String, Integer> verifierLog) {
int sum = verifierLog.values().stream().mapToInt(Integer::intValue).sum();
return sum == 0 ? VerificationStatus.passed : VerificationStatus.fallback;
}
이 메서드가 보장하는 것은 주어진 개수와 상태의 일관성이다. 모델이 실제 충돌을 놓쳐 0을 반환하면 코드도 통과로 분류한다. 당시 파서에는 누락되거나 해석하지 못한 개수를 0으로 바꾸는 처리도 있었다. 과거 코드의 복사본에 검토 항목은 있지만 위반 기록이 없는 응답을 넣었을 때도 passed가 나왔다. 따라서 이 경로를 독립적인 배치 검증으로 설명할 수는 없다.
이 AI 생성·검토 경로는 7월 24일에 제거됐다. 현재 편집기의 기하 함수는 입력된 점유 영역, 경계, 여유 공간으로 결과를 계산한다. 모델이 세어 준 개수에 의존하지 않지만 입력 치수가 틀리거나 계산에 없는 조건은 알아낼 수 없다. 1편에서 정리한 입력 신뢰도와 판정 범위의 한계는 여기에도 적용된다.
스키마와 CI에서 확인한 구현 상태
초기 네 엔티티 계획에서 시작한 모델은 현재 소스의 JPA 엔티티 기준 11개로 늘었다. 공간과 가구 외에도 계정, 동의, 즐겨찾기, 배치 저장 등을 다룬다. Flyway 마이그레이션 파일은 V1부터 V27까지 27개다. 이는 저장소에 있는 파일 수이며 특정 데이터베이스에 적용된 횟수를 조회한 결과는 아니다.
되돌린 설계도 파일에 남아 있다. V21에서 추가한 공간의 너비·깊이 컬럼을 V25가 삭제한다. 방별 형상은 별도의 데이터 구조로 옮겨졌다. 이처럼 추가와 제거 기록을 함께 읽으면 어떤 필드를 현재 계산에 사용하는지 추적할 수 있다.
배포 준비와 배포 완료도 구분해야 한다. 서버에는 데이터베이스 접속 정보를 환경변수로 받는 application-prod.yml 틀이 이미 있다. 이 파일만으로 운영 배포가 완료됐다고 판단할 수는 없으며, 이번 검수에서는 운영 환경의 구동 여부까지 확인하지 않았다.
지속적 통합(Continuous Integration, CI) 워크플로 두 개는 문서 저장소에 있다. 웹·API 폴더를 작업 경로로 지정하지만 다른 저장소의 소스를 가져오는 단계는 없다. GitHub Actions의 작업 경로 설정이 누락된 코드를 가져다주지는 않으므로, 문서 저장소만 체크아웃한 상태로는 이 구성을 실행할 수 없다. (GitHub Actions 워크플로 문법)
9월 22일 GitHub API로 조회한 문서 저장소의 Actions 실행 기록은 0건이었다. 이 조회로 과거 실행 기록의 삭제 여부까지 알 수는 없고, 저장소 분리만이 미실행의 원인이었다고 단정할 수도 없다. 확인된 문제는 현재 워크플로의 경로와 체크아웃 구성이 맞지 않는다는 점이다. 자동 검사를 연결하려면 각 코드 저장소로 워크플로를 옮기거나, 필요한 저장소를 명시적으로 내려받아야 한다.
학습: 교체 가능성과 검증 범위를 따로 확인한다
인식 호출을 분리해 두면 모델 교체의 영향을 줄일 수 있다. 그래도 응답 형식, 전송 동의, 실패 처리까지 자동으로 같아지지는 않는다. 판정 역시 코드로 옮겼다는 사실보다 어떤 입력을 독립적으로 확인하는지가 중요했다. 이번 검수에서는 관련 기존 단위 테스트와 추가 재현으로 호출 분기·폴백 범위·과거 상태 계산을 확인했으며 실제 모델의 정확도를 측정하지는 않았다.
현재 구현의 책임과 남은 과제도 나뉜다. 서버는 저장 권한과 데이터 관계를 확인하고, 웹은 변환된 좌표로 배치를 계산한다. CI는 이 코드를 실제로 가져와 검사하도록 연결해야 한다. 다음 편에서는 정규화 좌표를 실측 좌표로 옮기면서 겪은 문제와 기하 판정의 조건을 더 자세히 다룬다.