GraphDB
Neo4j
PHP
Rust
Code Analysis
Dependency
AWS
GraphDB로 코드 구조 시각화 하기 (2/4): PHP 의존성 분석 도구 구축 실습
시리즈 연결
- Part 1: Neo4j와 프로그램 분석의 만남
 - Part 2: PHP 의존성 분석 도구 구축 실습 (현재 글)
 - Part 3: AWS 환경에서 Neo4j 배포와 파이프라인 구축
 - Part 4: 고급 시각화와 프로덕션 운영
 - Part 5: Cypher 쿼리 언어 완벽 가이드
 
개요
Part 1에서 GraphDB의 기본 개념과 Neo4j를 학습했습니다. 이번 Part 2에서는 실제 레거시 PHP 프로젝트를 대상으로 파일 간 의존성을 분석하고 Neo4j에 저장하는 도구를 직접 구축합니다.
대규모 PHP 프로젝트에서 수천 개의 파일이 복잡하게 얽힌 include, require 관계를 파악하는 것은 리팩토링과 마이그레이션의 첫 단계입니다. GraphDB를 활용하면 이러한 의존성을 효과적으로 시각화하고 분석할 수 있습니다.
학습 목표
- Rust로 고성능 PHP 파일 스캐너 구현
 - 정규표현식 기반 의존성 추출 로직
 - Neo4j 그래프 모델링과 Cypher 활용
 - 실전 의존성 분석 쿼리 패턴
 - AWS 환경 배포를 위한 설계 고려사항
 
1. 아키텍처 설계
1.1 시스템 구성도
graph TD
    A[PHP Project<br/>Source Files] --> B[Rust Scanner<br/>Analyzer]
    B --> C[Neo4j DB<br/>Graph DB]
    C --> D[Cypher<br/>Queries]
    
    style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px
    style B fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style C fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    style D fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
1.2 데이터 플로우
- 파일 스캔: 프로젝트 디렉토리를 재귀적으로 탐색
 - 패턴 추출: 정규표현식으로 
include_once구문 파싱 - 그래프 생성: Neo4j에 노드(파일)와 관계(의존성) 저장
 - 분석 및 시각화: Cypher 쿼리로 의존성 패턴 분석
 
1.3 기술 스택 선정 근거
Rust 선택 이유
- 성능: 수만 개 파일도 빠르게 처리 (C++ 수준 성능)
 - 안전성: 컴파일 타임에 메모리 안전성 보장
 - 비동기 처리: Tokio를 통한 효율적인 I/O 처리
 - 크로스 플랫폼: AWS Lambda, ECS, EC2 어디서든 배포 가능
 
Neo4j 선택 이유
- 관계 최적화: 의존성 그래프 순회에 최적화된 성능
 - Cypher 언어: 직관적인 그래프 쿼리
 - ACID 보장: 데이터 일관성 보장
 - 확장성: AWS Neptune으로 마이그레이션 가능
 
