Enola 구축하기, 파트 2: 소스 코드에서 아키텍처 사실 모델(Architectural Fact Model)까지
요약
AI 에이전트 Enola가 소스 코드에서 단순한 구문 분석을 넘어 결정론적인 아키텍처 사실 모델을 추출하는 과정을 설명합니다. 파싱된 데이터를 기반으로 프레임워크의 맥락을 이해하여 의미 있는 아키텍처 관계를 구축하는 방법론을 다룹니다.
핵심 포인트
- 단순 파싱과 아키텍처 추출의 분리 필요성
- 타입이 지정된 아키텍처 사실 모델 구축의 중요성
- 프레임워크 및 언어 해석을 통한 관계 의미 보존
- 그래프 구조 내에서 관계의 신뢰성 확보
파트 1에서 저는 AI 에이전트가 추론을 시작하기 전에 Enola가 왜 결정론적인 아키텍처 사실(architectural facts)을 추출하는지 설명했습니다.
그다음 설계 질문이 남습니다:
이러한 사실들을 어떻게 표현해야 할까요?
명백한 답은 모든 심볼(symbol), 의존성(dependency), 경로(route), 서비스(service), 그리고 리포지토리(repository)를 포함하는 그래프를 구축하는 것입니다.
하지만 어려운 부분은 노드(node)와 엣지(edge)를 그래프에 넣는 것이 아닙니다.
그 관계들이 무엇을 의미하는지를 보존하는 것입니다.
함수 호출(function call)은 패키지 의존성(package dependency)과 같지 않습니다. 경로 등록(route registration)은 임포트(import)가 아닙니다. 타입 참조(type reference)는 두 서비스가 계약(contract)을 공유한다는 증거가 아닙니다.
만약 모든 관계가 일반적인 연결(generic connection)이 되어버린다면, 결과물인 그래프는 탐색(traversable)은 가능할지 몰라도 아키텍처 분석을 수행할 만큼 충분히 신뢰할 수 없게 됩니다.
따라서 Enola는 타입이 지정된 아키텍처 사실 모델(typed architectural fact model)로 시작합니다.
파싱(Parsing)은 증거를 제공할 뿐, 아키텍처를 제공하지는 않는다
파서(parser)는 함수 호출(function call), 문자열 리터럴(string literal), 임포트(import), 어노테이션(annotation), 또는 메서드 선언(method declaration)이 존재한다는 것을 알려줄 수 있습니다.
이는 필요하지만, 아직 아키텍처 사실은 아닙니다.
다음 Go 애플리케이션을 고려해 보십시오:
api := router.PathPrefix("/api").Subrouter()
registerCourseRoutes(api)
다른 곳에서는:
func registerCourseRoutes(router *mux.Router) {
router.HandleFunc("/courses", listCourses)
}
파서는 문자열 리터럴과 함수 호출을 모두 드러낼 수 있습니다.
아키텍처 사실은 다음과 같습니다:
GET /api/courses
-> handled_by listCourses
이러한 사실을 생성하려면 프레임워크, 라우터가 경로를 구성하는 방식, 그리고 값이 함수 호출을 통해 어떻게 이동하는지를 이해해야 합니다.
동일한 문제가 Spring 어노테이션(annotations), Rails 스코프(scopes), Axum 라우터(routers), Next.js 파일 컨벤션(file conventions), 생성된 클라이언트(generated clients), 의존성 주입(dependency-injection) 프레임워크, 그리고 메시지 버스(message-bus) 설정에서도 나타납니다.
따라서 Enola는 파싱(parsing)과 아키텍처 추출(architectural extraction)을 분리합니다:
소스 코드 (Source code)
↓
언어 및 프레임워크 해석 (Language and framework interpretation)
...
파서는 구문(syntax)을 제공합니다.
추출기(extractor)는 해당 구문이 아키텍처 내에서 무엇을 의미하는지를 결정합니다.
저장소 경계는 추출 범위(extraction scope)입니다
Git 저장소(repository)는 분석을 시작하기에 편리한 장소입니다.
저장소는 소스 리비전(revision), 설정 경계(configuration boundary), 파일, 빌드 메타데이터(build metadata), 그리고 안정적인 소스 위치를 제공합니다.
하지만 저장소가 반드시 아키텍처적 진실(architectural truth)의 단위인 것은 아닙니다.
모노레포(monorepo)는 독립적으로 배포되는 여러 서비스를 포함할 수 있습니다. 작은 저장소는 다른 곳에서 관리되는 스키마(schema), 인프라(infrastructure), 또는 생성된 클라이언트(generated clients)에 의존할 수 있습니다. 런타임 동작(runtime behavior) 또한 애플리케이션 저장소 외부의 배포 설정(deployment configuration)에 의존할 수 있습니다.
따라서 Enola는 저장소를 독립적으로 주소 지정이 가능한 **추출 범위 (extraction scope)**로 취급합니다.
해당 범위 내에서 Enola는 다음과 같은 사실(facts)을 확립할 수 있습니다:
Repository contains module
Module contains file
File declares symbol
...
이러한 사실들은 다른 저장소가 로드되지 않은 상태에서도 유용하게 유지됩니다.
범위(scope)는 각 엔티티(entity)에 로컬 식별성(local identity)과 출처(provenance)를 부여합니다. 이것이 저장소 내에 전체 아키텍처가 포함되어 있음을 의미하지는 않습니다.
소스 소유권 경계(source ownership boundaries)와 아키텍처 경계(architectural boundaries)는 일치하는 경우가 드물기 때문에 이러한 구분은 중요합니다.
서로 다른 사실에는 서로 다른 식별 규칙이 필요합니다
모든 아키텍처 개념에 적용되는 보편적인 식별자(identifier)는 존재하지 않습니다.
심볼(symbol)에는 다음이 필요할 수 있습니다:
- 추출 범위 (extraction scope)
- 언어 (language)
- 정규화된 이름 (qualified name)
- 소스 위치 (source location)
- 소스 리비전 (source revision)
HTTP 라우트(route)에는 다음이 필요할 수 있습니다:
- 메서드 (method)
- 정규화된 경로 (normalized path)
- 서비스 컨텍스트 (service context)
gRPC 메서드에는 다음이 필요할 수 있습니다:
- 패키지 (package)
- 서비스 (service)
- 메서드 (method)
Kafka 토픽(topic)에는 다음이 필요할 수 있습니다:
- 해결된 토픽 이름 (resolved topic name)
- 네임스페이스(namespace) 또는 환경 컨텍스트 (environment context)
이것이 이름만으로는 불충분한 이유입니다.
두 저장소가 모두 동일한 계약(contract)을 참조하지 않고도 각각 UserDTO를 포함할 수 있습니다. 반대로, PublicUser라고 불리는 Go 타입과 ProfileResponse라고 불리는 Swift 타입은 동일한 API의 양면을 나타낼 수 있습니다.
Enola는 이들 사이에 관계를 확립할 증거가 나타나기 전까지는 해당 엔티티들을 별개로 유지합니다.
타입이 지정된 관계(Typed relationships)는 두 요소가 연결된 이유를 보존합니다
일반적인 그래프는 다음과 같이 표현할 수 있습니다:
A -> B
하지만 아키텍처 분석(architectural analysis)을 위해서는 해당 에지(edge)가 왜 존재하는지 알아야 합니다.
다음 사례들을 비교해 보십시오:
CheckoutController
-> PaymentService.authorize를 호출(calls)
checkout
-> payments를 임포트(imports)
POST /checkout
-> CheckoutController에 의해 처리됨(handled_by)
이러한 관계들은 서로 다른 질문들을 지원합니다.
호출(call) 에지는 도달 가능성(reachability)을 확인하는 데 유용할 수 있습니다.
임포트(import) 에지는 의존성 사이클(dependency-cycle) 탐지에 유용할 수 있습니다.
경로-핸들러(route-to-handler) 에지는 요청 실행(request execution)을 추적하는 데 유용할 수 있습니다.
방향(direction) 또한 중요합니다. 클라이언트가 경로(route)를 소비(consume)하는 것이지, 경로가 클라이언트를 소비하는 것이 아닙니다.
따라서 Enola는 관계를 일반적인 연결성이 아닌, 타입이 지정되고 방향성이 있는 사실(typed and directed facts)로 표현합니다.
그렇다고 해서 추출된 모든 에지가 동일하게 강력한 것은 아닙니다.
관계는 이를 생성하는 데 사용된 다음과 같은 증거(evidence)를 유지해야 합니다:
- 소스 위치(source locations)
- 추출기(extractor)
- 해결 방법(resolution method)
- 소스 리비전(source revision)
- 설정(configuration)
- 직접 추출되었는지 또는 유도되었는지 여부
이러한 증거가 없다면, 그래프는 또 다른 불투명한 답변이 되어버립니다.
하나의 모델, 여러 분석 프로젝션(analysis projections)
전체 사실 모델(fact model)은 단일 분석이 탐색해야 하는 것보다 더 많은 관계를 포함하고 있습니다.
어떤 하위 집합이 관련이 있는지는 질문에 따라 결정됩니다.
패키지 사이클(package-cycle) 탐지의 경우:
노드(Nodes): 패키지(packages)
에지(Edges): 패키지 의존성(package dependencies)
심볼 도달 가능성(symbol reachability)의 경우:
노드(Nodes): 심볼(symbols)
에지(Edges): 호출(calls) 및 참조(references)
경로 실행(route execution)의 경우:
노드(Nodes): 경로(routes), 핸들러(handlers), 서비스(services)
에지(Edges): handled_by 및 calls
이것이 중요한 이유는 사용 가능한 모든 에지를 사용하면 기술적으로는 연결되어 있지만 아키텍처적으로는 의미 없는 경로가 생성될 수 있기 때문입니다.
어떤 타입이 특정 패키지에 속해 있고, 그 패키지가 다른 패키지에 의존하며, 두 번째 패키지에 HTTP 경로가 포함되어 있다고 가정해 봅시다.
전체 그래프를 통하면 경로가 존재합니다.
하지만 그것이 해당 타입이 그 경로의 실행에 참여한다는 것을 의미하지는 않습니다.
따라서 Enola는 모든 순회(traversal)를 동일하게 취급하는 대신, 특정 분석을 위한 제약된 투영(constrained projections)을 구축합니다.
구체적인 예시: 일반적인 의존성 그래프가 실패하는 이유
동일한 모노레포(monorepo)에 저장된 프론트엔드와 백엔드를 가정해 봅시다.
프론트엔드는 API 클라이언트 패키지를 임포트합니다:
web
-> depends_on api-client
백엔드는 라우팅 프레임워크를 임포트합니다:
backend
-> depends_on router
전통적인 의존성 그래프는 두 관계를 모두 정확하게 기록합니다.
하지만 다음과 같은 질문에는 답할 수 없습니다:
어떤 프론트엔드 메서드가
POST /api/orders를 소비하는가?
이 질문에 답하기 위해서는 몇 가지 추가적인 사실(facts)이 필요합니다:
submitOrder
-> makes_request POST /api/orders
POST /api/orders
-> handled_by CreateOrderHandler
CreateOrderHandler
-> calls OrderService.Create
유용한 아키텍처 경로는 패키지 의존성 경로가 아닙니다.
그것은 요청(request), 경로(route), 핸들러(handler), 그리고 호출(call) 관계를 결합한 투영(projection)입니다.
이것이 바로 Enola가 단순한 리포지토리 의존성 그래프뿐만 아니라, 타입이 지정된 아키텍처 사실 모델(typed architectural fact model)을 필요로 하는 이유입니다.
모델의 한계
Enola는 로드된 소스 및 설정 입력으로부터 확립될 수 있는 아키텍처를 구축합니다.
소스 코드만으로 프로덕션 시스템의 모든 측면을 설명할 수 있다고 주장하지는 않습니다.
배포 매니페스트(Deployment manifests), 게이트웨이(gateways), 서비스 메시(service meshes), 런타임 설정(runtime configuration), 리플렉션(reflection), 피처 플래그(feature flags), 그리고 인프라스트럭처는 런타임에 보이는 아키텍처를 변경할 수 있습니다.
따라서 모델은 다음을 구분해야 합니다:
- 확립된 사실 (established facts)
- 유도된 관계 (derived relationships)
- 해결되지 않은 관계 (unresolved relationships)
- 분석 범위 외의 정보 (information outside the analyzed scope)
이는 개발자와 에이전트(agents) 모두에게 중요합니다.
엣지(edge)가 누락되었다고 해서 항상 관계가 존재하지 않는다는 것을 의미하지는 않습니다. 이는 관련 소스, 설정 또는 리졸버(resolver)를 사용할 수 없었음을 의미할 수 있습니다.
다음 단계
타입이 지정된 사실 모델은 하나의 추출 범위(extraction scope) 내의 아키텍처를 설명할 수 있습니다.
프로덕션 시스템은 이러한 범위들을 가로질러 존재합니다.
모바일 클라이언트가 다른 곳에서 관리되는 백엔드를 호출합니다. 하나의 서비스가 이벤트를 발행하면 다른 리포지토리(repository)가 이를 소비합니다. 생성된 클라이언트(generated client)는 별도의 코드베이스(codebase)에 정의된 계약(contract)을 구현합니다.
다음 포스트에서는 Enola가 이름이나 유사성만으로 엔티티(entity)를 병합하지 않고, 어떻게 이러한 독립적으로 추출된 모델들을 연결하는지 설명하겠습니다.
Enola는 오픈 소스입니다 GitHub
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기