ํ์ด์ฌ ๋ ํฌ์งํ ๋ฆฌ ์๋ ๋ถ์ ์คํฌ๋ฆฝํธ: ์ฝ๋ ํ์ง·์์กด์ฑ·๋ณต์ก๋๋ฅผ ํ ๋ฒ์
etc 2025. 8. 21. 13:23๐ ํ์ด์ฌ ๋ ํฌ์งํ ๋ฆฌ ์๋ ๋ถ์ ์คํฌ๋ฆฝํธ: ์คํ ์์ + ์์ธ ํด์ค + ์ ์ฒด ์ฝ๋
ํค์๋: ํ์ด์ฌ ์ฝ๋ ๋ถ์, ๋ ํฌ์งํ ๋ฆฌ ํฌ์ค์ฒดํฌ, LOC, ์์กด์ฑ ๋ถ์, ๋ณต์ก๋, ์ ์ง๋ณด์์ฑ, pytest, AST, ๊ทธ๋ํ ์๊ฐํ
ํ์ด์ฌ ํ๋ก์ ํธ์ ์ํ๋ฅผ ํ ๋ฒ์ ํ์ ํ๊ณ ์ถ์ ๋๊ฐ ๋ง์ฃ . ์ด ๊ธ์ ๊ทธ๋ฐ ์๊ฐ์ ์ํด ์ค๋นํ **์๋ ๋ถ์ ์คํฌ๋ฆฝํธ(analyze_repo.sh)**์ ์คํ ์์, ์์ธ ๋์ ํด์ค, ๊ทธ๋ฆฌ๊ณ ์ ์ฒด ์ฝ๋๊น์ง ํ ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์คํฌ๋ฆฝํธ๋ pygount/radon/pydeps/pipreqs/pytest/ripgrep ๋ฑ ๋๊ตฌ๋ฅผ ์กฐํฉํด ๋ ํฌ์งํ ๋ฆฌ ํฌ์ค ๋ฆฌํฌํธ๋ฅผ ์์ฑํฉ๋๋ค.