2. 스캐너 구현
2.1 프로젝트 셋업
# Rust 프로젝트 생성
cargo new php-dependency-scanner
cd php-dependency-scanner
# Cargo.toml 의존성 추가
[package]
name = "php-dependency-scanner"
version = "0.1.0"
edition = "2021"
[dependencies]
neo4rs = "0.7"           # Neo4j 클라이언트
tokio = { version = "1", features = ["full"] }
regex = "1.10"           # 정규표현식
walkdir = "2"            # 디렉토리 순회
serde = { version = "1.0", features = ["derive"] }
config = "0.14"          # 설정 파일 관리
2.2 핵심 코드 구현
use neo4rs::*;
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ⚠️ 보안: 환경변수에서 민감정보 로드
    let neo4j_uri = std::env::var("NEO4J_URI")
        .unwrap_or_else(|_| "neo4j://127.0.0.1:7687".to_string());
    let neo4j_user = std::env::var("NEO4J_USER")
        .unwrap_or_else(|_| "neo4j".to_string());
    let neo4j_password = std::env::var("NEO4J_PASSWORD")?;
    
    // Neo4j 연결 (TLS 권장)
    let graph = Graph::new(&neo4j_uri, &neo4j_user, &neo4j_password).await?;
    
    // 기존 데이터 초기화 (선택적)
    graph.run(query("MATCH (n) DETACH DELETE n")).await?;
    
    // include_once 패턴 정의
    let re = Regex::new(r#"include_once\s*\(\s*([^)]+)\s*\)"#)?;
    
    // ⚠️ 보안: 스캔 디렉토리 검증
    let project_dir = std::env::var("PROJECT_DIR")
        .unwrap_or_else(|_| "/path/to/php/project".to_string());
    let project_path = Path::new(&project_dir).canonicalize()?;
    
    let mut dependency_count = 0;
    
    // 모든 PHP 파일 순회
    for entry in WalkDir::new(&project_path)
        .into_iter()
        .filter_map(|e| e.ok()) 
    {
        // .php 파일만 처리
        if entry.path().extension().and_then(|s| s.to_str()) == Some("php") {
            // ⚠️ 보안: 경로 검증 (경로 순회 공격 방지)
            if !is_safe_path(entry.path(), &project_path) {
                continue;
            }
            
            if let Ok(content) = fs::read_to_string(entry.path()) {
                let source = entry.path()
                    .strip_prefix(&project_path)?
                    .to_str()
                    .unwrap();
                
                // 모든 include_once 매칭
                for cap in re.captures_iter(&content) {
                    let target = cap[1].trim_matches(|c| c == '"' || c == '\'').trim();
                    
                    // ⚠️ 보안: 입력 검증
                    if !is_valid_php_path(target) {
                        continue;
                    }
                    
                    // 공통 파일 제외
                    if target != "./_common.php" {
                        // ⚠️ 보안: 파라미터 바인딩 사용 (Cypher injection 방지)
                        graph.run(query(
                            "MERGE (s:PHPFile {path: $source}) 
                             MERGE (t:PHPFile {path: $target}) 
                             MERGE (s)-[:INCLUDES]->(t)"
                        )
                        .param("source", source)
                        .param("target", target))
                        .await?;
                        
                        dependency_count += 1;
                    }
                }
            }
        }
    }
    
    println!("✓ Saved {} dependencies to Neo4j", dependency_count);
    Ok(())
}
// ⚠️ 보안 함수: 경로 순회 공격 방지
fn is_safe_path(path: &Path, base: &Path) -> bool {
    path.canonicalize()
        .map(|p| p.starts_with(base))
        .unwrap_or(false)
}
// ⚠️ 보안 함수: PHP 파일 경로 검증
fn is_valid_php_path(path: &str) -> bool {
    // 위험한 패턴 차단
    !path.contains("..") &&           // 디렉토리 순회
    !path.starts_with("/") &&         // 절대 경로
    !path.contains("://") &&          // URL 스키마
    path.len() < 500 &&               // 경로 길이 제한
    path.chars().all(|c| {            // 허용된 문자만
        c.is_alphanumeric() || 
        c == '/' || c == '.' || c == '_' || c == '-'
    })
}
2.3 코드 상세 분석
정규표현식 패턴
let re = Regex::new(r#"include_once\s*\(\s*([^)]+)\s*\)"#)?;
패턴 구조:
include_once: 리터럴 매칭\s*: 공백 0개 이상 (유연한 코딩 스타일 지원)\(/\): 괄호 매칭 (이스케이프 필요)([^)]+): 캡처 그룹 - 괄호 내부 내용 (파일 경로)
매칭 예시:
include_once("config.php");                    // ✓
include_once( "./utils/helper.php" );          // ✓
include_once(    DIR_SYSTEM . 'startup.php');  // ✓
디렉토리 재귀 탐색
for entry in WalkDir::new(project_dir)
    .into_iter()
    .filter_map(|e| e.ok())
