Backend
#
2025-09-08 ⋯ Kubernetes #1 Pod, Port-forward
1. 실습환경설정 - 필요 패키지 - kubectl, jq, curl, maven, Java - kubectl - Kubernetes 클러스터와 통신하는 CLI 도구 - 쿠버네티스는 여러 개의 프로그램이 동시에 돌아가는 큰 시스템이고 여기에 지시를 내리는 도구. - Java 17 - 여러 프로그램을 실행하는 공통 실행 환경(JVM)을 제공 - 공통 실행 환경? - 여러 프로그램을 공통 언어로 사용하게해준다. - 프로그램들이 Java가 어디 있는지 알아야 하니까 JAVA_HOME이라는 환경 변수를 설정해준다. - 클라우드 인증 정보, 커맨드 스크립트 다운로드 - 자주 쓰는 커맨드 모음이라고 하는데 sh 파일들이 들어있었다 - 셸 시작할 때 자동으로 실행되도록 환경 변수 설정 - 터미널을 켤때마다 자동으로 설정이 적용되게. 2. 실습 코드 다운로드 - 파일 구조 3. 실습1 - Pod, Service, Deployment - Harbor Image Registry - SKALA 환경에서는 Docker Hub 대신 자체적으로 관리하는 Harbor Image Registry 사용 - Docker Hub가 전 세계가 공유하는 큰 창고라면 arbor는 특정 조직 내부에서 운영하는 전용 창고이고 각자가 만든 Docker 이미지를 올리고다운받을수있다. - 로그인 방법 - 웹 콘솔 접속 - 브라우저에서 https://amdp-registry.skala-ai.com에 들어가서 계정과 비밀번호를 입력 - CLI에서 docker login 명령으로 로그인 - `docker login amdp-registry.skala-ai.com/skala25a` - robot 계정과 발급받은 토큰을 사용 - 로그인 안하면? - 도커 이미지를 빌드하고 push할 때 인증 문제가 발생한다. - (chatgpt에 치면 dockerhub로 유도한다) Pod 배포 - Pod는 Kubernetes에서 가장 작은 실행 단위. - 하나의 애플리케이션이 들어있다. - nginx라는 웹 서버 이미지를 Pod로 실행한다. - kubectl run - 이름을 sk019-nginx로 지정 - 사용할 이미지 nginx 설정 - 80 포트를 열기 - kubectl get pod - 해당 Pod가 잘 뜨는지 확인 - 결과? - 컨테이너를 Kubernetes 환경 위에 올렸다. Pod 연결 (port-forward) - Pod가 실행됐으니까 외부에서 접속할수있게하려면? - port-forward로 로컬 PC의 특정 포트와 Pod 내부의 포트를 직접 연결한다. 예를 들어 로컬 8080 포트를 Pod의 80 포트와 연결하면 브라우저에서 localhost:8080으로 접속했을 때 Pod 안의 nginx 서버와 통신할 수 있다. Service 연결 - Pod는 Kubernetes에서 실행되는 최소단위인데 수명이 아주 짧다. - Pod가 죽으면 Kubernetes는 자동으로 새로운Pod를띄우는데 이때새로만들어진Pod는 이름이랑 IP주소가 달라진다. - 예를들어 오늘은 sk001-nginx라는 Pod에 10.0.1.3 같은 IP가 있었는데 내일은 sk001-nginx-abc123라는 새 Pod가 10.0.1.7 같은 주소를 가질수있고 그래서 Pod에 직접 붙는 방식은 오래쓸수가 없다. - Service는 특정 label(예: app=nginx)이 붙은 Pod들을 자동으로 찾아 연결해줘서 Pod가 교체되더라도 항상 같은 주소로 접속할 수 있게 해준다. - 예를들어 `kubectl expose pod sk001-nginx --port=8080 --target-port=80`를 하면 클러스터 안에서 8080 포트로 들어오는 요청을 자동으로 Pod의 80 포트로 전달해주는 Service가 생성된다. - Pod의 IP나 이름이 바뀌어도Service가 그걸 대신 추적해서 연결해줌. - 그래서 port-forward와 Service 연결의 차이? - port-forward는 임시로 내 PC와 특정 Pod 사이를 직접 연결하는 것 디버깅이나 빠른 테스트 때는 편리하지만 Pod가 재시작하면 연결이 끊긴다. - Service는 안정적인 네트워크 자원으로 Pod가 몇 번 바뀌든 항상 같은 주소로 접근할 수 있게 해준다. Pod manifest를 사용한 배포 - 지금까지는 kubectl run 같은 명령어로 직접 Pod를 띄웠는데 예를 들어 kubectl run sk001-nginx --image=nginx라고 하면 곧바로 Pod가 생성되었다. - 이렇게하면 문제가 매번 명령어를 새로 쳐야 해서 사람이 실수할 수 있고 누군가는 포트를 빼먹고 누군가는 이름을 다르게 적어서 환경이 제각각이 될수있다. - manifest파일을 사용해서 pod가 어떤 이름을 가질지 어떤 이미지를 쓸지 몇 개를 띄울지 환경 변수는 뭔지 등을 작성하고 이를 사용해서 pod를만든다. - pod.yaml - sk001-pod-test라는 이름의 Pod를 만드는데 안에는 nginx 컨테이너가 들어있고 80번 포트를 열고 USER_NAME이라는 환경 변수에 sk001을 넣는다. - env.properties - 설정값 세팅 파일 - gen-yaml.sh - 원래는 pod.t만 있었고 gen-yaml.sh을써서 pod.yaml을 생성한다 Pod manifest로 배포 - sk019-pod-test Pod 생성 - 저때는 ContainerCreating이었는데 곧 Running됐을듯. - sk019-nginx Pod는 이전에생성한 nginx Pod. Pod 삭제 후 Deployment 배포, 재생성 확인 - sk019-pod-test Pod를 지우고 deploy.yaml을 적용해서 sk019-deploy-test Deployment를 생성, Deployment가 내부적으로 새로운 Pod를 하나 띄운다. - deployment가 sk019-deploy-test-7d5b5cfd56-l2djw를 띄웠다. - sk019-deploy-test-7d5b5cfd56-lk6m5를 삭제하면? - 단일 pod으면 그냥없어지는데 - Deployment로 관리되는 Pod는 Kubernetes가 “이 Deployment는 Pod 1개를 유지해야 해”라는 선언을 기억하고 있기 때문에 방금 삭제하자마자 새로운 Pod를 곧바로 생성한다. - 지웠는데도 sk019-deploy-test-7d5b5cfd56-l2djw가 ContainerCreating. (곧 Running) 4. 실습2 - 쿠버네티스 배포 Spring Boot 컨테이너 만들기 - JAR 빌드 - Maven으로 jar 빌드 - 수행하면 target/ 아래에 spring-boot-app-0.0.1-SNAPSHOT.jar 같이 JAR가 생긴다 - 도커 이미지 빌드, 푸시 - 왜 push가 필요하냐면 쿠버네티스 노드가 이미지를 가져갈 주소가 Harbor 레지스트리이기 때문이야 로컬 도커 데몬에만 있으면 클러스터가 못 본다. - 뭔말이냐면 - 내가 노트북에서 docker build로 이미지를 만들면 결과물은 내 로컬 도커 엔진 안에만 저장돼있고 내 컴퓨터 안에서만 그 이미지를 쓸 수 있는데 - 쿠버네티스 클러스터의 Pod는 내 노트북에서 실행되는 게 아니라 클러스터 안의 노드 서버들에서 실행된다. 쿠버네티스가 Pod를 만들 때 nginx:latest 이미지를 가져와서 컨테이너를 띄워라 << 이렇게 노드에 지시하는데 - 여기서 노드는 이미지를 가져올 저장소 주소가 필요하다. 기본적으로는 Docker Hub 같은 공개 레지스트리를 보거나 따로 지정된 Harbor 같은 사설 레지스트리를 본다. - 내가 만든 이미지를 Harbor 레지스트리에 push하지 않으면 이미지가 노트북 로컬 Docker 안에만 있으니 쿠버네티스 노드들(클러스터)은 그 이미지를 찾을 수 없고 Pod 상태가 ImagePullBackOff로 빠진다. - 결론 - build만 하면 내 노트북 안에만 있고 - push까지 해야 Harbor 레지스트리에 올라가서 쿠버네티스 노드들이 거기서 이미지를 pull해서 컨테이너를 실행할 수 있다. FastAPI 컨테이너 만들기 Harbor에 정상 등록됐는지 확인 https://amdp-registry.skala-ai.com/ 접속해서 떠있는지보기. 쿠버네티스 배포 - 배포? - 내가 만든 이미지를 클러스터에서 실행 가능한 애플리케이션으로 올리기. - 배포정보 - deploy.yaml - 어떤이미지를 쓸건지 (image: amdp-registry.../sk019/myapp:latest) - 몇 개의 Pod를 유지할 건지 (replicas: 1) - 어떤 포트를 열 건지 (containerPort: 8080) - 라벨(sk019-myfirst-api-server) - 배포명령실행 1. kubectl은 API Server에 deploy.yaml 내용을 전달 2. API Server는 etcd(쿠버네티스 데이터 저장소)에 “이런 Deployment를 유지하라”라는 선언을 저장 3. 스케줄러가 클러스터 노드 중 하나를 선택, 해당 노드의 kubelet이 “이 Pod는 이 이미지를 써야 해”라고 파악한 뒤, 컨테이너 런타임(docker/containerd)이 Harbor 레지스트리에서 이미지를 pull해온다. 4. 이미지가 잘 내려받아지면 컨테이너가 시작되고, Pod 상태가 Running으로 바뀐다. - 네트워크 구성, pofr-forward 실행 - Pod는 내부 IP가 매번 바뀌기 때문에 Pod가 뜨더라도 외부에서 바로 접속할 수는 없고 그래서 service.yaml로 Service 리소스를 만들고 label을 기준으로 Pod와 연결. - 포트 포워딩 - Service가 생겼다면, 로컬에서 테스트할 수 있도록 포트를 터널링한다. http://localhost:8080/api/users로 접속하면, 사실은 클러스터 안 Pod까지 트래픽이 전달된다. - http://localhost:8080 접속해보면 제대로 뜬다. 포트포워딩 의문점1 - 포트 포워딩이 그래서 하는것은? - 쿠버네티스 Pod는 클러스터 내부 네트워크(IP 대역)에서만 접근 가능하고 내 노트북 브라우저에서 직접 Pod IP를 찍어도 접근이 안됨. 즉 내 PC -> 쿠버네티스 클러스터로 가는 길은 막혀있다. - port-forward는 임시 터널로써 `kubectl port-forward` 명령을 쓰면 내 PC의 포트와 클러스터 안 리소스(Pod 또는 Service)의 포트를 직접연결한다. - `kubectl port-forward svc/sk019-service 8080:8080`하면 내 PC 로컬 8080 포트로 들어오는 요청을 클러스터 안 sk019-service의 8080 포트로 바로 보내는 터널을 만든다. 포트포워딩 의문점2 - http://localhost:8080/api/users로 접근하면 클러스터 안 Pod까지 간다? - 포트포워딩이 걸린 상태에서 `http://localhost:8080/api/users`로 접속하면 - 브라우저는 “로컬 8080”으로 요청을 보냄 - kubectl이 이 요청을 가져가서 클러스터 안 Service -> Pod으로 전달 - Pod 안의 Spring Boot 애플리케이션이 /api/users 요청을 처리하고 응답을 돌려줌 - 응답이 다시 포트포워딩을 통해 내 PC의 브라우저로옴 - 결과적으로는 - 내 PC localhost:8080에 접속한 것처럼 보이지만, 실제 "처리"는 클러스터 안 Pod가 한다. - 결과적으로는에서 말하는 "처리"란? - 브라우저 주소창에 http://localhost:8080/api/users를 입력 -> 브라우저가 HTTP 요청 패킷을 생성해서 내 PC의 8080 포트로 보냄 -> kubectl port-forward가 이 요청을 받아서 클러스터 안으로 전달(Kubernetes Service안으로 던짐) - 클러스터 안에서? - Service가 label로 연결된 Pod를 찾아서 트래픽을 넘김(label로 연결된 Pod = Spring Boot 컨테이너가 들어 있는 Pod) -> Pod 안에는 내가 만든 Spring Boot 애플리케이션이 실행 중. - Pod 안에서? - 컨테이너 안에서 Java 프로세스가 떠 있고, 8080 포트를 열어놓고 있다. Spring Boot는 /api/users라는 URL 요청을 Controller 클래스에 매핑해 둔다. 예를 들어 UserController라는 클래스에 @GetMapping("/api/users")가 있다면, 요청이 오면 그 메서드가 실행되고 JSON 응답(예: [{id:1, name:"Alice"}, {id:2, name:"Bob"}])을 생성해서 HTTP 응답으로 내보낸다. Pod가 만든 응답은 Service -> port-forward 터널 -> 내 PC의 localhost:8080을 거쳐 브라우저로 돌아온다. - 결론 - 브라우저 입장에서는 그냥 로컬에서 프로그램이 실행된 것처럼 보이지만 실제로는 클러스터 안 Pod가 로직을 수행하고 응답을 돌려준것. - 요청 = /api/users - 처리 = Spring Boot 애플리케이션이 Controller/Service/Repository를 통해 데이터 조회/가공 - 응답 = JSON 결과를 브라우저로 반환
2025-09-02 ⋯ DBMS 및 SQL 활용 #5 Vector DB 스키마 설계
1. 개념 KNN vs ANN KNN과 ANN의 공통 목적 - 질문을 하고 그 질문과 비슷한 질문이나 답변을 데이터베이스에서 찾기 구현 차이 - 모든 데이터를 하나하나 다 비교해서 가장 가까운 것을 찾는다(KNN) - 데이터 전체를 다 비교하지 않고 인덱스를 이용해서 후보군을좁혀서 그 안에서만 비교(ANN) - 친구가 수십만 명 있으면 모든 친구에게 질문을 던져서 과거 답변을 확인하는 대신 비슷한 취향을 가진 대표 그룹 몇 개를 빠르게 찾고 그 안에서만 가장 가까운 답을 고르는 방식. 그러면 인덱스는 비슷한취향그룹 찾는데만 쓰고 그룹 안에서는 knn인가? - 맞음 - 스텝(툴): 후보군 좁히기(ann) -> 후보군 내부 검색(knn 등) DB의 목적 일반적인 db는? - 숫자, 문자열 같은 정형화된 값을 행과 열로 저장하고 필터링과 조인을 수행해서 원하는 정보를 뽑아낸다. 원하는 정보 뽑아내기? - 조건에 맞는 행만 걸러내기 - ex) 나이가 20세 이상인 학생만 찾기 (`SELECT * FROM 학생 WHERE 나이 >= 20;`) - 서로 다른 테이블을 연결해서 더 풍부한 정보 만들기 - ex2) 학생 & 수강 테이블 조인을 통해 "홍길동 학생이 수강하는 과목 목록" 같은 테이블 만들기 (`SELECT 학생.이름, 수강.과목명` `FROM 학생` `JOIN 수강 ON 학생.학번 = 수강.학번;`) 일반적인 db와 벡터 db의 차이 일반적인 DB는 정확한 값을 기준으로한다. - 예를들면 학생 이름이 "홍길동"인 데이터를 찾고 싶다면 `WHERE 이름 = '홍길동'` 같은 조건을 써서 완전히 일치하는 값을 찾는다. - "값이 같은지 여부"라는 불(boolean) 논리에 기반해 검색과 조인을 수행. 벡터 db는 정확한값이 아니라 "얼마나 비슷한가"라는 정도를 계산한다. - "얼마나 비슷한가" 기준? - 벡터 간 distance 또는 similarity - 텍스트, 이미지, 오디오 같은 데이터는 숫자 하나로 일치 여부를 판별할 수 없기 때문에 임베딩을 통해 벡터 공간에 투영한 뒤 그 벡터가 서로 얼마나 가까운지를 측정한다. - 예를들면 "강아지"라는 단어를 검색했을 때 정확히 "강아지"라는 텍스트만 주는 게 아니라 "개", "강쥐", "멍멍이" 같은 비슷한 개념을 함께 찾아줄수있다. 메타데이터 벡터 검색만 하면 - 비슷한 벡터를 찾아줄 뿐 의미는 알려주지못함. - 비슷한 벡터를 찾을때 모두 가져올 뿐 날짜 등 필터링은 못함. 메타데이터가 있으면 - 사용자가 입력한 텍스트와 비슷한 문서를 벡터 검색으로 찾고 그 문서의 제목·저자·링크 같은 메타데이터를 함께 보여줄수있다 - 벡터 유사도로 후보를 먼저 고른 뒤 메타데이터로 Query Filtering을 하면 사용자가 원하는 결과를 정확히 얻을 수 있다. 동적 업데이트 -> 데이터가 계속 들어오거나 수정될때를 고려 Incremental Indexing(점진적 인덱싱) - HNSW - 그래프기반 인덱스 구조 - 데이터가 노드, 비슷하면 엣지가있음 - 새로운벡터가 들어오면 그벡터가 노드가 됨 즉 새로운 데이터(벡터)가 들어와도 기존 그래프(인덱스)가 유지돼서 데이터가 계속들어와도 검색 성능이 떨어지지 않으면서 반영된다. Lazy Update(지연 업데이트) - 새로운벡터가 들어와도 즉시 반영하지않고 일정 시간이 지나면 한꺼번에 인덱스에 반영 - 자원을효율적으로 쓸수있다. Delete & Rebuild(삭제후 재구성) - 시간이 지나면 쓸모없는 데이터가 쌓이기때문에 일정 시간이 지나면 불필요한 벡터는 지우고 인덱스를 재정리해서 최적화해야 검색 속도가 유지되고 공간 낭비를 막을수있다. 결론 - 평소에는 Incremental Indexing과 Lazy Update로 작은 변화들을 처리하다가 주기적으로 Delete & Rebuild를 해서 전체 구조를 최적화한다. Chunking(청킹) 모델은 한 번에 처리할 수 있는 길이에 제한이 있고 긴 텍스트를 그대로 벡터화하면 중요한 부분이 묻힌다. - 그래서 청킹해서 데이터를 자른다 고정 크기 방식 (Fixed Size Chunking) - 1,000자짜리 문서를 200자로 잘라 5개로 만들기. - 간단하고 구현이 빠른데, 문장이 잘리거나 의미가 끊길 수 있다. 의미 기반 방식 (Semantic Chunking) - 단순히 길이가 아니라 내용의 의미 단위 즉 문단, 주제, 혹은 문맥이 바뀌는 지점에서 나눈다. 덩어리 하나가 온전한 의미를 담고 있어 검색이나 답변 생성에서 품질이 좋아진다. 중첩 방식 (Overlapping Chunking) - 데이터를 자를 때 앞 조각과 뒤 조각이 일부 겹치도록 하는 방식, 예를 들어 200자 단위로 자르되 다음 청크는 앞에서 50자를 다시 포함시키는데 이렇게 하면 문맥이 잘려 나가는 문제를 줄일 수 있다. 요약 기반 방식 (Summarization Chunking) - 긴 텍스트를 직접 다루기 힘들 때, 아예 요약을 해서 작은 덩어리로 줄여서 검색할 때는 요약된 덩어리만쓰는건데 검색 속도가 빨라지고 컨텍스트 길이를 절약할 수 있지만 요약 과정에서 중요한 세부 정보가 사라질 수 있다. 계층적 방식 (Hierarchical Chunking) - 텍스트를 먼저 큰 단위(챕터)로 나누고, 그 안에서 절, 문단 단위로 세분화한다. - "문단 단위로 세분화" - 1장 2장으로 나누고 1장을 1.1, 1.2절로 나누고 1.1절을 첫번재문단 두번째문단 일케 나눈다. - 문단만 최종 결과물인게 아니고 1장 같은 큰 단위도 쓰고 1.1절 같은 중간 단위도쓰고 문단 같은 작은 단위도 쓰므로 따로따로 결과물로 저장한다.
2025-08-28 ⋯ DBMS 및 SQL 활용 #4 pgvector 기반 유사도 검색 + FastAPI 연동
1. 실습 시나리오 2. 코드 SQL 유사도 검색 vector_search_api.py client.py 터미널 실행 3. 코드설명 SQL 유사도 검색 - embedding_vector vector_cosine_ops - embedding_vector 컬럼을 대상으로 인덱스를 생성 - 코사인 거리(cosine distance)를 기준으로 유사도 검색을 최적화 - WITH (lists = 100) - ivfflat는 전체 벡터 공간을 리스트로 나눠서 가장 가까울가능성이 높은 그룹에서 탐색하는 기법을 쓰는데 → 100개의 리스트로 나눠 탐색한다. - embedding_vector vector_l2_ops - embedding_vector 컬럼을 대상으로 인덱스를 생성 - L2 거리(유클리드 거리)를 기준으로 유사도 검색을 최적화 - random_vector() 목적 - 성능 실험용으로 길이 384짜리 난수 벡터 생성 - array_agg(random())::vector(384) - array_agg(random()): 0 이상 1 미만 난수 384번 생성해서 384차원 배열 생성 - ::vector(384): 벡터로 변환 - 블록 목적 - LIMIT 5 vs 50, 코사인 vs L2 케이스별 실행 속도 비교 - DO $$ ... $$ - 익명 PL/pgSQL 블록 (DB에 저장되지 않는 블록) - DECLARE - 블록 안에서 사용할 Timestamp 변수 t1, t2를 선언 - BEGIN … END; - 실제 실행할 로직을 작성 - t1 := clock_timestamp(), t2 := clock_timestamp() - t1에 시작 시간, t2에 끝 시간 저장 - PERFORM id, title - PERFORM: 쿼리 실행 - LIMIT 5, LIMIT 50 - 가장 유사한 문서 5개만 찾을 때와 50개 찾을 때. - FROM design_doc ORDER BY embedding_vector <=> random_vector() - embedding_vector와 랜덤으로 만든 벡터(random_vector())의 코사인 거리를 계산해서 정렬 - FROM design_doc ORDER BY embedding_vector <-> random_vector() - embedding_vector와 랜덤으로 만든 벡터(random_vector())의 L2 거리를 계산해서 정렬 vector_search_api.py - search_vector() 목적 - 클라이언트가 벡터를 보내면 DB에서 가장 비슷한 문서들을 찾아서 반환 - @app.post("/search") - HTTP POST 요청이 /search 경로로 들어오면 search_vector 함수를 실행. - get_db_conn() - PostgreSQL 연결 생성 (psycopg2) - conn.cursor() - SQL 실행을 위한 커서(cursor) 객체 생성 - query - 입력 벡터와 가장 코사인 거리가 가까운 문서 N개를 찾는 쿼리 - embedding_vector <=> %s::vector - Python에서 넘긴 벡터 문자열을 vector 타입으로 가져오는데 정렬 기준은 코사인 거리 - "[" + ",".join(map(str, data.vector)) + "]” - 클라이언트가 보낸 vector(리스트)를 문자열로 바꿔서 PostgreSQL의 vector 타입으로 해석되게. - cur.execute(query, (vector_str, data.limit)) → rows = cur.fetchall() - 쿼리 실행 & 결과(rows) 가져옴 - return … - DB에서 가져온 튜플들을 JSON 응답 형식으로 반환 - except Exception as e: raise HTTPException(status_code=500, detail=f"DB error: {str(e)}") - DB 연결 실패, 쿼리 오류 등이 나면 500 Error 처리. client.py - cur.execute("SELECT id, title, content, embedding_vector FROM design_doc WHERE id = 1;") - 첫 번째 문서를 기준 문서로 사용할예정이므로 design_doc 테이블에서 id=1인 문서 조회 - requests.post("http://127.0.0.1:8000/search") - HTTP POST 요청: 로컬에서 실행 중인 FastAPI 서버 주소 http://127.0.0.1:8000/search로 - json={"vector": vec, "limit": 1} - 기준 문서에서 뽑아온 벡터(vec)와 가장 가까운 문서 1개 요청 - response.json()["results"][0] - 결과 리스트의 첫 번째 요소(가장 유사한 문서) 가져오기 4. 실행 결과 및 해석 성능 비교 (LIMIT 5 vs LIMIT 50) & (cosine vs L2) - LIMIT 5 vs LIMIT 50 - LIMIT 5: 9.582 ms - LIMIT 50: 4.426 ms - LIMIT 50이 LIMIT 5보다 약 5ms 더 빠르게 수행됨. - cosine vs L2 - cosine: 6.079 ms - L2: 4.114 ms - L2 연산이 cosine 연산보다 약 2ms 더 빠르게 수행됨. - 결과 해석 - LIMIT 값이 크다고 무조건 느려지지 않았는데, 실행 시간은 LIMIT 값에 비례하지 않을 수 있고 이는 ivfflat 인덱스를 사용할 때는 “몇 개를 더 읽어오느냐”보다 “인덱스에서 후보군을 어떻게 선택하느냐”가 더 중요하기 때문일수 있다 - ivfflat은 “전체 데이터를 다 보지 않고, 후보군(클러스터)만 먼저 고른 뒤, 그 안에서 정렬해서 결과를 뽑는 방식”인데 - LIMIT 값이 작든 크든 먼저 후보군을 고르고 정렬하는 과정은 거의 똑같은데 실제로 시간이 더 걸리는 건 “후보군 선택과 정렬”이지 LIMIT 5에서 5개를, LIMIT 50에서 50개를 뽑는 그 ‘추출 단계’ 자체는 별로 비중이 크지 않기 때문일 수 있다 - 그래서 LIMIT 값이 크다고 무조건 느려지지 않았던것일수있다. - L2(<->)가 코사인(<=>)보다 빠르게 나왔는데 L2 거리는 그냥 좌표 차이 제곱해서 더하는 계산이고 코사인 거리 = 내적 계산 + 벡터 크기(norm) 계산이 필요하기 때문에 연산이 더 복잡하므로 시간이 더 소요되는 것이 정상적인 결과 - 실제 서비스에서 속도만 중요하다면 L2를 쓰고 의미적 유사도(문장의 방향성)가 더 중요하다면 코사인을 쓰는 게 맞을수있다 - 벡터 길이가 384차원이고 쿼리도 정렬 기반인데 모두 10ms 이내라면 인덱스가 잘 적용되고 있는 것으로 보이고 - 인덱스가 없었다면 후보군 없이 전체 데이터를 일일이 다 비교해야 해서 시간이 훨씬 소요되는데 ivfflat이 후보군을 뽑아서 연산 범위를 줄여줬기 때문에 시간이 많이 감소하였다. FastAPI 서버 실행 및 클라이언트 실행 - 실행 내용 - DB의 id=1번 문서를 쿼리로 사용해서 가장 유사한 문서 1개를 반환했고 id=1번 문서가 반환 - 결과 해석 - 쿼리로 준 문서 벡터 id=1와 가장 가까운 것은 id=1이므로 그대로 반환 5. 개념 - ivfflat? - 일반 텍스트 검색이나 숫자 검색은 B-Tree 인덱스를 많이 쓰지만 - 벡터 검색은 고차원 벡터 간 거리 계산이 필요하기 때문에 가장 가까울 가능성이 높은 그룹에서만 검색하는 근사 최근접 탐색(ANN, Approximate Nearest Neighbor) 기반으로 유사한 데이터를 찾아서 탐색속도가 빠른 ivfflat를 쓴다. - 인덱스 생성하는 이유? - 문서 의미가 얼마나 방향이 비슷한지를 빠르게 찾기위해서. - 인덱스가 문서 의미가 얼마나 방향이 비슷한지를 빠르게 찾는데 필요한 이유? - 인덱스 없는 경우 - design_doc 테이블의 모든 행에 대해 embedding_vector와 query_vector의 코사인 거리를 계산하므로 10만 건 데이터가 있으면 10만 번의 384차원 내적 연산을 수행. - 인덱스 있는 경우 - USING ivfflat (embedding_vector vector_cosine_ops) 하면 벡터 공간을 리스트 여러개로 미리나눠두고 가장 가까울 가능성이 높은 리스트 몇 개만 선택해서 선택된 리스트 안에서만 거리를 계산한다. 비슷한 후보군 안에서만 비교하기 때문에 속도가 훨씬 빨라진다. - 익명 PL/pgSQL 블록 사용 장점? (함수나 프로시저로 저장하지 않고 일회성 코드 블록으로 실행하는 이유?) - 간단히 성능 테스트, 데이터 초기화, 실험을 할거라서 굳이 DB 객체(함수·프로시저)를 생성하고 저장할필요가 없어서 실행 후 흔적이 안남게함. - 일반 SQL로는 안 되는 로직(변수 선언, IF 조건문, LOOP 반복문)을 실행할수있어서.
2025-08-28 ⋯ DBMS 및 SQL 활용 #3 집계함수, 고급 객체기능, 고급 인덱스
1. GROUPBY GROUP BY - 테이블 안에 있는 데이터를 특정 기준으로 묶어서 요약. - 테이블 embedding_store에서 - id, user_id, cluster_id, similarity, tag 5개 컬럼이 있는데 - 있는 그대로보면 큰 그림을 보기 힘들다 즉 해석이 어렵다. - GROUP BY를 쓰면 요약 정보를 만들수있는데 - user_id로 묶으면 “사용자 A는 총 10건, 사용자 B는 총 5건” 같은 식으로 정리 / cluster_id로 묶으면 “클러스터 1은 평균 유사도가 0.8, 클러스터 2는 0.5” / tag로 묶으면 “계약 태그는 100건, 고객상담 태그는 30건” 같은 결과가 나오고 이렇게 하면 데이터의 전체 분포와 패턴을 이해할 수 있다. AI 연계? - 벡터 데이터에서 클러스터링을 하고 나면 각 클러스터의 특징을 봐야되는데 - SQL로 GROUP BY cluster_id를 해서 평균 유사도, 최소 유사도 등을 구해서 평균 유사도가 지나치게 낮은 클러스터가 발견되면 “이 클러스터는 불분명하게 묶였네” 이런식으로 클러스터를 판단할수있다 - SQL로 GROUP BY tag 해서 클러스터내 사용자별 태그 분포를 보면 어떤 사용자가 어떤 패턴을 많이 보이는지를 확인할수있다. - 이런식으로 단순 SQL 집계가 단순 통계가 아니라 이상치 탐색, 품질 저하 감지, 태그 자동 분류 같은 AI 전처리 과정에 활용 가능. Vector DB 분석에서 GROUP BY 활용 - 벡터 데이터는? - 문장, 이미지 같은 걸 임베딩해서 저장해둔 값 - 클러스터링을 하고 나면 각 클러스터가 잘 묶였는지를 확인해야 하고 이때 GROUP BY cluster_id로 묶어서 평균 유사도를 보면 클러스터를 판단할수있다 - 여기서 평균 유사도가 0.9 이상이면 잘 뭉쳐진 클러스터일 가능성이 크고 0.5 이하라면 내부 데이터가 제각각이라 불분명하게 묶인 클러스터라고 판단가능 - 이렇게 SQL 집계로 클러스터 품질을 확인할수있다. AI 결과 검증에서 GROUP BY 활용 - AI 모델이 분류 작업을 했을때 - 실제 라벨(true_label)과 예측 결과(pred_label)가 테이블에 있고 카테고리별 정확도를 구할 수 있다 - 이렇게 하면 “카테고리 A의 정확도는 0.95, 카테고리 B는 0.62” 같은 결과가 나오니까 어떤 클래스에서 모델이 잘 못 맞추는지 바로 확인할 수 있고 이는 모델 개선 포인트로 이어진다. 추천 시스템에서 GROUP BY 활용 - 추천 시스템에서는 사용자가 어떤 아이템을 자주 고르는지, 또는 어떤 유형의 아이템을 선호하는지를 분석해야 하는데 - 사용자별 선택 기록을 GROUP BY user_id나 GROUP BY item_category로 묶으면 개인의 선호를 확인 가능하다 - 이렇게 하면 “사용자 A는 주로 액션 영화를 많이 선택, 사용자 B는 로맨스 위주” 같은 패턴이 보이고 이를 활용해서 토대로 개인화 추천을 강화할 수 있다. 분류 성능 비교에서 GROUP BY 활용 - 분류 모델이 여러 개 있다면 카테고리별로 각 모델의 성능을 나란히 비교할수있다. - 이렇게 하면 “모델 A는 카테고리 X에서는 정확도가 높지만, 카테고리 Y에서는 낮다” 같은 판단(비교) 가능. 2. ROLLUP & CUBE sales_summary 테이블 - 지역(region), 제품(product), 매출액(amount) - East 지역의 A 제품 매출 100, B 제품 매출 150 / West 지역의 A 제품 200, B 제품 50 - 일반적인 GROUP BY region, product를 쓰면? - SUM()으로 합계를 계산했고 그대로 네 줄이 다시 나오면서 매출액이 합계로 정리된다 - 그런데 이렇게 하면 지역별 합계나 전체 합계를 따로 보려면 다시 쿼리를 작성해야함. - GROUP BY ROLLUP(region, product)를 쓰면? - 네 줄의 상세 데이터에 더해서 지역별 소계와 전체 합계까지 자동으로 붙는다. - East 소계: East 지역은 A 100, B 150을 합쳐 250 - West 소계: West는 A 200, B 50을 합쳐 250 - 전체 합계: 500 - 소계를 표시할 때는 product 칸이 NULL로 나타나고 전체 합계는 region과 product가 모두 NULL로 표시. - GROUP BY CUBE(region, product)를 쓰면? - 지역별 합계와 전체 합계뿐 아니라 제품별 합계도 같이 나온다. - East-A, East-B, West-A, West-B 같은 상세 데이터가 나오고 (기본 GROUP BY) - East 전체, West 전체, 그냥 전체 데이터가 나오고 (GROUP BY ROLLUP) - 제품 A 전체, 제품 B 전체 데이터도 나온다. ROLLUP과 CUBE의 차이? - ROLLUP은 계층적으로 요약 - ROLLUP(region, product)이면 - 첫 번째 컬럼(region)을 기준으로 묶고 -> 그 "안에서" 두 번째 컬럼(product)을 묶고 -> 마지막으로 전체 합계까지 올라감 - East-A 100, East-B 150, East 전체 250 / West-A 200, West-B 50, West 전체 250 / 전체 500 - 보면 East / West 로 묶고 -> East 안에서 A/B로 묶고 -> 전체 500 함. - CUBE는 가능한 모든 조합 - CUBE(region, product)이면 - East-A 100, East-B 150, East 전체 250 / West-A 200, West-B 50, West 전체 250 / 제품 A 전체 300 / 제품 B 전체 200 / 전체 500 - 보면 East / West 로 묶고 -> East 안에서 A/B로 묶고 -> A/B로 묶고 -> A안에서 East/West로 묶는건 의미없으니 없고 -> 전체 500 함. 3. UDF & 시퀀스 & 저장 프로시저 & UDT & 트리거 (p.95-101) UDF - SQL 문법만으로는 반복적인 계산이나 특정 규칙 적용이 어려운데 - UDF를 만들어놓으면 데이터베이스 안에 내장된 함수 외에도 필요할 때 불러다 쓸 수 있다. - is_similar 함수 - 두 개의 실수값이 주어진 임계치 이상으로 가까운지를 판별하는함수 - 실질적 활용? - 임베딩 스토어에서 코사인 유사도가 일정 기준 이상인 후보만 필터링하는 기능이니까 - 데이터베이스 안에서 바로 AI 예측 후보 선별에 쓸수있다. 시퀀스 - 자동으로 증가하는 고유 ID를 만들어줌 - 테이블에 데이터를 넣을 때 시퀀스를 만들어 두고 nextval로 꺼내 쓰면 순차적으로 값이 올라가니까 데이터마다 일일이 ID를 붙이지 않아도 된다. - 예시 - CREATE SEQUENCE my_seq START 1; -> 이렇게 만들어 두면 - DEFAULT nextval('my_seq')를 컬럼에 달아주면 - INSERT INTO embedding_store (user_text, embedding) 할 때 자동으로 ID가 올라간다 - 매번 새로운 번호가 붙기 때문에 중복 없는 고유 ID를 쉽게 관리할수있다. - 실질적 활용? - 모델 예측 결과나 벡터 데이터가 쌓일 때 결과를 추적하거나 버전을 구분할때 - 벡터를 하나씩 저장할 때마다 고유 번호를 자동으로 달아주면 나중에 “이 임베딩이 어떤 실험에서 나온 것인지”를 관리하기 쉽다. - 결과 추적? - 어떤 문장을 임베딩해서 384차원짜리 벡터를 만들었고 -> 벡터를 테이블에 저장할건데 -> 임베딩은 숫자 배열이므로 나중에 “이 벡터가 언제, 어떤 실험, 어떤 모델로 만들어진 건지”를 추적하기 어려운데 -> 이때 시퀀스로 생성한 고유 ID를 같이 붙여 주면? - 첫 번째 벡터 저장 → ID = 1000 - 두 번째 벡터 저장 → ID = 1001 - 세 번째 벡터 저장 → ID = 1002 - 이렇게 고유 ID가 붙으면 나중에 분석할 때 “ID=1002인 벡터는 실험 X에서 나온 결과다” 하고 연결하기 쉽다. - 버전 관리? - 같은 문장을 두번 실험에 다르게 임베딩했으면 1차 실험 때는 모델 버전 1로 뽑은 벡터 2차 실험 때는 모델 버전 2로 뽑은 벡터가 있을 수 있고 -> 이럴 때 고유 ID를 붙여 두면 “실험 1번에서 나온 ID 1010 벡터와, 실험 2번에서 나온 ID 2020 벡터를 비교하자” 이렇게 버전 관리 할수있다. 저장 프로시저 - 여러 SQL 문장을 묶어 하나의 절차처럼 실행 - 예시 - 예측 결과 테이블 prediction_results가 있고 실제 라벨(true_label)과 모델이 예측한 라벨(pred_label)이 있다. - AI 모델이 예측한 결과를 5개 저장하려고 한다. - 저장 프로시저가 없으면 개발자가 직접 5번 INSERT 문을 날려야하는데 - 저장 프로시저가 있으면 똑같이 5건을 넣어야 하는 상황에서 CALL 한 줄만 쓰면 된다. - 프로시저 내부에 반복문(FOR i IN 1..p_count)이 있어서 알아서 5번 INSERT를 실행해준다 사용자 정의 데이터 타입(UDT) - 보통 테이블 컬럼은 숫자, 문자열 같은 단순 타입인데 내가 원하는 구조를 만들어서 하나의 타입처럼 쓸 수 있다. - 예측 결과를 저장하려고 할때. - 썼을때와 안썼을때의 차이를 보면? - 구체적으로 어디가 다르냐면 - 데이터 넣기 - 안썼을때: (model_name, label, score) -> label과 score를 각각 컬럼에 직접 넣는다. - udt 썼을때: (model_name, result) -> label과 score를 ROW()로 묶어서 result라는 한 컬럼에 넣는다. - 조회 - 안썼을때: SELECT label, score -> 그냥 컬럼 이름(label, score)으로 바로 꺼낸다. - udt 썼을때: SELECT (result).label, (result).score -> result 안에서 필드를 꺼내는 방식으로 꺼낸다. - 의문점 - 출력 결과가 똑같은데 왜쓰는거지? - 답 - 출력 결과만 비교하면 같지만 확장성에서 차이가있다. - 안 썼을 때는 함수가 여러 개 값을 리턴해야 하면 RETURNS TABLE(label TEXT, score FLOAT) 같은 형태로 정의해야 하는데 썼을 때는 함수가 RETURNS prediction_result_type로 정의되니까 “이 함수는 예측 결과 하나를 리턴한다”라고 직관적으로 쓸 수 있다 즉 데이터 구조를 하나의 타입으로 추상화할 수 있다. - 안 썼을 때는 label, score를 다른 테이블에서도 쓰려면 매번 두 컬럼을 복사해야 하는데 썼을 때는 그냥 result prediction_result_type 하나만 선언하면 되니까 중복 정의를 줄이고 일관성 유지 가능(이건 예시에선 2개여서 메리트 없어보이는데 개수 늘어나면 납득됨) - 복잡한 구조 확장 - 예측 결과가 단순히 label+score로 끝나지 않고 label, score, confidence_interval, metadata 같이 커질 수 있는데 안 썼을 때는 컬럼이 점점 늘어나고 테이블마다 다 복사해야 하지만 썼을 때는 타입만 확장하면 모든 테이블·함수에서 동일하게 활용 가능하다. 트리거 - 데이터가 삽입, 수정, 삭제될 때 자동으로 실행되는 규칙 - 예시 - 새로운 벡터가 들어왔는데 유사도가 0.5보다 낮으면 경고 테이블에 따로 기록하려고 할때? - 궁극적인 차이는 - 메인 테이블 + 경고 테이블: 트리거를 쓰든 안 쓰든 구조는 똑같음 - 데이터 넣을 때 - 트리거 안 쓰면: INSERT (메인 테이블), INSERT (경고 테이블, 조건 만족 시) -> N개의 쿼리를 개발자가 직접 작성 - 트리거 쓰면: INSERT (메인 테이블) -> → 1줄만 작성하면 나머지(조건 체크 + 경고 INSERT)는 DB가 자동 처리. 4. 윈도우 함수 (p.126-129) 집계함수와 윈도우함수 차이 - 비슷하지만 GROUP BY처럼 그룹을 한 줄로 압축하지 않고, 각 행마다 순위, 누적합, 이전 값 같은 걸 계산함 - GROUPBY -> 학생(그룹) 단위로 묶어서 한 줄로 결과를 압축했다. - OVER (PARTITION BY student) -> 학생(그룹)별로 평균을 계산하되 결과는 행마다 달아줬다. 윈도우 함수 - ROW_NUMBER() - 그룹 안에서 순번을 매긴다. - 사용자별로 점수를 내림차순 정렬하고 ROW_NUMBER를 매기면, 그 사용자 안에서 1등, 2등, 3등을 구할 수 있다. - RANK() - 동점이 있을 때 같은 순위를 부여하고 건너뛰기가 발생한다. - 1등이 두 명이면 다음 순위는 3등. - DENSE_RANK() - 같은 순위가 있더라도 건너뛰지 않고 다음을 2등으로 붙인다. - NTILE(n) - 데이터를 n개 구간으로 자른다. - 100명을 NTILE(5)로 나누면 성적을 기준으로 20명씩 다섯 구간으로 나눌 수 있다. - LAG() & LEAD() - 현재 행 기준으로 앞 행이나 뒤 행 값을 참고할 수 있어서 시간 순서대로 점수를 나열해 두면 바로 직전 점수와 비교하거나 다음 점수를 미리 볼 수 있다 - SUM() OVER, AVG() OVER - 누적합이나 누적평균 구한다. AI 연계 - 예측 결과를 저장한 prediction_logs 테이블 - 활용 - 여러 모델 버전이 같은 사용자에 대해 점수를 매겼을 때 그중 가장 높은 점수를 고르기. - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY pred_score DESC) -> rownum = 1인 행만 선택 - 모델 간 성능 비교 - RANK() OVER (PARTITION BY user_id ORDER BY pred_score DESC) - 예측 점수 상위 20% 사용자 그룹을 뽑기 - NTILE(5) OVER (ORDER BY pred_score DESC) -> bucket = 1인 행만 선택 - 이전 점수와 비교해 사용자의 점수가 올랐는지 떨어졌는지 확인 - LAG(pred_score) OVER (PARTITION BY user_id ORDER BY created_at) -> pred_score - prev_score 차이 계산 - 모델 정확도의 누적 변화 확인 - SUM(pred_score) OVER (...), AVG(pred_score) OVER (...) 5. 고급 인덱스 (p.144-148) 고급 인덱스? - 일반적인 데이터베이스 인덱스는 B-Tree 인덱스. - AI에서 다루는 데이터는 단순 숫자 키가 아니라 JSON 문서, 벡터, 시계열 로그처럼 복잡하거나 대용량 특성이 있어서 다른 종류의 인덱스들이 필요하다. GIN 인덱스 - Inverted Index: 거꾸로 색인. - 보통 행 -> 마다 단어가 있는데 - "찾고 싶은 단어 -> 그 단어가 들어 있는 행"으로 인덱스를 만든다. - "category"="esg"인 행을 찾고 싶으면 테이블을 처음부터 끝까지 보지 않고 인덱스를 통해 곧바로 1, 3행을 볼수있다. GiST 인덱스 - AI에서 쓰는 벡터 데이터에서의 인덱싱은 - 사전처럼 정확한 값을 빠르게 찾기보다는 이 벡터와 가장 비슷한 벡터를 찾는, 정확히 같은 값이 아니라 가까운 값을 찾는 경우가 많다. - GiST 인덱스는 “거리 기반” 검색을 빠르게 해 주는 구조여서 가까운 것을 찾는 인덱싱에 적합하다. BRIN 인덱스 - 범위별 최소·최대 값만 기록해 두고, 그 안에 데이터가 있을 거라고 좁혀 가는 방식 - 일기장이 날짜 순으로 - 1월 1일~1월 10일 -> 1권 - 1월 11일~1월 20일 -> 2권 - 1월 21일~1월 31일 -> 3권 - 이렇게 적혀있으면 1월 15일 일기를 찾으려고하면 2권만 열어 보면 된다. - 빠른 이유는 범위만 보고 필요한 블록만 열어보면 되기 때문. - 잘 맞는 경우는 로그, 시계열 데이터 - 잘 안 맞는 경우는 무작위 데이터. 왜냐면 “최소~최대”로 구간을 좁힐 수 없기 때문에 범위가 의미가 없다.
2025-08-27 ⋯ DBMS 및 SQL 활용 #2 트랜젝션 격리수준, pgaudit, AI 시스템 운영
1. 트랜젝션 격리수준 트랜젝션 - 데이터베이스에서 하나의 작업 단위. - 여러 개의 쿼리나 연산이 묶여 하나로 실행되는데 그 결과는 전부 성공하거나 아니면 전부 실패해서 원래 상태로 되돌아가야 한다. - 그렇지 않으면 데이터가 꼬인다. 문제는? - 여러 사람이 동시에 같은 데이터베이스를 건드린다. - 그래서 데이터가 뒤섞이지 않도록 격리 수준이라는 규칙을 둬야한다. 데이터가 뒤섞인다? - 은행 계좌에서 A 트랜잭션이 “잔액 100만 원에서 10만 원 빼기” 작업을 하고 있고 동시에 B 트랜잭션이 “잔액 100만 원에서 20만 원 빼기” 작업을 한다고 하면 - 각각 따로 실행하면 당연히 최종 잔액은 70만 원이 되어야 한다. - 그런데 둘이 겹쳐서 실행되면 이런 일이 생길 수 있다. 1. A가 잔액을 읽음 → 100만 원 2. B도 잔액을 읽음 → 100만 원 3. A는 100만 원에서 10만 원 빼서 90만 원을 저장 4. B는 자기도 100만 원이라고 알고 있으니까 20만 원 빼서 80만 원을 저장 5. 결과적으로 최종 잔액은 80만 원이 됨 근데 사실 두 번 다 반영되려면 70만 원이 되는 게 맞음. - 결론 - 뒤섞인다 = 여러 트랜잭션이 동시에 실행되면서 서로의 중간 작업 결과가 충돌하거나 덮어씌워져서 최종 데이터가 잘못된 상태로 기록된다. 격리 수준(Isolation Levels) - “내 작업이 다른 사람 작업과 얼마나 떨어져 있나”를 정하는 규칙. - 격리 수준이 낮으면 동시에 빨리 처리할 수 있지만 데이터가 꼬일 위험이 크고 격리 수준이 높으면 꼬임은 막을 수 있지만 속도가 느려진다. Read Uncommitted - 다른 사람이 아직 확정하지 않은 값도 읽을 수 있음 - 작업의 거리가 가까워서 발생할수있는 문제: A가 계좌 잔액을 100만 원에서 50만 원으로 바꾸려다가 아직 완료하지 않은 순간에 B가 그 값을 읽어버리면 B는 50만 원이라는 잘못된 값을 보고 계산을 시작할 수 있음(Dirty Read) Read Committed - 확정된 데이터만 읽을 수 있음 - 같은 데이터를 두번 조회했을때 값이 다를 수 있음. A가 잔액을 조회했을 때는 100만 원이었는데 그 사이 B가 그 값을 200만 원으로 바꾸고 확정해버리면 A가 다시 같은 잔액을 조회했을 때 값이 달라져 있다(Non-Repeatable Read) 의문점 - '같은 데이터를 두번 조회했을때 값이 다를 수 있음'이 왜 문제가 되는가? (당연한거 아닌가 변화가 확정된건데) - 트랜잭션이라는 단위가 가져야 하는 “일관성 보장”이 깨짐. - 트랜잭션은 하나의 논리적 작업 단위인데 즉 그 안에서 여러 SQL 문이 실행될 때 그 문들은 같은 시점의 데이터 상태를 공유한다는 가정이 필요하다. - 예를 들어 트랜잭션 T1이 "잔액을 읽어서 100만 원 이상이면 10% 이자를 주는 UPDATE" 작업을 할때 T1이 먼저 SELECT 잔액을 해서 100만 원이라고 확인 -> 그 사이에 트랜잭션 T2가 잔액을 200만 원으로 바꾸고 커밋 -> 이제 T1이 다시 SELECT 잔액을 해서 계산하려 하면 200만 원이 보임 -> 같은 트랜잭션 안에서 읽은 값이 불일치하므로 T1의 로직은 잘못된 가정 위에서 실행될 수 있다. - 이런 Non-Repeatable Read는 격리 수준을 더 올리면 막을 수 있다. Repeatable Read - 같은 데이터를 여러 번 읽어도 값이 변하지 않는다 즉 내가 한 번 확인한 계좌의 값은 트랜잭션이 끝날 때까지 변하지 않는다. - “고객 수가 몇 명인지” 같은 조건을 걸고 데이터를 읽는 트랜젝션을 수행할때 그 사이에 다른 사람이 새로운 고객을 추가할 경우, 나는 같은 조건으로 다시 조회했을 때 처음보다 고객 수가 늘어난 것을 보게 된다 예를 들어 처음엔 고객이 10명이었는데 다시 보니 11명으로 바뀌어 있다. 이미 본 고객들의 정보는 그대로지만, 집합 자체가 달라진다(Phantom Read). 의문점2 - '이미 본 고객들의 정보는 그대로지만, 집합 자체가 달라진다'가 왜 문제가 되는가? (트랜젝션 자체는 잘돌아갔어도 트랜젝션의 근본적인 목적인 '고객 전체 데이터에 대한 결과 내기'가 안돼서 문제인지?) 답2 - 트랜잭션의 목적(예: 고객 전체 데이터를 기준으로 무언가 계산하거나 판단하는 것)이 제대로 달성되지 못한게 문제다. - 트랜잭션의 목적 - 단순히 SQL을 순서대로 실행하는 것이 아니라 “논리적으로 일관된 하나의 시점(state)을 기준으로 작업을 수행한다”는 걸 보장해서 전체 집합에 대한 일관된 결과를 내는 것이 목적. - 예를 들어 트랜잭션의 목적이 “현재 전체 고객 수를 기준으로 통계를 계산하는 것”일때 - 내트랜잭션을 시작해서 SELECT * FROM customers WHERE condition... 으로 전체 집합을 조회했을 때는 10명이었고 -> 같은 트랜잭션 안에서 이 10명에 대해 뭔가 합계·평균·비율 등을 계산하는데 -> 그 사이에 다른 트랜잭션이 조건에 해당하는 새로운 고객을 INSERT하고 COMMIT해버리면 -> 내가 같은 조건으로 다시 SELECT 하면 이제는 11명이 나와서 -> 내 트랜잭션 안의 앞부분과 뒷부분이 “서로 다른 현실”을 보게 됨 - “고객 전체를 대상으로 한 통계”라는 내 작업의 논리적 일관성을 깨뜨린다. - 요약 - 트랜잭션의 목적이 단순히 한 행을 읽거나 수정하는 게 아니라, “조건에 맞는 전체 집합을 기준으로 어떤 결과를 계산하거나 보장하는 것”이라면 - 격리 수준이 낮으면 트랜잭션 안에서 집합 자체가 변해서 논리적으로 앞뒤가 안 맞는 결과를 낼 수 있고, - 그렇기 때문에 SQL 표준은 이런 현상을 “문제”라고 규정하고, 격리 수준을 통해 제어할 수 있도록 만든 것입니다. 의문점3 - Repeatable Read랑 Unrepeatable Read 차이? 답3 - Non-Repeatable Read (문제 현상) - 트랜잭션 안에서 동일한 조건으로 같은 “특정 행”을 두 번 읽었는데 값이 달라진 경우 - 고객 ID=5번을 첫 번째 조회에서는 나이=30살로 읽었는데 다른 트랜잭션이 그 고객의 나이를 40살로 바꾸면 내가 다시 ID=5번을 읽으면 40살로 보인다. - 같은 행의 값이 바뀌어 반복 불가능한 읽기가 되었다. - Repeatable Read (격리 수준) - Non-Repeatable Read라는 현상을 막기위한 '이미 읽은 행의 값은 트랜잭션 종료까지 고정'이라는 방식. - 고객 ID=5번을 첫 번째 조회에서는 나이=30살로 읽었는데 다른 트랜잭션이 그 고객의 나이를 40살로 바꾸고 커밋하더라도 내가 같은 트랜잭션 안에서 다시 ID=5번을 조회했을 때 여전히 30살로 보인다. - 이미 읽은 행의 값은 트랜잭션이 끝날 때까지 변하지 않는다. - Phantom Read (문제 현상) - 트랜잭션 안에서 동일한 조건으로 "같은 집합"을 두 번 읽었을 때 새로운 행이 끼어들어 결과 집합이 달라지는 경우(기존 행의 값은 변하지 않음) - 나이 ≥ 30살 조건으로 고객 집합을 조회했을 때 10명이었다. 다른 트랜잭션이 나이=35살인 고객(ID=11번)을 새로 INSERT하고 커밋하면 내가 같은 조건으로 다시 조회했을 때 11명으로 보인다. - 기존에 읽은 행들의 값은 그대로지만 집합에 새로운 행이 끼어들어 결과 건수가 달라졌다. 의문점4 - '집합에 새로운 행이 끼어들어 결과 건수가 달라짐'이 왜 문제가 되는가? (고객이 추가된건데 당연한 결과 아닌가? 트랜젝션도 문제없는데) 답4 - 트랜잭션이 한 덩어리의 논리적 작업으로서 동일한 기준(같은 시점·같은 집합) 위에서 결론을 내야 하는 경우는 집합 일관성이 요구되는데 그게 깨져서. - 집합 일관성이 요구되는 경우? - case1: “나이 ≥ 30 고객이 10명 이상이면 VIP 프로모션 집행”이라는 로직에서 1. T1이 처음 조회해 10명을 확인해 프로모션을 집행하기로 결정 2. 그 사이 T2가 1명 INSERT 3. T1이 다시 확인하니 11명 4. 정책 근거의 일관성이 깨짐. 로그엔 “10명이라 집행”이라 찍혔는데, 검증 단계에선 “11명 기준으로 집행됐어야 한다”가 되어 회계/감사·추적 시 앞뒤가 맞지 않게 된다프로모션 집행한다고했는데 예산/재고 산정이 “10명분”으로 계산된 뒤 “11명”으로 검증되면 과소/과다 집행 이슈 발생. - case2: “10명 이하일 때만 집행” 로직에서 1. 첫 조회 10명 -> 집행(YES) 2. 그 사이 1명 INSERT로 11명 -> 동일 트랜잭션에서 재조회 시 미집행 -> '집행여부' 결론 뒤집힘 - case3: “10명 이하일 때만 집행” 로직에서 1. 첫 조회 10명 기준으로 10장 발급 2. 재조회 11명 -> 미발급 1명 발생해서 무결성/공정성 깨짐 - 결론 - 집행 여부가 같아도 근거가 변해 논리적 일관성·정합성이 깨지거나, 결론 자체가 뒤집힘 또는 현시점에 적절하지않은 결론이 도출되어서 트랜젝션 성공 여부와 관련없이 트랜젝션 수행 목적이 제대로 이행되지않는게 문제다. ~*의문점4는 다시보니 의문점2랑 똑같은 질문...*~ Seriesable - 모든 트랜잭션이 순차적으로 실행된 것과 같은 결과를 보장 - 동일한 시점의 데이터를 기준으로 처리하므로 Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생하지 않는다 예를 들어 “나이 ≥ 30 고객이 몇 명인지”를 조회했을 때 처음 10명이었다면, 트랜잭션이 끝날 때까지는 다른 트랜잭션이 고객을 추가하더라도 여전히 10명으로 보이며, 새로운 행이 끼어드는 일이 없다. 의문점5 - Repeatable Read도 트랜잭션이 끝날 때까지 동일한 값이 보장된다고 했는데 Serializable이랑 다른점? 답5 - Repeatable Read - 보장하는 것: 이미 읽은 행(row)의 값은 트랜잭션 종료까지 변하지 않는다. - 보장하지 않는 것: 아직 읽지 않은 “범위(gap)”에 새로운 행이 삽입되는 것은 막지 않는다. - WHERE age >= 30 같은 조건 조회 시, 이미 읽은 고객들의 나이는 그대로지만, 그 조건에 맞는 새로운 고객이 추가되어 “집합”이 달라질 수 있다(Phantom Read) - Serializable - 보장하는 것: 트랜잭션 전체가 직렬(순차) 실행된 것과 동일한 결과 즉 단순히 이미 읽은 행만 고정하는 게 아니라, 조건/범위 전체를 잠가서 새로운 행이 끼어드는 것까지 차단함. - WHERE age >= 30 조건으로 처음 10명이었다면, 내 트랜잭션이 끝날 때까지는 집합이 변하지 않는다. 다른 트랜잭션이 INSERT를 시도하면 내 트랜잭션이 끝날 때까지 대기하거나 충돌로 막힌다. 의문점5 결론 - 집합이 바뀌는건 트랜젝션 수행에 영향을 안준다 << 가 전제되는듯. - 트랜젝션 수행에는 영향이 없고 트랜잭션의 논리적 목표(집합 단위의 일관된 판단/계산)에 문제가 생긴다. - Serializable은 그것마저 차단한다. 격리수준-비유없는 정의 - 동시에 실행되는 여러 트랜잭션 간의 상호작용을 얼마나 차단할지를 정의하는 규칙. - 격리 수준이 낮으면 동시성은 높지만 데이터 일관성이 약해지고 격리 수준이 높으면 데이터 일관성은 강해지지만 동시성이 떨어진다. 2. pgaudit 필요성 - 데이터베이스를 운영할 때 단순히 쿼리가 잘 돌아가는지만 보는 게 아니라, 누가 언제 어떤 SQL을 실행했는지 기록으로 남겨야 함. - 보안 규정이나 법적 규제에서는 “권한 변경이 있었는가, 데이터가 언제 어떻게 수정되었는가, 누가 조회했는가” 같은 사항을 추적할 수 있어야 하고 내부 직원이 부적절하게 데이터를 열람하거나 외부 공격자가 침입했을 때를 대비해 이러한 흔적을 감시할 수 있는 장치가 필요하다 설치 Homebrew PostgreSQL 17 PATH 추가 슈퍼유저 postgres role 생성 후 postgres로 접속 bash psql bash pgaudit 라이브러리 로드 설정 bash sql 주요 설정값 세팅 bash로 하기 sql로 하기 테스트1 테스트2 - DDL/DML 실행후 로그 확인 - CREATE TABLE temp_test(id INT); - 이미 같은 이름의 테이블이 있어서 relation "temp_test" already exists 에러 발생 (정상 동작) - INSERT INTO temp_test VALUES (1); - 두 번 실행됨 - 그래서 id 값이 1인 레코드가 두 개 들어감 - GRANT SELECT ON temp_test TO postgres; - 권한 부여 정상 완료 다시 쿼리 생성해야된대서 다시하기 - FATAL: terminating connection due to unexpected postmaster exit - PostgreSQL 서버가 잠깐 죽었다가(FATAL) 자동으로 재기동 - INSERT INTO temp_test VALUES (1); - 세 번 실행됨 - 그래서 id 값이 1인 레코드가 3개 들어감 - GRANT SELECT ON temp_test TO postgres; - 권한 부여 정상 완료 자꾸 pgAdmin 자체가 실행한 모니터링 쿼리만 뜨는데 ... 머지 ㅠㅠ 3. AI 시스템 운영 AI 파이프라인 - 데이터를 수집하고 정제 -> 벡터화·임베딩을 거쳐 데이터베이스에 저장 -> 그 후 학습과 추론 과정을 통해 모델을 활용 -> 서비스나 API로 결과를 노출 특성? - 각 단계는 담당자와 보안 위험이 다르다. - 수집 단계에서는 민감한 원본 데이터가 노출될 수 있고, 정제 단계에서는 변조가 일어날 수 있다. 임베딩 단계에서는 모델 노출이 위험 요소가 되고, DB 저장은 권한 누수가 문제가 된다. 학습·추론 단계는 반복 호출과 탈취가 이슈이고, 서비스/API 단계에서는 불필요한 노출을 막아야 한다. - 이에따라 ETL 담당자, 데이터 엔지니어, ML 엔지니어, DBA, 서비스 관리자, API 사용자처럼 책임 담당자가 나뉜다. 권한 분리 - 분리 방식? - 수집을 맡은 data_ingestor는 INSERT나 TRUNCATE 권한만, 정제를 맡은 data_cleaner는 SELECT와 UPDATE 권한만, 모델을 다루는 ml_engineer는 SELECT와 실행 권한만 가진다. API 사용자(api_user)는 결과 조회만 허용되고, 최종적으로 admin만 모든 권한과 보안 정책 관리 권한을 갖는다. - PostgreSQL에서 구현 - 벡터 저장 테이블을 만들고 각 역할에 필요한 권한만 부여. - data_ingestor는 INSERT, SELECT, ml_engineer는 SELECT, UPDATE, api_user는 SELECT만 허용하는 식. 데이터 보호 전략 - 민감한 필드는 뷰(View)로 가공해 노출을 제한 - 행 단위 보안(Row-Level Security)을 적용해 “자신이 생성한 데이터만 볼 수 있다” 같은 조건 생성 - 접근 기록은 pgaudit 같은 로깅 확장이나 API Gateway 로그를 통해 남기고 API 키 인증을 통해 모델 접근 제한 API 접근 통제 - FastAPI나 Flask에서 사용자 인증 토큰(OAuth, JWT)을 활용해 접근을 검증 - 추론 요청 시에는 사용자 IP와 쿼리 내용을 저장해 추적 가능성을 확보 - OpenAI나 BERT 같은 대형 모델을 활용할 경우 응답 길이 제한, 시간 제한, 비속어 필터링 - 벡터 검색 결과는 SCORE 기준으로 중요도 있는 일부만 노출되도록 제어해 불필요한 데이터 유출 통제 - GraphRAG 같은 방식은 노드·엣지 단위로 권한을 세분화해 특정 사용자에게 필요한 정보만 노출
2025-08-27 ⋯ DBMS 및 SQL 활용 #1 설계안 데이터 적재 (postgresql, pgvector)
1. 실습1 실습 시나리오 - 사용자가 설계안 텍스트(예: description)를 입력 - 해당 텍스트에 대해 Python에서 AI 임베딩을 수행 - 임베딩 결과가 유효할 경우 design 테이블에 등록 (COMMIT) - 실패하면 아무 데이터도 등록하지 않음 (ROLLBACK) - PostgreSQL + pgvector 확장 사용 - Python에서 psycopg2 + 임베딩 처리 코드 SQL python 시나리오 구현 - 사용자가 설계안 텍스트(예: description)를 입력 - insert_design(desc) -> description(desc): str - 해당 텍스트에 대해 Python에서 AI 임베딩을 수행 - get_embedding(description) -> client.embeddings.create - 임베딩 결과가 유효할 경우 design 테이블에 등록 (COMMIT) - insert_design -> conn.commit() - 실패하면 아무 데이터도 등록하지 않음 (ROLLBACK) - insert_design -> except Exception as e -> conn.rollback() 개념 트랜젝션? - DB에서 여러 SQL 실행을 하나의 작업 단위로 묶는것 - 여러 SQL 실행? - BEGIN; (시작) / INSERT ... (데이터 넣기) / UPDATE ... (데이터 수정하기) / COMMIT; (끝내기 → 확정 반영) 등 commit? - commit 전에는 cursor.execute를 실행해도 DB 내부 버퍼/임시 상태에만 반영됨. - commit을 하면 변경사항을 실제 DB 파일(디스크)에 확정 저장되고 다른 클라이언트(psql, pgAdmin 등)에서도 데이터를 조회 가능. 2. 실습2 실습 시나리오 - FastAPI 기반 /register_design API를 구현해보세요(Python) - Streamlit 를 통해 입력 UI를 만들고 위에 만든 FastAPI를 호출하는 방식으로 해보세요. 아래의 순서대로 진행해보세요. 1. PostgreSQL의 `design` 테이블 (생성됨) 2. FastAPI 서버 실행: `uvicorn app:app --reload` 3. Streamlit 클라이언트 실행: `streamlit run streamlit_client.py` 4. 입력 → POST → 등록 확인 코드 FastAPI 서버 Streamlit 클라이언트 시나리오 구현 - FastAPI 서버- class DesignRequest - Input: 사용자가 Streamlit 화면에서 입력한 설계안 텍스트를 json {"description": "텍스트"} 로 변환 - Pydantic이 json을 검증후 python 객체(req.description)로 변환 - Output: req.description (문자열) - FastAPI 서버- register_design() - Input: req.description (문자열) - OpenAI API 호출해서 임베딩 벡터 생성 → PostgreSQL design 테이블에 (description, embedding) 저장 - Output: 성공/실패 메시지 JSON 응답 ({"status": "success", "message": "등록 성공"}) - Streamlit - Input: 사용자가 입력 설계안 description 텍스트 - FastAPI에 전송하면 json {"description": "텍스트"} 로 감싸서 fastapi에 POST 요청 → description 데이터 등록 - Output: 성공 실패 메시지 표시 개념 class DesignRequest와 register_design()와의 호환? - JSON을 파싱해서 Python 객체로 바꾸고 description이 문자열인지 검증한 뒤 통과하면 register_design()에서 DesignRequest 객체를 만들어 req에 넣는다. 롤백? - rollback을 안 하면 “INSERT는 됐는데 commit 전에 에러 발생” 같은 상태가 DB에 남을 수 있음. 3. 실습2 - 레퍼런스 코드
2025-08-20 ⋯ python #3 pgvector 유사 리뷰 검색
1. 목적 고객 리뷰 문장을 벡터로 임베딩하고 PostgreSQL의 pgvector 기능을 활용하여 비슷한 리뷰를 검색하는 기능을 구현 2. 코드 skala conda 환경을 만들었었는데 pgvector 돌리기용으로 지피티가 추천해준 패키지 조합이 있어서 그냥 force로 저렇게 깔아줬다. 3. 생각 PostgreSQL 테이블 생성 단계에서 나는 python으로 그냥 쏴줬는데 pgadmin 왔다갔다하면서 연동 느낌을 주는게 목적인가? 싶어서 남들 코드로 확인만 해보기. 1. pgadmin을 들어가서 postgresql에 테이블 생성 요게 정석인듯. python으로 review를 embedding이라는 벡터로 만들고 -> SQL 쿼리문 작성하고 -> python으로 연결해서 python으로 리뷰 임베딩을 작성하고 -> reviews, embeddings를 db에 저장. 내코드는? DB연결을 먼저하고 테이블 생성을 해줌. 여기는 똑같다.
2025-08-19 ⋯ LLM #2 LLM과 AI 기술요소를 활용하여 비즈니스 서비스 기획안 작성
1. 목적 - 등기부등본/건축물대장 업로드 시 AI가 자동으로 문서를 분석하여 전세사기 위험 요소를 탐지하고 수치화한다. 2. 모델 구성도 데이터 수집및 정규화 - 기술요소: PaddleOCR - 선택 이유: 한국어 인식 정확도와 속도가 좋고, 오픈소스+온프레미스 운영 가능(비용·보안 유리), 표 레이아웃/좌표 추출 지원. - 입력 - 파일: PDF/스캔 이미지(JPG/PNG) - 매개변수: lang="korean", det+rec 사용, dpi(≥300) - 출력 - 텍스트 블록: [{page, bbox, text}] - 정규화 결과: 주소/금액/날짜/권리유형 표준화(JSON) 위험 특약/권리 분석 - 기술요소: RAG - 선택 이유 - 사실 기반 답변: 등기부등본, 계약서, 법률 조항 등 최신 외부 데이터를 활용하여 허위 정보 생성을 방지하고 사실에 기반한 분석 결과를 제공 - 유연성 및 확장성: 새로운 법률 개정, 최신 판례, 특약 유형 변화 등에 맞춰 데이터베이스를 쉽게 업데이트할 수 있어 최신 정보를 반영한 분석이 가능 - 근거 제시: 원본 문서 기반 신뢰할 수 있는 분석 결과 - 입력 - 문서 데이터: PDF/스캔 이미지(JPG/PNG) 형태의 등기부등본, 계약서 사본 - 질의(Query) 벡터: OCR로 추출된 텍스트 블록 중 특약 및 권리 관련 문장 - Vector DB: 특약, 등기부등본 상 권리, 법률 조항, 과거 피해 사례 등 텍스트 데이터를 벡터화하여 저장 - 출력 - 위험 라벨: '선순위 임차인 존재', '가압류', '근저당권 과다' 등 - 근거 스팬: 원본 문서 내 위험 라벨의 근거가 되는 문장 및 위치 - 위험 지수: 특약 및 권리 유형의 위험성을 정량화한 점수 ML 위험 예측 - 기술요소: LightGBM - 선택 이유 - 수치·범주 혼합 데이터에서 빠르고 강력하며 해석·튜닝이 쉽고, 소규모부터 대규모까지 안정적. - 입력 - 재정 지표: 전세가율, 채권최고액/보증금 비율 - 권리 정보: 근저당권 수, 소유권 변경 횟수/최근성 - RAG 결과: 위험 지수 - 출력 - 위험 점수: 0~100점 - 위험 등급: 5단계 LLM 리포트 생성 - 기술요소: GPT-4o - 선택 이유 - 한국어 설명 품질·사실성·형식 제어가 우수, 근거 텍스트/수치 결합 요약에 강함. - 입력(프롬프트 구성) - 메타데이터: 주소, 면적, 보증금, 계약일 - ML 결과: 위험 점수, 위험 등급 - RAG 결과: 위험 라벨 상위 n개와 근거 문장 - 출력 - 자연어 리포트: 근거 하이라이트 포함 - 맞춤형 권고사항: 위험도별 액션 가이드 비고 - 교육과정에서 사용한기술요소로 구성한게 좋다고 하셧는데 RAG로 위험라벨뽑는게 core 로직인데 그걸 구현하는게 매우어려울것같다고하셧다 - RAG 출력인 위험지수를 LightGBM 인풋으로 넣는게 좋다고 해주셧는데 비정형데이터로부터 숫자 정보를 앞에서 뽑아놓은걸 뒤에서 안쓰는게 아까우니까 넣는게낫겟지? 라고 막연하게생각했는데 좋다고 피드백와서조앗다 3. RAG 이해하기 Input - 사용자가 업로드한 계약서 텍스트 - OCR 모듈이 PDF/이미지에서 추출해 JSON 또는 텍스트 형태로 전달한다. - 검색 쿼리 - 계약서 위험 분석을 위해 골라놓은 쿼리 (특약 조항이나 권리 의무 조항. 예를들어 “임대인의 권리 제한 조건은 무엇인가?”, “근저당권 관련 조항은 포함되어 있는가?”) Process (하는일) - 텍스트 벡터화 - 계약서 조항/문장들을 임베딩 모델(e.g., Sentence-BERT, OpenAI Embedding API)로 벡터로 변환. - Vector DB 저장/검색 - 모든 조항을 벡터 DB(예: Pinecone, Weaviate, Milvus, FAISS)에 저장한 뒤, 쿼리 벡터와 유사도 검색을 수행 - 조항 필터링/정규화 - 검색된 조항 중에서 위험 분석에 필요한 "특약/권리" 관련 조항만 필터링 - 리스크 라벨링 - 미리 학습된 ML 모델(또는 룰셋)을 이용해 해당 조항이 위험(High Risk), 주의(Warning), 안전(Safe) 등으로 분류 *미리 학습된 ML 모델? - Raw ML 모델 + “위험 조항 vs 일반 조항” 라벨링 되어있는 계약서 데이터셋 = 사전 학습된 ML 모델. Output - 위험 라벨: ex. High Risk, Moderate Risk, Safe - 근거 문장(조항 원문): 검색된 계약서의 특정 문장/조항 - 메타데이터: 조항 위치, 페이지, 좌표 등 OCR에서 받은 정보 - 예시 출력 4. 더 구체화된 모델 구성도 사실 gpt로부터 얻어낸 초기 모델구성도는 더더 디테일하고 장황했는데 풀어보자면 다음과같았다. 데이터 수집및 정규화 - 기술요소 - OCR: PaddleOCR(korean, layout) 또는 Tesseract(kor+osd) + 문서구역 감지(layout-parser) - 표/구역 파서: pdfplumber, camelot, heuristic 규칙 - Input - 파일: 스캔 이미지(PNG/JPG) 또는 PDF - 메타: dpi, page_range, 언어=ko - Output - 텍스트 블록 목록 + 좌표(bbox), 페이지 인덱스 - 섹션 태깅: 표제부/갑구/을구, 계약서 제목/항/특약 위험 특약/권리 분석 - 구성: 문장/조항 분할 → NER → 관계추출(RE) → 위험 조항 분류 → 규칙 후처리 1. 문장/조항 분할 - 기술요소: KoELECTRA-small(문장경계) 또는 쉬운 대안: pysbd-ko + 규칙 - Input: OCR 정제 텍스트(최대 수천 자) - Output: 문장/조항 토큰열(512 토큰 겹침 윈도우 포함) 2. 개체 인식(NER) - 기술요소: KorFinBERT/KoBERT/Legal-BERT 파인튜닝(토크나 분류) - 라벨: PERSON(임대인/임차인/소유자), ADDR, MONEY(deposit, max_claim), DATE, RIGHT_TYPE(근저당/가압류/가처분…), PRIORITY, ORG/BANK, CONTACT - Input: 조항 단위 토큰열 - Output: 개체 span + 라벨 + 점수 3. 관계 추출(RE) - 사용 모델: Legal-BERT 문장/문맥 쌍 분류(개체쌍→관계), 또는 biaffine 관계추출기 - 스키마: (RIGHT_TYPE–MONEY(max_claim)–DATE(setup)–PRIORITY), (LESSOR↔OWNER match_flag), (CLAUSE↔RISK_KEYWORD) - Input: 개체 주석된 문장 + 후보 개체쌍 - Output: 관계 라벨/점수 4. 위험 조항 분류(다중라벨) - 사용 모델: KorFinBERT/Legal-BERT(Sequence multi-label) + focal loss - 클래스: double_contract, deposit_return_risk, multi_mortgage, frequent_ownership_change, block_move_in, unfair_special_terms 등 - Input: 조항 텍스트(최대 512 토큰) - Output: 라벨별 확률, 최상위 라벨, 근거 토큰 5. 규칙 후처리(하이브리드) - 사용 엔진: 룰 엔진(jsonlogic/自製) - Input: NER/RE/분류 결과, 외부 수치(채권최고액/보증금 비율 등) - Output: 보정된 위험 신호(플래그 및 가중치) 6. NLP 모듈 최종 Output 묶음 2.5 Feature Engineering - Input - 외부 수치: 전세가율, 지역 중앙값 대비 편차, 거래 변동성 등 - 등기부: 근저당 건수, max_claim/보증금, 권리 중첩기간, 소유권 변경횟수·최근성 - NLP: 위험라벨 개수/비율/최대확률, 특약위험지수, 증거문장 수 - Output - 정규화/인코딩된 피처 벡터(X), 타깃(y: 사기/피해사례 라벨 or 위험레벨 라벨) ML 위험 예측 - 사용 모델 - 탐색: AutoGluon/H2O.ai/PyCaret - 본선: LightGBM/XGBoost/RandomForest(+ LogisticRegression baseline) - 확률 보정: Isotonic/Platt - Input - 피처 벡터(X), 학습 시 타깃(y) - 추론 시: 단건/배치 X - Output - 위험 확률(0~1), 등급(저/중/고), SHAP(전역/개별) - 검증/운영 지표 - ROC-AUC, PR-AUC, recall@HIGH, Brier score(캘리브레이션), 시계열 블록 CV LLM 리포트 생성 - 사용 모델 - GPT-4o / Claude 3 Sonnet / LLaMA-3(온프레미스) - Input(프롬프트 구성) - 요약 목표: “전세사기 위험 리포트 생성” - ML: risk_prob, risk_grade, 상위 SHAP 근거(수치) - NLP: 위험 라벨 상위 n개 + 근거 문장 span/원문 - 메타: 주소, 면적, 계약일, 보증금 등 - Output - 자연어 리포트(근거 인용), 권고사항, 하이라이트 포인터 다음 단계들 품질·모니터링 - Input - 추론 로그(입력 해시, 모형버전, risk_prob, 라벨, SHAP), 분포 통계 - Output - 드리프트 경보, 재학습 트리거 이벤트 보안·거버넌스 - Input/Output - PII 토큰화/해시, 암호화 저장, 접근 로그 - 추적성: 모델·피처 버전, 프롬프트·리포트 해시 배포·운영(Ops) - Input - 동기 API(단건) / 비동기 배치(폴더/버킷) - Output - 처리 상태, 리포트 ID, 지연·오류 메트릭 End to End로 입력->출력 예시 생각 1. 먼가어려웠는데 전체흐름을 이해하는게필요할거같아서 1회독을 해보앗다 2. 첨엔 실습 설명 들으면서 먼말인지 1도안와닿았는데 얘기하면서하다보니깐또 하게댓다. 3. 조모임은 부족한내가 나혼자부족하면갠찮은데 외부에 노출대서 영향을줄수도잇다는생각이들어서? 더 부담대고 도망가고싶은거같은데 그럼에도불구하고 multi head의 힘은 확실히있구나라고생각들어서 살면서 조모임력은 필요하다는것을 인정하게되엇다 4. 교수님이 PaddleOCR 언급을 되게오래하시면서 써본사람이잇는건지 어쩌고 하셧는데 그냥 지피티 돌려서 나온건데 생각햇다 .. (지금도 뭔지모름)