보안관제자동화 에이전트 회고 1주차 - Kotlin + Spring Boot + PostgreSQL + pgvector를 선택한 이유
목차
Kotlin + Spring Boot를 선택한 이유
가장 크게 작용한 건 개발 속도였다. Spring Boot는 인증, 세션, RBAC, 검증, 에러 처리, API 문서화까지 컨벤션이 이미 잡혀 있는 프레임워크다. 이 컨벤션 위에서 기능을 쌓는 것과, 그 컨벤션 자체를 하나씩 손으로 조립하는 것의 속도 차이는 팀 규모가 작을수록 커진다. 인증 미들웨어, RBAC 가드, 요청 검증, 에러 응답 포맷 같은 걸 매번 새로 설계하지 않고 Spring Security·Bean Validation·@RestControllerAdvice 위에 얹는 쪽을 택했다.
JDK 21의 가상스레드(Loom)도 이 선택에 들어간다. 요청마다 스레드를 점유하는 전통적인 blocking I/O 모델의 처리량 한계를, 리액티브 프로그래밍 모델로 전환하지 않고도 가상스레드로 우회할 수 있다. Spring MVC 코드를 그대로 두고 런타임 레벨에서 동시성 이득을 가져가는 방식이라, 코드 복잡도를 늘리지 않고 처리량을 확보했다.
그리고 결정적으로, Spring Boot는 OpenTelemetry를 액추에이터 모듈에서 기본 지원한다. 공식 문서는 이렇게 적는다.
Spring Boot’s actuator module includes basic support for OpenTelemetry.
SdkTracerProvider, SdkMeterProvider 같은 빈이 있으면 자동으로 등록되고, Resource 빈도 자동 구성된다. 다만 이 지원에는 한계도 명시돼 있다.
Spring Boot does not provide automatic exporting of OpenTelemetry metrics or logs. Exporting OpenTelemetry traces is only auto-configured when used together with Micrometer Tracing.
메트릭·로그 자동 export는 없고, 트레이스 export도 Micrometer Tracing과 같이 써야 자동 구성된다는 뜻이다. 그래도 처음부터 계측 레이어를 설계할 필요 없이 표준 빈 위에서 시작할 수 있다는 게 핵심이었다. 앞서 관측 스택(OTel Collector → Loki·Tempo·Prometheus)을 OTLP 표준으로 세워뒀는데, 애플리케이션 프레임워크가 그 표준을 기본으로 말해준다는 건 별도 계측 레이어를 처음부터 설계할 필요가 없다는 뜻이다. 관측 스택을 먼저 구성해 둔 순서가, 그 스택과 마찰 없이 맞물리는 백엔드 프레임워크를 고르는 데 실제로 영향을 줬다.
PostgreSQL을 선택한 이유
관계형 데이터 저장소로 처음부터 PostgreSQL을 썼고, 이후 배포 버전을 16-alpine에서 17로 통일하는 작업이 한 번 있었다. 이유는 단순했다. 로컬 개발 환경과 테스트는 이미 17을 쓰고 있는데 배포 차트만 16-alpine으로 뒤처져 있었다. “코드가 검증된 버전”과 “실제로 배포되는 버전”이 다르면, 로컬에서 통과한 테스트가 배포 환경에서 재현되지 않을 여지가 생긴다. 그 격차를 없애는 게 이 변경의 전부였다.
여기에 이미지 계열도 alpine(musl)에서 debian(glibc)으로 같이 바꿨다. 로케일·ICU 처리가 레퍼런스 구현에 가까운 debian 계열이 낫다는 이유였고, 이 결정에는 이후 벡터 확장을 붙일 때 debian 계열이 더 안전하다는 전망도 함께 있었다. 즉 PostgreSQL 버전·이미지 계열을 정리한 시점에 이미 벡터 확장을 붙일 계획이 같이 고려되고 있었다.
별도 벡터 DB 대신 pgvector를 선택한 이유
시맨틱 룰북 매칭(규칙 텍스트를 임베딩으로 바꿔서 유사도로 찾는 기능)을 붙일 때, 벡터 검색 전용 시스템을 새로 운영 대상에 추가하는 방법과, 이미 쓰고 있는 PostgreSQL에 pgvector 확장을 얹는 방법 중에서 후자를 택했다.
pgvector는 PostgreSQL extension으로 설치되고(CREATE EXTENSION vector), HNSW·IVFFlat 두 인덱스 타입을 지원한다. 공식 문서는 HNSW를 이렇게 설명한다.
An HNSW index creates a multilayer graph. It has better query performance than IVFFlat.
이 판단에는 단일 노드·전담 인프라 인력 부재라는, 인프라 스택을 고를 때와 같은 조건이 깔려 있다. 벡터 DB를 새로 하나 더 운영하면 백업·모니터링·장애 대응 대상이 하나 더 늘어난다. pgvector는 기존 PostgreSQL 인스턴스 안에서 vector 타입 컬럼과 HNSW 인덱스로 유사도 검색을 처리하기 때문에, 운영 대상이 늘지 않는다. 동시에 벡터 데이터가 그 벡터와 연결된 관계형 레코드(어떤 규칙에 속한 임베딩인지)와 같은 트랜잭션 경계 안에 있다는 것도 이점이다. 별도 시스템으로 쪼갰다면 두 저장소 간 정합성을 별도로 관리해야 했을 것이다.
구현은 코사인 유사도 기준 HNSW 부분 인덱스로 top-k 조회를 하는 형태로 붙었고, 기능 자체는 플래그로 기본 꺼둔 채 도입해서 영향 범위를 켜고 끄는 스위치로 통제했다. 새 기능을 붙일 때 기본값을 안전한 쪽(비활성)에 두고 검증 후에 켜는 원칙이 여기서도 그대로 적용된 것이다.
판단 기준
개발 속도가 우선순위일 때는 컨벤션이 이미 잡힌 프레임워크가 유리하다. 그 컨벤션을 손으로 다시 조립하는 비용은 팀이 작을수록 상대적으로 커진다.
인프라를 먼저 구성했다면, 그 인프라와 마찰 없이 맞물리는 프레임워크를 고르는 것도 기술 선택의 일부다. 관측 스택을 OTLP 기준으로 세워뒀다면, 그 표준을 기본 지원하는 프레임워크를 고르는 게 별도 계측 레이어를 설계하는 것보다 싸다.
새 데이터 종류(벡터)가 필요해졌을 때, 새 시스템을 추가할지 기존 시스템을 확장할지는 운영 부담으로 갈린다. 전담 인프라 인력이 없는 조건에서는 운영 대상을 늘리지 않는 확장(같은 DB 안의 확장 기능)이 새 시스템 도입보다 우선한다.
버전·이미지 계열 정리 같은 사소해 보이는 변경도 나중의 확장을 미리 고려해서 결정될 수 있다. debian 계열로의 전환이 그 시점 문제(로케일 불일치) 해결이면서 동시에 이후 벡터 확장을 위한 사전 정지 작업이었던 것처럼.
References
- Spring Boot Actuator, OpenTelemetry support: https://docs.spring.io/spring-boot/reference/actuator/observability.html
- pgvector (HNSW, IVFFlat index types): https://github.com/pgvector/pgvector