최적화 포인트:
filter_map(|e| e.ok()): 에러 무시, 성공한 엔트리만 처리- 심볼릭 링크 자동 처리
 - 대용량 디렉토리에도 메모리 효율적
 
Cypher MERGE 전략
MERGE (s:PHPFile {path: $source}) 
MERGE (t:PHPFile {path: $target}) 
MERGE (s)-[:INCLUDES]->(t)
MERGE vs CREATE:
CREATE: 중복 노드/관계 생성 → 데이터 오염MERGE: 없으면 생성, 있으면 재사용 → 멱등성 보장
성능 고려사항:
- 인덱스 필수: 
CREATE INDEX FOR (f:PHPFile) ON (f.path) - 배치 처리: 수천 개 쿼리는 트랜잭션으로 묶기
 
3. 그래프 모델링
3.1 노드 설계
// PHPFile 노드 구조
(:PHPFile {
  path: "src/controllers/UserController.php",
  name: "UserController.php",
  directory: "src/controllers",
  lineCount: 450,
  size: 15234,
  lastModified: datetime("2024-11-01T10:30:00")
})
3.2 관계 설계
// INCLUDES 관계
(file1)-[:INCLUDES {
  lineNumber: 12,
  conditional: false,  // if 블록 내부 include 여부
  count: 1            // 동일 파일 중복 include 횟수
}]->(file2)
3.3 확장된 스키마
실무 프로젝트에서는 더 많은 메타데이터가 필요합니다:
// 1. 패키지/모듈 노드 추가
CREATE (pkg:Package {name: "user-management", version: "2.1"})
CREATE (file:PHPFile {path: "src/User.php"})
CREATE (pkg)-[:CONTAINS]->(file)
// 2. 클래스 노드 추가
CREATE (class:PHPClass {
  name: "UserController",
  namespace: "App\\Controller",
  type: "controller"
})
CREATE (file)-[:DEFINES]->(class)
// 3. 함수/메소드 노드 추가
CREATE (method:PHPMethod {
  name: "getUserById",
  visibility: "public",
  parameters: ["id"],
  returnType: "User"
})
CREATE (class)-[:HAS_METHOD]->(method)
// 4. 복잡한 의존성
CREATE (method)-[:CALLS]->(otherMethod)
CREATE (class)-[:EXTENDS]->(parentClass)
CREATE (class)-[:IMPLEMENTS]->(interface)
4. 실전 분석 쿼리
4.1 기본 탐색 쿼리
특정 파일의 직접 의존성
// index.php가 직접 include하는 파일들
MATCH (source:PHPFile {path: "index.php"})
      -[:INCLUDES]->(target:PHPFile)
RETURN target.path AS included_file
ORDER BY target.path
역방향 의존성 (누가 나를 include하나?)
// config.php를 include하는 모든 파일
MATCH (source:PHPFile)-[:INCLUDES]->(target:PHPFile {path: "config.php"})
RETURN source.path AS dependent_file, count(*) AS include_count
ORDER BY include_count DESC
4.2 고급 분석 쿼리
의존성 체인 분석
// index.php에서 3단계 이내 의존성
MATCH path = (start:PHPFile {path: "index.php"})
             -[:INCLUDES*1..3]->(end:PHPFile)
RETURN DISTINCT end.path, length(path) AS depth
ORDER BY depth, end.path
순환 의존성 탐지
// 순환 참조 찾기
MATCH cycle = (a:PHPFile)-[:INCLUDES*]->(b:PHPFile)
              -[:INCLUDES*]->(a)
WHERE a <> b
RETURN [node IN nodes(cycle) | node.path] AS circular_dependency
LIMIT 10
순환 의존성이 위험한 이유:
- 리팩토링 어려움
 - 메모리 누수 가능성
 - 모듈 분리 불가
 
허브 파일 식별
// 가장 많이 include되는 파일 (높은 중요도)
MATCH (f:PHPFile)<-[:INCLUDES]-(dependent)
RETURN f.path AS hub_file, 
       count(dependent) AS usage_count
