Graph DB와 Embedding으로 구현하는 자동 코딩 피드백 루프
복잡한 PL/SQL 코드를 Java로 마이그레이션하거나, 대규모 레거시 코드를 리팩토링할 때 우리는 종종 예상치 못한 버그와 마주하게 됩니다.
모든 문제를 사람의 힘으로 찾고 고치는 일은 매우 번거롭죠. 그런데 만약 코드가 스스로 로그를 분석해 원인을 찾아 고친다면 어떨까요?
이번 글에서는 그래프 DB와 임베딩 벡터(Embedding)를 활용하여, 코드가 에러를 발견하고 수정까지 반복하는 자동화 피드백 루프 에이전트를 구현하는 방법을 소개합니다. 개발자에게 친숙한 방식으로 풀어보겠습니다.

코드 구조를 그래프로 표현하기
먼저 PL/SQL 코드를 개별 구문 단위로 쪼개어 그래프 노드로 표현합니다. 예를 들어 다음과 같은 PL/SQL 코드 조각이 있다고 해봅시다.
BEGIN
SELECT salary INTO v_salary FROM employees WHERE id = v_id;
IF v_salary < 0 THEN
RAISE invalid_salary;
END IF;
END;
이 코드에서는 SELECT
구문, IF
조건, 그리고 RAISE
예외 처리 등이 각각 하나의 노드(Node)가 됩니다. 그래프 DB를 사용하면 이러한 코드 조각들 사이의 관계를 명시적으로 저장할 수 있습니다. 우리는 두 종류의 관계를 정의했습니다.
- PARENT_OF: 포함 관계를 나타냅니다. (예:
BEGIN
블록 노드 PARENT_OF 그 안의SELECT
/IF
구문 노드들) - NEXT: 실행 순서 관계입니다. (예:
SELECT
노드 NEXTIF
노드,IF
노드 NEXTEND IF
등)
각 노드에는 해당 구문이 어떤 일을 하는지 한 줄로 요약한 설명(summary) 정보를 담았습니다. 예를 들어 SELECT ...
구문 노드의 summary는 "직원 테이블에서 급여를 조회하여 v_salary 변수에 담는다" 가 될 수 있습니다. 이러한 그래프 구조 덕분에 코드의 흐름과 계층 관계를 한눈에 파악할 수 있고, 노드 간 복잡한 관계도 표현 가능합니다
그래프 DB를 통해 복잡한 정보 간의 관계를 풍부하게 저장하고, 이를 기반으로 더 맥락에 맞는 검색과 분석을 할 수 있게 되는 것이죠.
노드 요약을 임베딩 벡터로 변환하기
그래프에 노드를 만들었다면, 이제 각 노드의 summary(요약문)를 벡터 임베딩으로 변환합니다. 임베딩(Embedding)이란 텍스트의 의미를 수치화한 고차원 벡터 표현입니다. 간단히 말해, 임베딩 벡터가 비슷하면 두 문장의 의미도 비슷하다고 볼 수 있습니다. OpenAI의 Embedding API 등을 사용하면 이러한 임베딩 벡터를 쉽게 얻을 수 있습니다. 예를 들어 summary "직원 테이블에서 급여를 조회하여 변수에 담는다" 를 임베딩하면, 모델은 이 문장의 의미를 수백 차원의 벡터로 반환해 줍니다.
우리는 각 노드 객체에 node.embedding
필드를 추가하여 이 벡터를 저장했습니다. 이렇게 하면 의미 기반 검색이 가능해집니다. 즉, SQL 구문 자체가 아니라 그 의미로 노드를 찾을 수 있게 되는 것이죠. (예를 들어 "급여를 계산"이라는 의미로 검색하면, 실제 SQL 키워드가 달라도 급여 계산 관련 노드들이 검색될 것입니다.) 실제로 벡터 기반 유사도 검색을 그래프와 결합하면, 보다 정확하고 맥락에 맞는 정보 검색이 가능함이 보고되고 있습니다.
왜 임베딩 벡터일까요? 전통적인 키워드 검색은 코드에 "salary"
라는 단어가 포함되어야만 찾을 수 있지만, 임베딩 검색을 쓰면 "임금", "급여" 처럼 단어가 다르더라도 의미가 비슷하면 잡아낼 수 있습니다. 또한 복잡한 문장도 의미 공간에서 유사도를 비교하니 코드 조각 간 의미 유사성을 비교하는 데 적합합니다.
PL/SQL에서 Java(Spring) 코드로 전환
이제 PL/SQL 코드 자체를 Java(Spring Boot) 코드로 변환합니다. PL/SQL은 데이터베이스 내부에서 동작하는 절차형 언어이고, 이를 현대적인 애플리케이션 아키텍처로 옮기기 위해 Java로 재구현하는 시나리오를 떠올리시면 됩니다. 예를 들면, 앞서의 PL/SQL BEGIN ... END;
블록은 Java의 메소드로, SELECT
구문은 JPA나 JDBC 호출로, IF
와 RAISE
는 Java의 조건문과 예외 처리로 각각 전환할 수 있겠지요.
이 단계에서는 1:1 코드 변환보다도, 기존 로직을 최대한 보존하여 Java로 옮기는 것이 핵심입니다. 전환된 Java 코드는 Spring Boot 환경에서 동작하며, 원래 PL/SQL과 동일한 기능을 수행해야 합니다. (예: 급여를 조회하고 검증하는 로직은 같아야 함) 하지만 사람이 수동으로 코드를 옮기거나, 자동 변환 도구를 써도 사소한 차이로 인해 버그가 생길 수 있습니다. 그래서 다음 단계에서 이 전환된 코드의 테스트와 검증을 진행하게 됩니다.
테스트 실행과 로그 수집
전환된 Java 코드를 충분히 작성했다면, 준비된 테스트 시나리오를 실행하여 제대로 동작하는지 검증합니다. 예를 들어 급여를 계산하는 코드라면 여러 직원 데이터에 대해 결과를 확인해보는 테스트를 할 것입니다. 이 과정에서 만약 에러가 발생하거나 결과 값이 예상과 다르면, 로그(log)에 그 증상이 남게 됩니다. 예를 들어 "Employee ID 123: 급여 불일치 오류 - 예상 값 5000, 계산된 값 0" 같은 로그가 출력될 수 있죠.
우리는 이처럼 실패한 테스트 케이스의 로그 메세지들을 수집했습니다. 중요한 것은 이 로그가 무엇을 의미하는지 이해하는 것입니다. 단순히 "NullPointerException at line 42"처럼 기술적 스택트레이스만 있는 로그보다는, 도메인 친화적인 오류 메세지 ("급여 불일치" 등)가 원인 분석에 유용합니다. 그래서 가능하면 애플리케이션에서 의미 있는 오류 메세지를 남기도록 구현해두는 것이 좋습니다.
이 단계까지 요약하면, Java로 옮겨진 코드에 대해 테스트를 돌리고, 문제가 드러나는 로그를 얻었다는 것입니다. 이제 이 로그를 단서로 어떤 코드가 문제인지 찾아내야 합니다.
로그도 임베딩으로 의미 파악하기
앞서 코드 노드들의 summary를 임베딩해둔 것처럼, 이번에는 로그 메세지를 같은 방식으로 임베딩합니다. 로그 텍스트(예: "급여 불일치 오류 - 예상 값 5000, 계산된 값 0")를 Embedding API로 보내면 이 역시 하나의 벡터 log.embedding
이 나옵니다. 이 벡터는 해당 오류 로그의 의미를 나타내는 좌표입니다.
이제 재미있는 작업을 할 수 있습니다. 바로 로그 임베딩과 코드 노드 임베딩들 사이의 유사도를 비교하는 것이죠. 일반적으로 벡터 간 유사도는 코사인 유사도(cosine similarity)를 많이 사용합니다. 코사인 유사도 값은 -1에서 1 사이인데, 1에 가까울수록 두 벡터가 가리키는 방향(즉 의미)이 매우 비슷함을 뜻합니다.
우리의 시스템은 수집된 log.embedding
과 그래프에 있는 모든 node.embedding
을 차례로 비교하여, 유사도가 높은 순으로 상위 몇 개의 코드를 뽑아냅니다. 예를 들어 "급여 불일치" 오류 로그의 임베딩은 아마도 급여 계산 혹은 처리와 관련된 코드 노드들의 임베딩과 높은 유사도를 보일 것입니다. 실제로 Neo4j 기반 에이전트 메모리 예시에서도 사용자 질문 임베딩을 그래프 DB에 저장해두고 유사한 질문-쿼리 쌍을 찾아오는 방식이 활용됩니다. 우리도 비슷하게 로그 임베딩으로 관련 코드를 찾아내는 거죠.
이렇게 하면 수많은 코드 중 어디를 살펴봐야 할지 자동으로 후보를 좁혀줍니다. 검색 엔진이 키워드로 문서를 찾듯이, 임베딩 검색은 의미로 코드 조각을 찾아주는 셈입니다.
유사도가 높은 코드 위치 찾기
유사도 계산 결과, 특정 PL/SQL 구문 노드 (혹은 그에 대응되는 Java 코드 부분)이 오류 로그와 가장 관련이 높다고 나왔다면, 우리는 그 노드를 문제 발생 가능 지점으로 간주할 수 있습니다. 예컨대 "급여 불일치" 로그와 가장 유사도가 높게 나온 노드의 summary가 "직원의 기본급과 보너스를 더해 총 급여를 계산" 이라면, 아마도 그 부분의 로직에 문제가 있을 가능성이 높습니다.
그래프 DB를 활용하면 이 문제 지점을 둘러싼 맥락(Context)도 쉽게 얻을 수 있습니다. 해당 노드의 부모 노드를 타고 올라가면 이 코드가 속한 프로시저나 모듈을 알 수 있고, NEXT 관계를 따라가면 이전/이후에 어떤 로직이 실행되는지도 추적할 수 있습니다. 이는 단순한 텍스트 검색으로는 얻기 힘든 구조적 정보입니다. 요컨대, 그래프가 구조를 제공하고 임베딩이 의미를 제공하여 두 가지 방식이 서로 보완적으로 버그 위치를 찾아내는 데 기여합니다.
LLM으로 원인 분석 및 수정 제안
이제 LLM의 등장입니다. LLM은 Large Language Model의 약자로, GPT-4와 같은 거대 언어 모델을 말합니다. 앞 단계에서 우리는 문제 있을 법한 코드 노드와 관련 정보를 식별했습니다. 다음으로, 해당 코드 조각(예: Java 메소드의 일부)과 오류 로그를 함께 LLM에게 전달하여 "이 코드에서 무엇이 잘못됐고, 어떻게 고칠 수 있을까?"를 물어봅니다.
현대의 LLM은 프로그래밍 언어와 자연어를 모두 이해하고 생성할 수 있기 때문에, 코드와 로그를 함께 맥락으로 주면 마치 숙련된 시니어 개발자처럼 원인 추론과 해결책 제안을 해줄 수 있습니다. 실제 사례로, Sentry와 같은 오류 추적 도구에서도 LLM을 이용해 단순 스택트레이스 이상의 깊은 원인 분석과 해결책을 제안하기 시작했습니다. LLM은 해당 코드에서 논리적으로 잘못된 부분이나 누락된 케이스를 찾아내어, "어쩌면 보너스를 계산할 때 0값이 들어가는 경우를 처리하지 않아 발생한 문제입니다. 보너스가 없으면 기본급만 반영되도록 코드를 수정하세요." 같은 식으로 구체적인 수정 방향을 알려줄 수 있습니다.
또한 LLM은 필요하다면 수정된 코드 스니펫도 제안할 수 있습니다. 예를 들어 if 조건이나 수식에서 무엇을 변경해야 하는지 실제 코드 형태로 보여줄 수 있죠. 이는 일종의 AI 코드 리뷰어 겸 디버거가 곁에 있는 것과 같습니다.
수정 적용 및 그래프 갱신
LLM이 제안한 해결책을 토대로 코드를 수정합니다. 예를 들어 "보너스 금액이 null인 경우 0으로 간주하도록 처리" 라는 제안을 받았다면, Java 코드를 고쳐서 null 체크 로직을 추가하거나 계산 공식을 변경하겠죠. 수정 후에는 잊지 말아야 할 작업이 있습니다. 그래프와 임베딩을 업데이트하는 것입니다.
코드가 바뀌었으니 해당 구문 노드의 summary도 바뀔 수 있습니다. 우리는 변경된 코드에 맞게 노드의 summary를 갱신하고, 다시 Embedding API를 호출하여 새로운 node.embedding
벡터를 얻어 덮어씁니다. 이렇게 해야 다음 번에 이 부분을 검색하거나 분석할 때 최신 코드 상태에 기반한 정확한 결과를 얻을 수 있습니다. 그래프 DB의 구조적인 부분(노드들 간 관계)은 대부분 동일하겠지만, 경우에 따라 새로운 코드 구문이 추가되었다면 노드를 추가하고 관계를 연결하는 작업도 이뤄집니다. 예를 들어 없던 예외 처리 블록이 추가되었다면 새로운 노드와 PARENT_OF 관계를 생성하는 식입니다.
테스트 재실행과 반복 루프
코드 수정을 반영했으면 다시 한번 테스트를 실행합니다. 이제 앞서 문제가 되었던 시나리오에서 오류가 해결되었는지 확인합니다. 운이 좋으면 문제가 해결되고 모든 테스트가 통과될 것입니다. 그러면 피드백 루프는 여기서 종료됩니다.
만약 여전히 문제가 남아 있거나, 수정으로 인해 다른 곳에서 새로운 오류가 발생했다면 어떻게 될까요? 걱정할 필요 없습니다. 우리의 피드백 루프 에이전트는 같은 방법으로 다시 한 번 작업을 반복합니다. 새로 발생한 로그를 임베딩하고 → 관련 코드 노드를 찾고 → LLM으로 분석하고 → 수정하는 과정을 버그가 사라질 때까지 반복합니다. 이런 자동화된 반복을 통해 지속적으로 코드의 품질을 향상시킬 수 있습니다.
이러한 반복 피드백 루프의 개념은 사람이 디버깅하는 과정을 모방한 것입니다. 사람도 "테스트 실행 → 오류 확인 → 원인 위치 추정 → 코드 수정 → 재실행" 과정을 거듭하죠. 다만 우리의 에이전트는 그래프 DB와 임베딩, LLM의 힘으로 그 과정을 자동화하고 가속화할 뿐입니다.
그래프 + 임베딩 + LLM의 강력한 조합
이번 글에서는 그래프 데이터베이스와 임베딩 벡터, 그리고 LLM을 활용하여 코드 자체가 스스로 오류를 찾고 고치는 피드백 루프 시스템을 살펴보았습니다. 핵심 아이디어를 다시 정리하면 다음과 같습니다.
- 코드를 그래프로 표현하여 구조화된 지식을 얻었다. (노드와 관계를 통해 코드 이해도 ↑)
- 임베딩 벡터로 의미를 표현하여 코드 구문의 의미적 유사성을 계산 가능하게 했다. (시멘틱 검색 기능)
- 테스트 로그를 임베딩으로 변환하여 오류 메시지의 의미와 가장 관련 있는 코드 위치를 자동으로 찾아냈다. (원인 추적 자동화)
- LLM이 분석과 해결책을 제안하여 사람 개입 최소화로도 코드 수정 방안을 얻을 수 있었다. (지능형 디버깅)
- 수정→재테스트의 루프를 자동화하여, 오류가 없어질 때까지 이 사이클을 반복할 수 있게 했다. (지속적 개선)
그래프 DB와 벡터 임베딩의 조합은 각기 구조와 의미라는 두 측면을 모두 활용하기 때문에 매우 강력합니다. 그래프를 통해 맥락과 관계를 유지하면서도, 임베딩으로 의미 기반 연관성을 찾을 수 있어 더욱 정교하고 맥락에 맞는 결과를 얻을 수 있습니다. 그리고 여기에 LLM의 사고력을 더하면, 단순히 에러 위치를 찾는 것을 넘어 "왜 문제인지, 어떻게 고칠지"까지 자동으로 도출해낼 수 있게 됩니다.
미래에는 이러한 자동 코딩 에이전트가 개발자의 반복적인 디버깅 업무를 크게 덜어줄 것으로 기대됩니다. 물론 현 단계에서는 여전히 사람의 검토와 확인이 필요하지만, 기술이 발전할수록 에이전트의 제안 정확도와 수정 신뢰도는 더욱 높아지고 있습니다. 이번에 소개한 접근법이 여러분의 코드 유지보수 및 디버깅 프로세스에 작은 영감을 줄 수 있으면 좋겠습니다.