Dev Stories

LangChain으로 완성하는 코드 분석 자동화 E2E 구현 경험 공유

안녕하세요. KT 기술혁신부문, ITDev본부 AX프로토타입팀 소속 박경덕입니다.

LLM을 통한, ‘서비스 코드 분석 기능’ 구현 과정에서 마주했던 기술적 문제점과 해결에 대한 경험을 공유드립니다.


기획 : 왜 서비스 코드 분석인가?

클라우드 또는 온프레미스 환경을 Azure로 이전하는 과정에서,
코드/인프라 스택·마이그레이션 영향도를 사전에 분석할 수 있는 자동화 기능의 필요성을 인식해 기획을 시작했습니다.


이러한 분석을 통해 호환성 문제를 조기에 발견하고, 

나아가 비용과 일정을 예측함으로써 전략을 보다 안정적이고 체계적으로 추진할 수 있을 것으로 기대했습니다.

초기 기획

  • Azure AI Search를 통한 의미 기반 검색
    기존 코드베이스를 단순 키워드 검색이 아닌,
    의미 단위로 탐색할 수 있도록 Azure AI Search를 활용하는 방향으로 기획을 시작했습니다.

  • Azure OpenAI를 통한 확장
    단순 검색을 넘어, LLM과 결합하면 코드 해석과 질의응답까지 가능할 것으로 기대하였습니다.

최종 설계

그러나 이후 말씀드릴 “자연어 임베딩의 한계”로 인해, 다음과 같이 최종적으로 구조를 변경했습니다.


  • GraphDB를 통한 파일/폴더 구조 데이터 적재

  • VectorDB 내 코드 의미 임베딩 데이터 보존

  • Azure OpenAI를 통한 확장


이제, 왜 설계가 이렇게 바뀌었는지. 또 결과 도출을 위해 어떤 과정을 거쳤는지 써보겠습니다!


임베딩이란 ?
임베딩은 글이나 코드를 숫자로 바꿔 의미의 비슷함을 계산할 수 있게 하는 기술입니다.
그를 통해, 단순 키워드 유사도가 아니라 ‘의미상 가까운 것’을 더 잘 찾아낼 수 있습니다.


#1 자연어 임베딩의 한계

Azure 내 Blob, SQL Server 등에 데이터를 적재하고, 

이를 AI Search로 옮겨오며 임베딩을 진행, 검증하던 중 문제를 파악했습니다.


AI Search에 “Logging 함수” 등의 자연어 질의를 던졌을 때, 

결과는 얼핏 그럴싸했지만 아래와 같은 이슈가 생긴 것이었는데요.


  • 다르게 명명된, 실제로 동일하게 기능하는 함수 파악 불가

  • 변수/함수명에 의존적


그렇게 자연어로 접근하는 것이 아닌, 코드의 의미를 반영해야 한다는 목표가 생겼습니다.


코드의 기능적 의미를 파악하는 임베딩 모델

Function.gif

< 그림 1. 함수 임베딩 비교: 의미 군집화의 관점에서 >


즉, 대개는 사람이 확인하기 쉽게 자연어로 변수/함수명이 작성되고, 그러한 데이터들로 인해 얼핏 그럴싸한 검출이 이뤄졌던 것입니다.


그래서 위 예시처럼, AI Search를 활용한 방식(좌측)은 

문장의 자연어 의미를 좇는 만큼, 코드를 이루는 단어 의미에 의존하며, "기능적인 의미를 추적하기 어렵다"는 결론에 다다랐습니다.


이에 코드 의미를 파악 할 수 있는 모델(GraphCodeBert)를 추가로 선정하여, 이를 자연어 모델과 비교 후 결과를 확인하고자 했습니다.
이미지

< 그림 2. 테스트에 활용한 각 언어별 함수 구성 >


데이터는 동일한 기능의 함수를 배치한 뒤 언어를 다르게 하고, 이름이나 변수명을 모두 무작위로 변경하였습니다.

이런 환경에서는, 코드 구조와 더불어 코드의 의미까지 전부 파악해야 테두리로 표시한 로그 작성부분을 검출할 수 있을 것입니다.


그리고, 각 모델의 임베딩 데이터를 대상으로 다시 “Logging 함수” 질의를 수행했습니다.

이미지
< 그림 3. 각 모델의 로그 관련 함수 검출 결과 >


언어나 변수/함수명의 무작위성에도 불구하고, GraphCodeBert는 타 모델보다 관련 코드를 더 정확하게 검출하는 모습을 확인할 수 있었습니다.


