본문 바로가기

development/Database

Neo4j Graph Database로 기술스택 추천 시스템 만들기

"Java 기술 스택을 가진 사람은 후속 학습으로 어떤 기술을 익힐 수 있고, 어떤 직무에 지원할 수 있는가?"와 같은 다단계 관계 질의를 처리하려면 관계형 데이터베이스로는 복잡한 JOIN이 필요합니다.

이런 문제를 해결하기 위해 Graph Database인 Neo4j를 활용해서 기술스택-직무-학습방법 간의 관계를 직관적으로 모델링하고, 사용자의 현재 기술스택을 기반으로 맞춤형 추천을 제공하는 시스템을 구현해보기로 했습니다.

 

By Neo4jInc - 자작, 퍼블릭 도메인, https://commons.wikimedia.org/w/index.php?curid=104622494


기술 스택

  • Graph Database: Neo4j (Docker로 로컬 호스팅)
  • 백엔드 API: FastAPI + Python
  • 그래프 쿼리 언어: Cypher
  • 개발 환경: Python 가상환경, Neo4j Desktop/Browser

주요 기능

  1. 기술스택 기반 다음 학습 기술 추천
  2. 보유 기술을 바탕으로 지원 가능한 직무 매칭
  3. 기술별 효과적인 학습 방법 제안
  4. 그래프 구조 시각화 및 통계

환경 설정 및 초기 구축

1. Neo4j 로컬 설치 및 실행

Docker를 활용해서 Neo4j를 로컬에서 실행했습니다:

# Neo4j 컨테이너 실행
docker run \
    --name neo4j-local \
    -p 7474:7474 -p 7687:7687 \
    -e NEO4J_AUTH=neo4j/password \
    -e NEO4J_PLUGINS='["apoc"]' \
    -d \
    neo4j:latest
  • 7474 포트: Neo4j Browser (웹 인터페이스)
  • 7687 포트: Bolt 프로토콜 (애플리케이션 연결)

username, password 설정 필요

2. Python 환경 구축

# 가상환경 생성
python3 -m venv venv
source venv/bin/activate

# 필수 패키지 설치
pip install neo4j fastapi uvicorn pydantic

3. Neo4j 연결 테스트

from neo4j import GraphDatabase

NEO4J_URI = "bolt://localhost:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "password"

driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))

# 연결 테스트
with driver.session() as session:
    result = session.run("RETURN 1 as test")
    print(result.single()["test"])  # 1 출력되면 연결 성공

그래프 데이터 모델 설계

노드 타입 정의

  1. Technology: 프로그래밍 언어, 프레임워크, 도구 등
  2. Job: 직무 (Backend Developer, Frontend Developer 등)
  3. LearningMethod: 학습 방법 (책, 온라인 강의, 프로젝트 등)

관계 타입 정의

  1. PREREQUISITE_FOR: 전제조건 관계 (Java → Spring Boot)
  2. COMMONLY_USED_WITH: 함께 사용되는 기술 (Spring Boot → AWS)
  3. BEST_LEARNED_BY: 효과적인 학습 방법 (React → 공식문서)
  4. REQUIRES: 직무에서 필요한 기술 (Backend Developer → Java)

Cypher로 데이터 생성

// 기술 노드 생성
CREATE 
  (java:Technology {name: 'Java', category: 'Programming Language', difficulty: 'Medium'}),
  (python:Technology {name: 'Python', category: 'Programming Language', difficulty: 'Easy'}),
  (js:Technology {name: 'JavaScript', category: 'Programming Language', difficulty: 'Easy'}),
  (spring:Technology {name: 'Spring Boot', category: 'Backend Framework', difficulty: 'Medium'}),
  (react:Technology {name: 'React', category: 'Frontend Framework', difficulty: 'Medium'}),
  (aws:Technology {name: 'AWS', category: 'Cloud Platform', difficulty: 'Hard'}),
  (mysql:Technology {name: 'MySQL', category: 'Database', difficulty: 'Easy'})

// 학습 경로 관계 생성
MATCH (java:Technology {name: 'Java'}), (spring:Technology {name: 'Spring Boot'})
CREATE (java)-[:PREREQUISITE_FOR {strength: 'High'}]->(spring)

MATCH (js:Technology {name: 'JavaScript'}), (react:Technology {name: 'React'})
CREATE (js)-[:PREREQUISITE_FOR {strength: 'High'}]->(react)

MATCH (spring:Technology {name: 'Spring Boot'}), (aws:Technology {name: 'AWS'})
CREATE (spring)-[:COMMONLY_USED_WITH {strength: 'High'}]->(aws)