ORDER BY usage_count DESC
LIMIT 20
허브 파일 관리 전략:
- 변경 시 광범위한 영향도
 - 철저한 테스트 필요
 - API 안정성 중요
 
고립된 파일 찾기
// 의존성이 전혀 없는 파일 (데드 코드 후보)
MATCH (f:PHPFile)
WHERE NOT (f)-[:INCLUDES]->() 
  AND NOT ()<-[:INCLUDES]-(f)
RETURN f.path AS isolated_file
4.3 영향도 분석
파일 변경 시 영향 범위
// database.php 변경 시 영향받는 모든 파일
MATCH path = (affected:PHPFile)
             -[:INCLUDES*1..]->(target:PHPFile {path: "config/database.php"})
RETURN DISTINCT affected.path AS affected_file,
       length(path) AS dependency_distance
ORDER BY dependency_distance, affected_file
레이어 분석
// 계층별 파일 분포
MATCH (f:PHPFile)
WITH f, 
     CASE 
       WHEN f.path STARTS WITH 'controllers/' THEN 'Presentation'
       WHEN f.path STARTS WITH 'models/' THEN 'Business Logic'
       WHEN f.path STARTS WITH 'config/' THEN 'Configuration'
       ELSE 'Other'
     END AS layer
RETURN layer, count(f) AS file_count
ORDER BY file_count DESC
5. 성능 최적화
5.1 인덱스 전략
// 필수 인덱스
CREATE INDEX FOR (f:PHPFile) ON (f.path);
CREATE INDEX FOR (f:PHPFile) ON (f.name);
// 복합 인덱스 (Neo4j 4.x+)
CREATE INDEX FOR (f:PHPFile) ON (f.directory, f.name);
// 전체 텍스트 검색 인덱스
CREATE FULLTEXT INDEX file_search FOR (f:PHPFile) 
ON EACH [f.path, f.name];
5.2 배치 처리
대량 데이터 import 시 성능 개선:
// 트랜잭션 배치 처리
let mut txn = graph.start_txn().await?;
for (source, target) in dependencies {
    txn.run_queries([
        query("MERGE (s:PHPFile {path: $source})")
            .param("source", source),
        query("MERGE (t:PHPFile {path: $target})")
            .param("target", target),
        query("MERGE (s)-[:INCLUDES]->(t)")
    ]).await?;
}
txn.commit().await?;
성능 비교:
- 개별 쿼리: ~100 deps/sec
 - 배치 처리: ~5,000 deps/sec
 - LOAD CSV: ~50,000 deps/sec
 
5.3 쿼리 최적화
// ❌ 비효율적: 모든 노드 스캔
MATCH (f:PHPFile)
WHERE f.path CONTAINS "controller"
RETURN f
// ✅ 효율적: 인덱스 활용
MATCH (f:PHPFile)
WHERE f.directory = "controllers"
RETURN f
// ✅ 더 효율적: 직접 조회
MATCH (f:PHPFile {directory: "controllers"})
RETURN f
6. 실전 활용 시나리오
6.1 레거시 마이그레이션
상황: PHP 5.6 → PHP 8.2 마이그레이션
// 1. 순환 의존성 제거 우선순위
MATCH cycle = (a)-[:INCLUDES*2..]->(a)
RETURN cycle, length(cycle) AS cycle_length
ORDER BY cycle_length DESC
// 2. 허브 파일 안정화
MATCH (hub:PHPFile)<-[:INCLUDES]-(dep)
WITH hub, count(dep) AS deps
WHERE deps > 10
RETURN hub.path, deps
ORDER BY deps DESC
// 3. 고립 파일 제거
MATCH (isolated:PHPFile)
WHERE NOT (isolated)--()
RETURN isolated.path
6.2 모듈화 리팩토링
목표: 모놀리식 구조를 마이크로서비스로 분리
// 강하게 연결된 파일 그룹 찾기 (응집도 높은 모듈 후보)
CALL gds.louvain.stream({
  nodeProjection: 'PHPFile',
  relationshipProjection: 'INCLUDES'
})
YIELD nodeId, communityId
RETURN gds.util.asNode(nodeId).path AS file,
       communityId AS potential_module
