"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. 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 프로토콜 (애플리케이션 연결)

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 출력되면 연결 성공
그래프 데이터 모델 설계
노드 타입 정의
- Technology: 프로그래밍 언어, 프레임워크, 도구 등
- Job: 직무 (Backend Developer, Frontend Developer 등)
- LearningMethod: 학습 방법 (책, 온라인 강의, 프로젝트 등)
관계 타입 정의
- PREREQUISITE_FOR: 전제조건 관계 (Java → Spring Boot)
- COMMONLY_USED_WITH: 함께 사용되는 기술 (Spring Boot → AWS)
- BEST_LEARNED_BY: 효과적인 학습 방법 (React → 공식문서)
- 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
'development > Database' 카테고리의 다른 글
| 벡터 데이터베이스(ChromaDB)로 똑똑한 법률 문서 검색 시스템 만들기 (1) | 2025.09.14 |
|---|