// 직무 노드와 관계 생성
CREATE 
  (backend:Job {
    title: 'Backend Developer',
    company_type: 'Tech Startup',
    required_skills: ['Java', 'Spring Boot', 'MySQL'],
    salary_range: '4000-6000만원'
  }),
  (frontend:Job {
    title: 'Frontend Developer', 
    company_type: 'E-commerce',
    required_skills: ['JavaScript', 'React'],
    salary_range: '3500-5500만원'
  })

FastAPI 서버 구현

데이터 모델 정의

from pydantic import BaseModel
from typing import List, Optional

class UserTechStack(BaseModel):
    user_name: str
    technologies: List[str]
    experience_level: Optional[str] = "Intermediate"

class TechRecommendation(BaseModel):
    technology: str
    category: str
    reason: str
    difficulty: str
    prerequisites: List[str]

class JobRecommendation(BaseModel):
    job_title: str
    company_type: str
    required_skills: List[str]
    match_percentage: int
    missing_skills: List[str]

핵심 추천 로직 구현

@app.post("/recommend/technologies", response_model=List[TechRecommendation])
async def recommend_technologies(user_tech: UserTechStack):
    """사용자의 현재 기술스택 기반으로 다음 학습할 기술 추천"""
    
    with driver.session() as session:
        result = session.run("""
        WITH $tech_list as user_techs
        UNWIND user_techs as tech_name
        MATCH (user_tech:Technology {name: tech_name})
        MATCH (user_tech)-[r:PREREQUISITE_FOR|COMMONLY_USED_WITH]->(recommended:Technology)
        WHERE NOT recommended.name IN user_techs
        RETURN DISTINCT 
            recommended.name as technology,
            recommended.category as category,
            recommended.difficulty as difficulty,
            type(r) as relation_type,
            r.strength as strength,
            collect(DISTINCT user_tech.name) as prerequisites
        ORDER BY 
            CASE WHEN r.strength = 'High' THEN 1 
                 WHEN r.strength = 'Medium' THEN 2 
                 ELSE 3 END,
            recommended.difficulty
        LIMIT 10
        """, tech_list=user_tech.technologies)
        
        recommendations = []
        for record in result:
            reason_map = {
                'PREREQUISITE_FOR': f"'{', '.join(record['prerequisites'])}' 기반으로 다음 단계 학습 가능",
                'COMMONLY_USED_WITH': f"'{', '.join(record['prerequisites'])}'와 함께 자주 사용되는 기술"
            }
            
            recommendations.append(TechRecommendation(
                technology=record['technology'],
                category=record['category'],
                difficulty=record['difficulty'],
                reason=reason_map.get(record['relation_type'], "연관 기술"),
                prerequisites=record['prerequisites']
            ))
    
    return recommendations

직무 매칭 알고리즘

@app.post("/recommend/jobs", response_model=List[JobRecommendation])
async def recommend_jobs(user_tech: UserTechStack):
    """보유 기술 기반 적합한 직무 추천"""
    
    with driver.session() as session:
        result = session.run("""
        MATCH (job:Job)
        WITH job, $tech_list as user_techs
        WITH job, user_techs,
             [skill IN job.required_skills WHERE skill IN user_techs] as matched_skills,
             [skill IN job.required_skills WHERE NOT skill IN user_techs] as missing_skills,
             size(job.required_skills) as total_required
        WITH job, matched_skills, missing_skills, total_required,
             (size(matched_skills) * 100 / total_required) as match_percentage
        WHERE match_percentage >= 30
        RETURN 
            job.title as job_title,
            job.company_type as company_type,
            job.required_skills as required_skills,
            job.salary_range as salary_range,
            match_percentage,
            missing_skills
        ORDER BY match_percentage DESC
        LIMIT 10
        """, tech_list=user_tech.technologies)
        
        job_recommendations = []
        for record in result:
            job_recommendations.append(JobRecommendation(
                job_title=record['job_title'],
                company_type=f"{record['company_type']} ({record.get('salary_range', 'N/A')})",
                required_skills=record['required_skills'],
                match_percentage=int(record['match_percentage']),
                missing_skills=record['missing_skills']
            ))
    
    return job_recommendations

API 명세서

 

1. 서버 상태 확인

GET /

서버 상태 및 사용 가능한 엔드포인트 목록 조회

요청

GET /

응답 예시

{
  "message": "🚀 Neo4j Graph Database 기반 Tech Stack Recommendation API",
  "neo4j_status": "✅ 연결됨",
  "endpoints": {
    "/setup": "그래프 데이터 초기화 (POST)",
    "/technologies": "모든 기술 목록",
    "/recommend/technologies": "기술 추천 (POST)",
    "/recommend/jobs": "직무 추천 (POST)",
    "/graph/stats": "그래프 통계",
    "/docs": "API 문서 (Swagger UI)"
  }
}

