안녕하세요. 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 함수” 등의 자연어 질의를 던졌을 때,
결과는 얼핏 그럴싸했지만 아래와 같은 이슈가 생긴 것이었는데요.
-
다르게 명명된, 실제로 동일하게 기능하는 함수 파악 불가
-
변수/함수명에 의존적
그렇게 자연어로 접근하는 것이 아닌, 코드의 의미를 반영해야 한다는 목표가 생겼습니다.
코드의 기능적 의미를 파악하는 임베딩 모델

< 그림 1. 함수 임베딩 비교: 의미 군집화의 관점에서 >
즉, 대개는 사람이 확인하기 쉽게 자연어로 변수/함수명이 작성되고, 그러한 데이터들로 인해 얼핏 그럴싸한 검출이 이뤄졌던 것입니다.
그래서 위 예시처럼, AI Search를 활용한 방식(좌측)은
문장의 자연어 의미를 좇는 만큼, 코드를 이루는 단어 의미에 의존하며, "기능적인 의미를 추적하기 어렵다"는 결론에 다다랐습니다.

< 그림 2. 테스트에 활용한 각 언어별 함수 구성 >
데이터는 동일한 기능의 함수를 배치한 뒤 언어를 다르게 하고, 이름이나 변수명을 모두 무작위로 변경하였습니다.
이런 환경에서는, 코드 구조와 더불어 코드의 의미까지 전부 파악해야 테두리로 표시한 로그 작성부분을 검출할 수 있을 것입니다.

언어나 변수/함수명의 무작위성에도 불구하고, GraphCodeBert는 타 모델보다 관련 코드를 더 정확하게 검출하는 모습을 확인할 수 있었습니다.
쉽게 나타내자면, 아래와 같은 차이가 드러난 것으로 볼 수 있겠습니다.

< 그림 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을 사용하며 얻은 아이디어가 바탕이 되었습니다.

< 그림 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 요약
그를 통해, 해당 도구의 수행결과로 무엇을 얻고자 하는지를 요약에 포함하여, 의도와 방향성을 부여했습니다.
이에 따라 핵심 의미를 보존하면서도, 크게는 토큰을 40% 가량 줄일 수 있었습니다.
추가 조정 : 모델 분리

< 그림 7. 탐정(판단) LLM과 조수(요약) LLM >
요약은 의미를 유지하며 내용을 다시 생성하므로 나름의 창의성이 필요했고,
반대로 판단은 주어진 단서만으로 수행하는 만큼 최대한 보수적이어야 했습니다.
즉 같은 LLM이라도 작용하는 영역이 다르니, 용도에 따라 모델을 달리 사용하면 더욱 목적에 부합하는 응답을 낼 것으로 보았습니다.
Azure OpenAI의 경우, 다양한 모델을 파라미터로 조정할 수 있어 해당 방식을 충분히 사용할 수 있었습니다.

위와 같이, 아직 모델은 GPT-4.1로 제한하고 있지만 파라미터를 구분하여 나름의 역할을 줄 수 있었습니다.
여러 검증을 통해 입력 토큰을 좀 더 최적화할 수 있다면, 더욱 다양한 모델을 활용할 수 있을 것으로 보고 있습니다.
마치며...

< 그림 9. 간단하게 나타낸 Agent의 동작 >
위의 시행착오를 거치며, 단순히 AI Search에 의존하는 초기 기획에서 벗어나
VectorDB와 GraphDB를 종합적으로 활용하는 Agent 구조를 마련할 수 있었습니다.
지금은 LLM이 요약 외에 도구 발행, 단서 수집 판단, 최종 응답 생성을 함께 수행하고 있지만,
LLM을 좀 더 세분화하여 플래닝(도구발행)과 판단, 응답 생성의 역할을 분리할 계획에 있습니다.
외에도, SonarQube 등의 정적 분석 도구를 활용하여
품질/보안/복잡도 지표를 LLM에 단서로 제공하는 방안 또한 고려하고 있습니다.
마지막으로,
이번 프로젝트를 통해 단순히 LangChain과 LLM을 적용하는 경험을 넘어,
데이터베이스·도구·LLM의 경계 설정이 얼마나 중요한지 배울 수 있었습니다.
자유로움보다 "역할을 분리하고 도구를 세분화하는 것"이 정확성과 효율을 높였기에,
LLM이 외려 제한된 규칙 속에서 가장 잘 작동한다는 점이 주요했습니다.
앞으로도 이러한 경험을 바탕으로,
새로운 기술을 단순히 도입하는 데 그치지 않고 맥락에 맞게 배치하고 운용하는 능력을 계속 확장해 나가려 합니다.