ORDER BY communityId, file
6.3 보안 감사
// 외부 입력을 받는 파일에서 데이터베이스 접근까지 경로
MATCH path = (entry:PHPFile {name: "api.php"})
             -[:INCLUDES*1..5]->(db:PHPFile)
WHERE db.path CONTAINS "database"
RETURN [node IN nodes(path) | node.path] AS data_flow
7. AWS 배포와 보안
7.1 보안 아키텍처
graph TB
    subgraph VPC["VPC (Private Subnet)"]
        Scanner["Scanner<br/>(ECS Task)"]
        Neo4j["Neo4j DB<br/>(EC2)"]
        Secrets["Secrets Manager<br/>- Neo4j Credentials<br/>- TLS Certificates"]
        
        Scanner -->|TLS 7687| Neo4j
        Scanner -->|Get Secrets| Secrets
    end
    
    Scanner -->|Structured Logs| CloudWatch["CloudWatch Logs"]
    
    style VPC fill:#e8eaf6,stroke:#3f51b5,stroke-width:3px
    style Scanner fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style Neo4j fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    style Secrets fill:#e0f2f1,stroke:#004d40,stroke-width:2px
    style CloudWatch fill:#fff9c4,stroke:#f57f17,stroke-width:2px
7.2 환경변수 관리
# .env.production (절대 Git에 커밋하지 말 것!)
NEO4J_URI=neo4j+s://production.neo4j.internal:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=<Secrets Manager에서 가져오기>
PROJECT_DIR=/mnt/source
# AWS Secrets Manager 사용 (권장)
AWS Secrets Manager 통합:
use aws_sdk_secretsmanager as secretsmanager;
async fn get_neo4j_password() -> Result<String, Box<dyn std::error::Error>> {
    let config = aws_config::load_from_env().await;
    let client = secretsmanager::Client::new(&config);
    
    let resp = client
        .get_secret_value()
        .secret_id("prod/neo4j/password")
        .send()
        .await?;
    
    Ok(resp.secret_string().unwrap().to_string())
}
7.3 IAM 권한 최소화
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:prod/neo4j/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/php-scanner:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "efs:ClientMount",
        "efs:ClientRead"
      ],
      "Resource": "arn:aws:elasticfilesystem:*:*:file-system/*",
      "Condition": {
        "StringEquals": {
          "elasticfilesystem:AccessPointArn": "arn:aws:elasticfilesystem:*:*:access-point/*"
        }
      }
    }
  ]
}
7.4 네트워크 보안
Security Group 설정:
# Neo4j Security Group (데이터베이스)
aws ec2 authorize-security-group-ingress \
  --group-id sg-neo4j \
  --protocol tcp \
  --port 7687 \
  --source-group sg-scanner \
  --description "Allow scanner access to Neo4j"
# Scanner Security Group (스캐너)
# 아웃바운드만 허용 (인바운드 불필요)
aws ec2 authorize-security-group-egress \
  --group-id sg-scanner \
  --protocol tcp \
  --port 7687 \
  --destination-group sg-neo4j
7.5 TLS/SSL 암호화
// Neo4j TLS 연결 (프로덕션 필수)
let config = ConfigBuilder::default()
    .uri(&neo4j_uri)
    .user(&neo4j_user)
    .password(&neo4j_password)
    .encryption(Encryption::Required)  // ⚠️ TLS 필수
    .build()?;