2. 그래프 데이터 초기화

POST /setup

Neo4j 그래프 데이터베이스를 샘플 데이터로 초기화

요청

POST /setup

응답 예시

{
  "message": "🎯 Graph Database 초기화 완료!"
}

오류 응답

{
  "error": "데이터 설정 실패: [오류 메시지]"
}

3. 기술 목록 조회

GET /technologies

등록된 모든 기술과 관련 정보 조회

GET /technologies

응답 예시

{
  "technologies": [
    {
      "name": "Java",
      "category": "Programming Language",
      "difficulty": "Medium",
      "next_technologies": ["Spring Boot"]
    },
    {
      "name": "Spring Boot",
      "category": "Backend Framework", 
      "difficulty": "Medium",
      "next_technologies": ["AWS", "Docker"]
    }
  ],
  "total": 14
}

4. 기술 추천

POST /recommend/technologies

사용자의 현재 기술스택을 기반으로 다음 학습할 기술 추천

요청 본문

{
  "user_name": "김개발자",
  "technologies": ["Java", "MySQL"],
  "experience_level": "Intermediate"
}

파라미터

  • user_name (string, 필수): 사용자 이름
  • technologies (array[string], 필수): 현재 보유한 기술 목록
  • experience_level (string, 선택): 경험 수준 (Beginner, Intermediate, Advanced)

응답 예시

[
  {
    "technology": "Spring Boot",
    "category": "Backend Framework",
    "difficulty": "Medium",
    "reason": "'Java' 기반으로 다음 단계 학습 가능",
    "prerequisites": ["Java"]
  },
  {
    "technology": "AWS",
    "category": "Cloud Platform",
    "difficulty": "Hard",
    "reason": "'Spring Boot'와 함께 자주 사용되는 기술",
    "prerequisites": ["Spring Boot"]
  }
]

5. 직무 추천

POST /recommend/jobs

사용자의 기술스택을 바탕으로 적합한 직무 추천

요청 본문

{
  "user_name": "김개발자",
  "technologies": ["Java", "Spring Boot", "MySQL"],
  "experience_level": "Intermediate"
}

파라미터

  • user_name (string, 필수): 사용자 이름
  • technologies (array[string], 필수): 보유한 기술 목록
  • experience_level (string, 선택): 경험 수준

응답 예시

[
  {
    "job_title": "Backend Developer",
    "company_type": "Tech Startup (3000-4000만원)",
    "required_skills": ["Java", "Spring Boot", "MySQL"],
    "match_percentage": 100,
    "missing_skills": []
  },
  {
    "job_title": "Full Stack Developer",
    "company_type": "Fintech (5000-7000만원)",
    "required_skills": ["Python", "Django", "React"],
    "match_percentage": 33,
    "missing_skills": ["Python", "Django", "React"]
  }
]

6. 그래프 통계

GET /graph/stats

그래프 데이터베이스의 노드 및 관계 통계 정보 조회

요청

GET /graph/stats

응답 예시

{
  "nodes": [
    {"type": "Technology", "count": 14},
    {"type": "Job", "count": 4}
  ],
  "relationships": [
    {"type": "PREREQUISITE_FOR", "count": 8},
    {"type": "COMMONLY_USED_WITH", "count": 5}
  ],
  "message": "📊 Graph Database 통계"
}

7. 시스템 테스트

GET /test

샘플 데이터로 추천 시스템 정상 작동 확인

GET /test

응답 예시

{
  "message": "🎯 Neo4j Graph Database 테스트 성공!",
  "sample_input": {
    "user_name": "김개발자",
    "technologies": ["Java", "MySQL"],
    "experience_level": "Intermediate"
  },
  "tech_recommendations": [
    {
      "technology": "Spring Boot",
      "category": "Backend Framework",
      "difficulty": "Medium",
      "reason": "'Java' 기반으로 다음 단계 학습 가능",
      "prerequisites": ["Java"]
    }
  ],
  "job_recommendations": [
    {
      "job_title": "Backend Developer",
      "company_type": "Tech Startup (4000-6000만원)",
      "required_skills": ["Java", "Spring Boot", "MySQL"],
      "match_percentage": 67,
      "missing_skills": ["Spring Boot"]
    }
  ]
}

오류 코드

HTTP 상태 코드

  • 200 OK: 요청 성공
  • 422 Unprocessable Entity: 입력 데이터 유효성 검사 실패
  • 500 Internal Server Error: Neo4j 연결 오류 또는 서버 내부 오류

오류 응답 형식

{
  "detail": "오류 상세 메시지",
  "error": "추가 오류 정보"
}

실제 동작 테스트API 명세서

테스트 시나리오 1: Java 개발자