쉽게 나타내자면, 아래와 같은 차이가 드러난 것으로 볼 수 있겠습니다.

의검비.gif

< 그림 4. 자연어 검색과 코드 의미 검색의 차이 >


이 검증/개발을 통해 코드를 기능적 의미에서 접근할 수 있게 되어, LLM에 단서를 줄 수 있는 기반을 마련했습니다.

그리고 자연스럽게, 초기 기획과 달리 VectorDB 내 코드 의미 임베딩 데이터 보존 방식을 채택하게 되었습니다.


추가로 AI Search를 벗어나 별도의 DB를 구축하는 등 어느 정도 설계에 대한 자유도가 생겨,

소스코드의 구조(파일/폴더)를 담을 수 있도록 GraphDB를 통한 파일/폴더 구조 데이터 적재를 함께 추가하였습니다.


#2 LangChain 도입

앞서 DB를 구축함으로써, Azure OpenAI는 더 이상 데이터 검색을 직접 수행하지 않게 되었습니다.

본래는 AI Search로의 검색도 함께 이뤄지는데, DB가 독자적으로 구성되면서 자연스레 추론엔진으로 역할이 제한되었습니다.


이에, LLM이 필요할 때 적절한 도구를 선택하여 호출, 수행할 수 있도록 LangChain의 Agent 패턴을 도입했습니다.


Agent 패턴은 LLM이 단순히 입력에 응답하는 수준을 넘어,
필요에 따라 외부 도구를 선택·호출하며 문제를 해결하는 구조를 말합니다.


시행착오 ① : 기본 도구(Tool) 사용

처음에는 LangChain에서 제공하는 기본 Tool 인터페이스만으로 GraphDB·VectorDB 검색을 연결했습니다.

그러나 이 방식은 입/출력값을 다루기 불편했고, 특히 LLM이 구조화된 반환값을 적절히 참조하게 만드는 데 번거로움이 컸습니다.


이를 개선하기 위해 입출력 형식을 명확히 정의한 커스텀 툴을 별도로 설계하여 적용했습니다.


시행착오 ② : 쿼리 직접 생성 도구

또 다른 접근으로, LLM이 직접 GraphDB·VectorDB 쿼리를 생성해 실행하고 응답을 받도록 유도했습니다.


  • 매번 생성되는 쿼리 편차가 커 정확성이 낮음
  • 복잡한 쿼리가 생성되어 수행 시간이 늦는 경우가 많음


그러나, 테스트를 통해 위 신뢰성 문제가 드러나 본 방법을 폐기하게 되었습니다.


해결 : 도구 역할 세분화 + Agent 패턴 도입

아래와 같이, Cursor IDE와 GitHub Copilot을 사용하며 얻은 아이디어가 바탕이 되었습니다.


도구호출.png

< 그림 5. 각 IDE에서 도구를 호출하는 모습 >


도구의 입출력 제한에서 더 나아가, 각 도구의 역할을 좀 더 특정하며 가짓수를 늘려주었습니다.

예를 들자면, 각 DB에 질의하는 쿼리를 미리 만들어두고 쿼리의 대상만 파라미터로 조정하는 등의 단순화를 거쳤습니다.


즉, LLM이 임의로 쿼리를 생성하기보단 도구를 좀 더 세분화하여 용도에 맞게 선택할 수 있도록 개선했습니다.


이미지

< 그림 6. LLM이 선택할 수 있는 도구들 >


최종적으로 아래와 같이 사용자의 쿼리(요청)를 위해, 도구들로써 단서를 계속 보강하여 응답하는 로직이 구성되었습니다.


   [1] 입력(단서 + 질문) 이 주어지면, LLM은 단서를 기반으로 답변 가능 여부를 판단

   [2-1] 답변 가능하다면, 단서들을 조합하여 최종 답변을 낸다.

   [2-2] 불가하다면, 추가 도구를 발행하고, 이를 수행토록 한다.

   [3] 수행을 진행하며, 단서를 보강한 뒤 [1]로 이동한다.


LangChain의 ReAct(Reasoning + Acting) 패턴과 맥락이 비슷하다고 볼 수 있겠습니다.

즉, 분석 요청에 대해, 단서를 점차 보강하며 최종 응답에 도달하는 방식으로 동작합니다.


#3 요약 : 입출력 토큰 상한문제

대규모 코드베이스를 다루는 과정에서, 특정 커스텀 툴(예: 파일 내용 읽기)은 수천 줄의 내용을 반환하기도 합니다. 