๐ ํ๋์ ๋ณด๋ ๊ธฐ๋ฅ ์์ฝ
- ๋๋ ํฐ๋ฆฌ ํธ๋ฆฌ & ํ์ผ ํฌ๊ธฐ: tree / find, du
- LOC(์ฝ๋ ๋ผ์ธ) ํต๊ณ: pygount (์ธ์ด๋ณ/์ฃผ์/๋น ์ค)
- ์์กด์ฑ ๋ถ์: pipreqs(์ถ์ ), pipdeptree(์ค์ ์ค์น ํธ๋ฆฌ)
- ๋ณต์ก๋/์ ์ง๋ณด์์ฑ: radon cc, radon mi
- ์ํฌํธ ๊ทธ๋ํ: pydeps → Graphviz SVG
- ํ ์คํธ ์์ง: pytest --collect-only
- TODO/FIXME ์ค์บ: ripgrep(๋๋ grep)
- AST ์ธ๋ฑ์ค: ํจ์/ํด๋์ค๋ฅผ JSON์ผ๋ก ์์ธ
- Fallback ์์กด์ฑ ์์ง๊ธฐ: ๋ฌธ๋ฒ ์ค๋ฅ ํ์ผ๋ ๊ด๋ํ๊ฒ import ์ถ์ถ
โ๏ธ ์ค์น (1ํ)
python -m venv .venv && source .venv/bin/activate
pip install --upgrade pip
pip install pipreqs pipdeptree radon pydeps pygount pytest
# ์์กด(๊ถ์ฅ)
# - graphviz: pydeps๊ฐ SVG/DOT ์ถ๋ ฅ์ ํ์
# - tree: ๋๋ ํฐ๋ฆฌ ๋งต ์์ฑ
# - ripgrep: TODO/FIXME ๊ฒ์ ๊ฐ์(์์ผ๋ฉด grep๋ก ๋์ฒด)
# Fedora/RHEL/CentOS:
# sudo dnf install graphviz tree ripgrep -y
# Ubuntu/Debian:
# sudo apt-get update && sudo apt-get install graphviz tree ripgrep -y
โถ๏ธ ์ฌ์ฉ ๋ฐฉ๋ฒ
chmod +x analyze_repo.sh
./analyze_repo.sh
์ ๋ก๋ ๊ถ์ฅ ์ธํธ(์์ฝ ๋ฆฌํฌํธ)
- analysis_out/repo_map.txt, file_sizes.txt, loc_summary.txt
- pipdeptree.txt, complexity.json, maintainability.json
- imports.svg(์์ฑ ์), test_collection.txt, todos.txt, ast_index.json
- requirements_inferred.txt ๋๋ requirements_inferred_fallback.txt
๐งฉ ์คํ ์์ & ๊ฒฐ๊ณผ ํด์ค (์์ )
- ๋ ํฌ ํธ๋ฆฌ/ํ์ผ ํฌ๊ธฐ → ๊ตฌ์กฐ ํ์ & ์ฉ๋ ์์ ํ์ผ ํ์ธ
- LOC ํต๊ณ → ์ฝ๋/์ฃผ์/๋น ์ค ๋ฐธ๋ฐ์ค ์ง๋จ
- ์์กด์ฑ → “์ค์ ์ค์น ํธ๋ฆฌ” vs “์ฝ๋๊ฐ ์๊ตฌํ๋ ํจํค์ง” ๋น๊ต
- ๋ณต์ก๋/์ ์ง๋ณด์์ฑ → ๋ฆฌํฉํฐ๋ง ์ฐ์ ์์ ์ ์ ๊ทผ๊ฑฐ
- ์ํฌํธ ๊ทธ๋ํ → ๊ฒฐํฉ๋ ๋์ ๋ชจ๋ ์๋ณ
- ํ ์คํธ ์์ง → ํ ์คํธ ์กด์ฌ/๋ช ๋ช ๊ท์น ํ์
- TODO/FIXME → ์์ ๋ฐฑ๋ก๊ทธ ์ถ์ถ
- AST ์ธ๋ฑ์ค → ํจ์/ํด๋์ค ํ์ ๋ฐ ๋ฌธ์ํ ์๋ํ ํ ๋
๐ก ์ค์ ํ
- IGNORE_PIPREQS์ ๋ฌธ๋ฒ ์ค๋ฅ/์คํ์ฉ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํ๋ฉด pipreqs ์คํจ๋ฅผ ์ค์ผ ์ ์์ต๋๋ค.
- --max-bacon=2๋ก import ๊ทธ๋ํ ๊น์ด ์ ํ → ๋ณต์กํ ๊ทธ๋ํ ๊ฐ๋ ์ฑ ๊ฐ์ .
- CI ํ์ดํ๋ผ์ธ์ ์ด ์คํฌ๋ฆฝํธ๋ฅผ ์ฐ๊ฒฐํด ์ ๊ธฐ ๋ฆฌํฌํธ๋ฅผ ๋ง๋ค๋ฉด ํ ์์ฐ์ฑ์ด ์ฌ๋ผ๊ฐ๋๋ค.
๐ analyze_repo.sh ์ ์ฒด ์ฝ๋
์๋ ์ฝ๋๋ฅผ ๋ ํฌ์งํ ๋ฆฌ ๋ฃจํธ์ analyze_repo.sh๋ก ์ ์ฅํ์ธ์.
#!/usr/bin/env bash
set -euo pipefail
# === ์ค์ ===
OUT="analysis_out"
IGNORE_PIPREQS="main_bat.py" # pipreqs๊ฐ ๊นจ์ง๋ ํ์ผ/ํด๋๋ฅผ ,๋ก ์ถ๊ฐ(์: "main_bat.py,build,dist")
SKIP_DIRS=".git,.venv,__pycache__,.mypy_cache,.pytest_cache,${OUT}"
PY_EXT=".py"
mkdir -p "$OUT"
# === ์ ํธ ===
have() { command -v "$1" >/dev/null 2>&1; }
note() { echo -e "\n[INFO] $*"; }
warn() { echo -e "\n[WARN] $*" >&2; }
fail() { echo -e "\n[ERROR] $*" >&2; exit 1; }
# === 1) ๋ ํฌ ํธ๋ฆฌ & ํ์ผ ํฌ๊ธฐ ===
note "๋ ํฌ ๋งต/ํ์ผ ํฌ๊ธฐ ์์ง..."
{
echo "### REPO TREE"
if have tree; then
tree -a -I "$(echo "$SKIP_DIRS" | tr , '|')" || true
else
warn "'tree'๊ฐ ์์ด 'find'๋ก ๋์ฒดํฉ๋๋ค(๊ฐ๋ต ์ถ๋ ฅ). 'sudo dnf/apt install tree' ๊ถ์ฅ."
find . -path "./.git" -prune -o -path "./${OUT}" -prune -o -print
fi
} > "$OUT/repo_map.txt"
du -ah --max-depth=2 2>/dev/null | sort -h > "$OUT/file_sizes.txt" || {
warn "du ์ต์
๋ฏธ์ง์ ํ๊ฒฝ. ๋์ฒด ๋ชจ๋๋ก ํ์ผ ํฌ๊ธฐ ์ง๊ณ."
find . -type f -not -path "./${OUT}/*" -not -path "./.git/*" -exec du -ah {} + | sort -h > "$OUT/file_sizes.txt" || true
}
# === 2) LOC/์ธ์ด ํต๊ณ (pygount) ===
note "LOC/์ธ์ด ํต๊ณ(pygount)..."
pygount . \
--format=summary \
--folders-to-skip "$SKIP_DIRS" \
> "$OUT/loc_summary.txt"
# (์ ํ) ํ์ผ๋ณ ์์ธ ํ์ ์ ์ฃผ์ ํด์
# pygount . \
# --format=cloc-xml \
# --folders-to-skip "$SKIP_DIRS" \
# > "$OUT/loc_by_file.cloc.xml"
# === 3) ์์กด์ฑ(์ถ์ + ์ค์ ํธ๋ฆฌ) ===
note "์์กด์ฑ ์ถ์ (pipreqs) ๋ฐ ์ค์น ํธ๋ฆฌ(pipdeptree)..."
# pipreqs๋ ๋ฌธ๋ฒ ์๋ฌ ํ์ผ์์ ์ค๋จ๋ ์ ์์ด --ignore ์ฌ์ฉ, ์คํจํด๋ ๊ณ์
pipreqs . \
--force \
--encoding=utf-8 \
--ignore "$IGNORE_PIPREQS" \
--savepath "$OUT/requirements_inferred.txt" || warn "pipreqs ์คํจ(๋ฌด์ํ๊ณ ๊ณ์). --ignore ๋ชฉ๋ก์ ๋๋ฆฌ๊ฑฐ๋ fallback ์ฌ์ฉ ๊ถ์ฅ."
# requirements.txt๊ฐ ์๋ค๋ฉด ์ค์น ํ ์์กด ํธ๋ฆฌ ์ถ๋ ฅ(์คํจ ํ์ฉ)
if [[ -f requirements.txt ]]; then
pip install -r requirements.txt || warn "requirements ์ค์น ์ค ๊ฒฝ๊ณ ๋ฐ์(๊ณ์ ์งํ)."
fi
pipdeptree > "$OUT/pipdeptree.txt" || warn "pipdeptree ์คํจ(๊ณ์)."
# === 4) ๋ณต์ก๋/์ ์ง๋ณด์์ฑ (radon) ===
note "๋ณต์ก๋/์ ์ง๋ณด์์ฑ(radon)..."
radon cc -s -a -j . > "$OUT/complexity.json" || warn "radon cc ์คํจ(๊ณ์)."
radon mi -s -j . > "$OUT/maintainability.json" || warn "radon mi ์คํจ(๊ณ์)."
# === 5) import ๊ทธ๋ํ (pydeps) ===
note "import ๊ทธ๋ํ(pydeps → SVG)..."
if have dot; then
:
else
warn "graphviz(dot) ๋ฏธ์ค์น. 'sudo dnf/apt install graphviz' ๊ถ์ฅ. ์๋๋ ํด๋ด
๋๋ค."
fi
# ์ต์ pydeps: -T svg -o ํ์ผ
if have pydeps; then
pydeps . --noshow --max-bacon=2 -T svg -o "$OUT/imports.svg" || warn "pydeps ๊ทธ๋ํ ์์ฑ ์คํจ(๊ณ์)."
else
warn "pydeps ๋ฏธ์ค์น. 'pip install pydeps' ํ ์ฌ์๋ ๊ฐ๋ฅ."
fi
# === 6) ํ
์คํธ ์์ง (pytest) ===
note "pytest ํ
์คํธ ์์ง..."
pytest --collect-only -q > "$OUT/test_collection.txt" || warn "pytest ์์ง ์คํจ(๊ณ์)."
# === 7) TODO/FIXME/HACK/XXX ๊ฒ์ ===
note "TODO/FIXME/HACK/XXX ํค์๋ ๊ฒ์..."
if have rg; then
rg -n "TODO|FIXME|HACK|XXX" --glob "!${OUT}/**" --glob "!.git/**" > "$OUT/todos.txt" || true
else
grep -RInE "TODO|FIXME|HACK|XXX" . \
--exclude-dir="$OUT" --exclude-dir=".git" --exclude-dir=".venv" \
> "$OUT/todos.txt" || true
fi
# === 8) AST ์ธ๋ฑ์ค(ํจ์/ํด๋์ค) ===
note "AST ์ธ๋ฑ์ค ์์ฑ(ํจ์/ํด๋์ค)..."
python - << 'PY' || warn "AST ์ธ๋ฑ์ค ์์ฑ ์ค ๋ฌธ์ (๊ณ์)."
import os, ast, json
skip = set(s.strip() for s in ".git,.venv,__pycache__,.mypy_cache,.pytest_cache,analysis_out".split(","))
index=[]
for root,_,files in os.walk("."):
if any(k in root for k in skip):
continue
for f in files:
if f.endswith(".py"):
p=os.path.join(root,f)
try:
with open(p,"r",encoding="utf-8",errors="ignore") as fh:
src = fh.read()
try:
m=ast.parse(src, filename=p)
except SyntaxError as e:
# ๋ฌธ๋ฒ ์๋ฌ๋ ๊ธฐ๋ก๋ง ๋จ๊ธฐ๊ณ ๊ฑด๋๋
index.append({"path":p,"error":f"SyntaxError: {e}"})
continue
funcs=[n.name for n in ast.walk(m) if isinstance(n,ast.FunctionDef)]
clss=[n.name for n in ast.walk(m) if isinstance(n,ast.ClassDef)]
index.append({"path":p,"functions":funcs,"classes":clss})
except Exception as e:
index.append({"path":p,"error":str(e)})
open("analysis_out/ast_index.json","w",encoding="utf-8").write(json.dumps(index,ensure_ascii=False,indent=2))
PY
# === 9) (์ต์
) pipreqs Fallback: ๊ด๋ํ import ์์ง๊ธฐ ===
note "pipreqs Fallback(๊ด๋ํ import ์์ง๊ธฐ) ์คํ..."
python - << 'PY' || warn "Fallback import ์์ง ์คํจ(๊ณ์)."
import os, ast, tokenize, io
root="."
skip=set(s.strip() for s in ".git,.venv,__pycache__,.mypy_cache,.pytest_cache,analysis_out".split(","))
imports=set()
stdlib={"os","sys","json","re","subprocess","pathlib","typing","logging","datetime","time","math","itertools","functools","collections","dataclasses","ast","tokenize","io","enum","statistics","decimal","random","uuid"}
def tolerant_parse(text):
try:
return ast.parse(text)
except SyntaxError:
return None
for r,_,fs in os.walk(root):
if any(k in r for k in skip):
continue
for f in fs:
if f.endswith(".py"):
p=os.path.join(r,f)
try:
txt=open(p,"r",encoding="utf-8",errors="ignore").read()
except Exception:
continue
m=tolerant_parse(txt)
if m:
for n in ast.walk(m):
if isinstance(n, ast.Import):
for a in n.names:
imports.add(a.name.split(".")[0])
elif isinstance(n, ast.ImportFrom) and n.module:
imports.add(n.module.split(".")[0])
else:
# ๊ฐ๋จ ๋ผ์ธ ์ค์บ(๋ฌธ๋ฒ ์๋ฌ ํ์ผ์ฉ)
for line in txt.splitlines():
line=line.strip()
if line.startswith("import "):
for part in line[len("import "):].split(","):
imports.add(part.strip().split(".")[0])
elif line.startswith("from ") and " import " in line:
mod=line.split()[1]
imports.add(mod.split(".")[0])
third=sorted(m for m in imports if m and m not in stdlib)
open("analysis_out/requirements_inferred_fallback.txt","w",encoding="utf-8").write("\n".join(third))
print(f"[fallback] third-party candidates: {len(third)} → analysis_out/requirements_inferred_fallback.txt")
PY
# === ์๋ฃ ์์ฝ ===
note "์๋ฃ! ์ฐ์ถ๋ฌผ ๋ชฉ๋ก:"
ls -lh "$OUT" || true
echo
echo "์
๋ก๋ ๊ถ์ฅ ํ์ผ:"
echo "- $OUT/repo_map.txt"
echo "- $OUT/file_sizes.txt"
echo "- $OUT/loc_summary.txt"
echo "- $OUT/pipdeptree.txt"
echo "- $OUT/complexity.json"
echo "- $OUT/maintainability.json"
echo "- $OUT/imports.svg (์์ผ๋ฉด)"
echo "- $OUT/test_collection.txt"
echo "- $OUT/todos.txt"
echo "- $OUT/ast_index.json"
echo "- $OUT/requirements_inferred.txt (์์ผ๋ฉด)"
echo "- $OUT/requirements_inferred_fallback.txt (๋ณด์กฐ)"
โ ์์ฝ & ๊ฒฐ๋ก
- ์ด ์คํฌ๋ฆฝํธ ํ๋๋ก ํ๋ก์ ํธ ์ํ ์ง๋จ์ ์๋ํํ ์ ์์ต๋๋ค.
- CI์ ๋ถ์ด๋ฉด ์ ๊ธฐ ๊ฑด๊ฐ ๋ฆฌํฌํธ๋ฅผ ์๋ ์์ฑํ ์ ์๊ณ , ์ ๊ท ์ธ์/๋ ๊ฑฐ์ ๊ด๋ฆฌ/๋ฆฌํฉํฐ๋ง ๊ธฐํ์ ํ์ํฉ๋๋ค.
- ํ์์ ํ์ผ๋ณ LOC CSV๋ ํธ์ถ ๊ทธ๋ํ ์์ฑ๊ธฐ ๋ฑ ํ์ฅ๋ ์์ฝ๊ฒ ์ถ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
๋ณธ ๊ธ์ chatgpt๋ฅผ ํ์ฉํ์ฌ ์์ฑ๋์์ต๋๋ค.
'etc' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ssh key๋ก ์๋ฒ ์ ์ (0) | 2020.05.18 |
|---|---|
| bash Prompt ์ค์ (0) | 2020.05.18 |
| ubuntu user ์์ฑ (0) | 2020.04.20 |
| git command (0) | 2020.03.16 |
| ansible๋ก ubuntu user ์์ฑ (0) | 2020.02.19 |