let graph = Graph::connect(config).await?;
Neo4j 서버 TLS 설정:
# neo4j.conf
dbms.connector.bolt.tls_level=REQUIRED
dbms.ssl.policy.bolt.enabled=true
dbms.ssl.policy.bolt.base_directory=/var/lib/neo4j/certificates/bolt
dbms.ssl.policy.bolt.private_key=private.key
dbms.ssl.policy.bolt.public_certificate=public.crt
7.6 감사 로깅
use tracing::{info, warn, error, instrument};
#[instrument(skip(graph))]
async fn scan_file(
    path: &Path, 
    graph: &Graph
) -> Result<usize, Box<dyn std::error::Error>> {
    info!(
        file_path = %path.display(),
        "Starting file scan"
    );
    
    // 파일 스캔 로직
    
    info!(
        dependencies_found = count,
        "Scan completed"
    );
    
    Ok(count)
}
// CloudWatch Logs로 전송
let subscriber = tracing_subscriber::fmt()
    .json()
    .with_target(false)
    .with_current_span(false)
    .finish();
7.7 보안 체크리스트
배포 전 필수 확인사항:
- 환경변수에 비밀번호 하드코딩 제거
 - AWS Secrets Manager 사용
 - IAM 역할 최소 권한 원칙 적용
 - VPC Private Subnet 사용
 - Security Group 인바운드 제한
 - Neo4j TLS/SSL 활성화
 - 입력 검증 함수 구현
 - 경로 순회 공격 방어
 - Cypher injection 방지 (파라미터 바인딩)
 - CloudWatch Logs 활성화
 - 에러 메시지에 민감정보 노출 방지
 
8. 확장 가능성
8.1 다른 언어 지원
// 패턴 추가
let patterns = HashMap::from([
    ("php", vec![
        r#"include_once\s*\(\s*([^)]+)\s*\)"#,
        r#"require_once\s*\(\s*([^)]+)\s*\)"#,
    ]),
    ("python", vec![
        r#"import\s+([a-zA-Z_][a-zA-Z0-9_]*)"#,
        r#"from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import"#,
    ]),
]);
8.2 AST 기반 분석
정규표현식의 한계를 극복하기 위해 AST 파서 사용:
use tree_sitter::{Parser, Language};
extern "C" { fn tree_sitter_php() -> Language; }
let mut parser = Parser::new();
parser.set_language(unsafe { tree_sitter_php() })?;
let tree = parser.parse(&source_code, None).unwrap();
// AST 순회하여 정확한 의존성 추출
8.3 실시간 모니터링
// 파일 시스템 워처
use notify::{Watcher, RecursiveMode};
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx)?;
watcher.watch(path, RecursiveMode::Recursive)?;
for event in rx {
    match event {
        Ok(Event { kind: EventKind::Modify(_), paths, .. }) => {
            // 변경된 파일만 재분석
            analyze_file(&paths[0], &graph).await?;
        }
        _ => {}
    }
}
결론
이번 글에서는 Rust와 Neo4j를 활용하여 실전 PHP 의존성 분석 도구를 구축했습니다.
주요 성과
- 고성능 스캐너: Rust 기반으로 대규모 프로젝트도 빠르게 처리
 - 그래프 모델링: Neo4j를 활용한 직관적인 의존성 표현
 - 실전 쿼리: 순환 의존성, 허브 파일, 영향도 분석 등 실무 패턴
 - 확장성: 다양한 언어와 분석 기법으로 확장 가능
 
실무 적용 가치
- 레거시 마이그레이션: PHP 5.6 → 8.x 업그레이드 전략 수립
 - 리팩토링 우선순위: 데이터 기반 의사결정
 - 보안 감사: 의존성 체인을 통한 취약점 추적
 - 기술 부채 가시화: 순환 의존성, 데드 코드 식별
 
다음 글 예고
Part 3에서는 이 도구를 AWS 프로덕션 환경에 배포합니다:
- EC2/ECS/EKS 배포 전략 비교
 - CI/CD 파이프라인 구축 (CodePipeline, GitHub Actions)
 - 자동화된 분석 워크플로우 (EventBridge, Step Functions)
 - 보안 및 네트워크 설정 (VPC, Security Groups, IAM)
 - 비용 최적화 실전 팁
 
참고 자료
질문이나 피드백은 언제든 환영합니다!