실제로 한 사례에서는 4,000줄 분량의 코드를 읽어 온 뒤, 

바로 LLM에 전달했다가 TPM(Tokens Per Minute) 상한을 넘는 문제로 분석이 중단되기도 했습니다.


비용과 지연, 그리고 응답 품질 측면 모두를 감안할 때 필히 해결해야 할 문제였습니다.


시도  : 코드 단순 축소

우선, 코드 자체를 물리적으로 압축하는 방법을 고려했습니다.


  • indent 축소
  • 2개 이상의 연속된 줄바꿈 변환
  • 주석 처리된 코드 제거

하지만 언어에 따라 indent가 필요치 않은 경우도 있었고, 주석이 없는 등 단순 제거만으로는 큰 축소가 어려웠습니다.

말 그대로 해당 방식을 통해 얻을 수 있는 토큰 절감의 효과는 제한적이어서, 다른 방법을 고안해야 했습니다.


시도 ② : LLM을 통한 요약

툴 실행 결과를 그대로 쓰는 대신 한 번 더 LLM을 거쳐 요약(summary)을 생성하는 방식을 시도했습니다.


해당 방식은 분량을 줄이는 데 큰 도움은 되었지만,

일방적으로 요약이 이뤄지다 보니, 필요한 정보가 누락되거나 다소 희석되는 등의 문제가 발생했습니다.


해결 : 수행 의도 + LLM 요약

최종적으로는, 최초 도구를 발행하면서 수행 의도를 함께 반환하도록 LLM 로직을 개선했습니다.

그를 통해, 해당 도구의 수행결과로 무엇을 얻고자 하는지를 요약에 포함하여, 의도와 방향성을 부여했습니다.


이에 따라 핵심 의미를 보존하면서도, 크게는 토큰을 40% 가량 줄일 수 있었습니다.


추가 조정 : 모델 분리

이미지

< 그림 7. 탐정(판단) LLM과 조수(요약) LLM >


요약은 의미를 유지하며 내용을 다시 생성하므로 나름의 창의성이 필요했고, 

반대로 판단은 주어진 단서만으로 수행하는 만큼 최대한 보수적이어야 했습니다.


즉 같은 LLM이라도 작용하는 영역이 다르니, 용도에 따라 모델을 달리 사용하면 더욱 목적에 부합하는 응답을 낼 것으로 보았습니다.

Azure OpenAI의 경우, 다양한 모델을 파라미터로 조정할 수 있어 해당 방식을 충분히 사용할 수 있었습니다.


이미지
< 그림 8. 각 LLM의 다른 파라미터 구성 >


위와 같이, 아직 모델은 GPT-4.1로 제한하고 있지만 파라미터를 구분하여 나름의 역할을 줄 수 있었습니다.

여러 검증을 통해 입력 토큰을 좀 더 최적화할 수 있다면, 더욱 다양한 모델을 활용할 수 있을 것으로 보고 있습니다.



며...

이미지

< 그림 9. 간단하게 나타낸 Agent의 동작 >


위의 시행착오를 거치며, 단순히 AI Search에 의존하는 초기 기획에서 벗어나 

VectorDB와 GraphDB를 종합적으로 활용하는 Agent 구조를 마련할 수 있었습니다.


지금은 LLM이 요약 외에 도구 발행, 단서 수집 판단, 최종 응답 생성을 함께 수행하고 있지만, 

LLM을 좀 더 세분화하여 플래닝(도구발행)과 판단, 응답 생성의 역할을 분리할 계획에 있습니다.


외에도, SonarQube 등의 정적 분석 도구를 활용하여

품질/보안/복잡도 지표를 LLM에 단서로 제공하는 방안 또한 고려하고 있습니다.


마지막으로,

이번 프로젝트를 통해 단순히 LangChain과 LLM을 적용하는 경험을 넘어, 

데이터베이스·도구·LLM의 경계 설정이 얼마나 중요한지 배울 수 있었습니다. 


자유로움보다 "역할을 분리하고 도구를 세분화하는 것"이 정확성과 효율을 높였기에,

LLM이 외려 제한된 규칙 속에서 가장 잘 작동한다는 점이 주요했습니다. 


앞으로도 이러한 경험을 바탕으로, 

새로운 기술을 단순히 도입하는 데 그치지 않고 맥락에 맞게 배치하고 운용하는 능력을 계속 확장해 나가려 합니다.


박경덕

MVP를 구현하고 검증하는 업무를 담당하고 있습니다.