입력:

{
  "user_name": "김자바",
  "technologies": ["Java", "MySQL"],
  "experience_level": "Intermediate"
}

기술 추천 결과:

  • Spring Boot (Java 기반 다음 단계)
  • AWS (Spring Boot와 함께 사용)

직무 추천 결과:

  • Backend Developer (100% 매칭)

테스트 시나리오 2: JavaScript 개발자

입력:

{
  "user_name": "이자스",
  "technologies": ["JavaScript"],
  "experience_level": "Beginner"
}

기술 추천 결과:

  • React (JavaScript 기반 프론트엔드 프레임워크)
  • TypeScript (JavaScript의 상위집합)

직무 추천 결과:

  • Frontend Developer (50% 매칭, HTML/CSS 부족)

그래프 구조의 장점 실감

1. 직관적인 관계 표현

관계형 데이터베이스에서는 복잡한 JOIN으로 표현해야 할 관계를 그래프에서는 노드와 엣지로 직관적으로 모델링할 수 있었습니다.

2. 효율적인 경로 탐색

"Java를 아는 사람이 Backend Developer가 되기 위한 최단 학습 경로"같은 복잡한 쿼리를 Cypher로 간단하게 표현할 수 있었습니다:

MATCH path = (start:Technology {name: 'Java'})-[*1..3]->(job:Job {title: 'Backend Developer'})
RETURN path
ORDER BY length(path)
LIMIT 1

3. 확장성

새로운 기술이나 직무, 학습방법을 추가할 때 기존 구조를 변경하지 않고도 자연스럽게 확장할 수 있었습니다.


마주친 문제들과 해결책

1. Neo4j Sandbox 연결 문제

문제: 초기에 Neo4j Sandbox 사용 시 Bolt 연결 오류 발생 해결: Docker로 로컬 Neo4j 환경 구축으로 전환

2. 중복 추천 결과

문제: 같은 기술이 여러 경로로 추천되어 중복 발생 해결: Python에서 중복 제거 로직 추가 및 전제조건 병합

# 중복 제거 및 전제조건 병합
unique_recommendations = {}
for rec in recommendations:
    if rec.technology not in unique_recommendations:
        unique_recommendations[rec.technology] = rec
    else:
        existing = unique_recommendations[rec.technology]
        existing.prerequisites.extend(rec.prerequisites)
        existing.prerequisites = list(set(existing.prerequisites))

3. 쿼리 성능 최적화

문제: 복잡한 관계 쿼리에서 성능 저하 해결: 인덱스 생성 및 쿼리 최적화

// 기술 이름에 인덱스 생성
CREATE INDEX tech_name_index FOR (t:Technology) ON (t.name)
CREATE INDEX job_title_index FOR (j:Job) ON (j.title)

프로젝트를 통해 배운 점

1. 그래프 데이터베이스의 적합성

복잡한 관계를 가진 데이터의 경우 그래프 데이터베이스가 관계형 데이터베이스보다 직관적이고 효율적임을 확인했습니다.

2. Cypher 쿼리의 표현력

SQL보다 관계 중심의 쿼리를 자연스럽게 표현할 수 있어 복잡한 비즈니스 로직을 단순하게 구현할 수 있었습니다.

3. 확장 가능한 아키텍처 설계의 중요성

초기 설계 시 확장성을 고려한 덕분에 새로운 노드 타입(학습방법)을 쉽게 추가할 수 있었습니다.


향후 개선 방향

1. 개인화 알고리즘 고도화

  • 사용자별 학습 이력 추적
  • 학습 성향 기반 맞춤 추천
  • 피드백 반영 시스템

2. 실시간 데이터 업데이트

  • 채용 시장 트렌드 반영
  • 기술 인기도 실시간 업데이트
  • 급여 정보 자동 수집

3. 시각화 기능 강화

  • 학습 경로 그래프 시각화
  • 기술 트렌드 대시보드
  • 개인별 성장 경로 추적

마무리

이번 프로젝트를 통해 그래프 데이터베이스의 강력함을 직접 경험할 수 있었습니다. 특히 복잡한 관계 데이터를 다룰 때 관계형 데이터베이스의 한계를 뛰어넘는 직관적이고 효율적인 솔루션을 제공한다는 점이 인상적이었습니다.

단순한 CRUD를 넘어서 관계 중심의 사고로 문제를 접근하는 방법을 배울 수 있었고, 이는 향후 복잡한 도메인의 시스템을 설계할 때 큰 도움이 될 것 같습니다.

다음 포스트에서는 Vector Database를 활용한 의미 기반 법률 문서 검색 시스템에 대해 다뤄보겠습니다.

 


 

프로젝트 저장소: graphdb-career-matcher