λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
AI

도ꡬ 섀계 베슀트 ν”„λž™ν‹°μŠ€ — μ—μ΄μ „νŠΈλ₯Ό λ˜‘λ˜‘ν•΄ 보이게 λ§Œλ“œλŠ” 건 λͺ¨λΈμ΄ μ•„λ‹ˆλΌ Tool Spec이닀

by The era of AI 2026. 4. 12.
728x90

μ‹œλ¦¬μ¦ˆ 3편. 1편 ν•˜λ„€μŠ€ μ—”μ§€λ‹ˆμ–΄λ§ μž…λ¬Έ · 2편 평가 ν•˜λ„€μŠ€ ꡬ좕 κ°€μ΄λ“œμ— 이어, 이번 글은 ν•˜λ„€μŠ€μ˜ κ°€μž₯ μ‹€μš©μ μΈ ꡬ성 μš”μ†Œ — 도ꡬ(Tools) λ₯Ό λ‹€λ£Ήλ‹ˆλ‹€.

λ“€μ–΄κ°€λ©°: 같은 λͺ¨λΈ, λ‹€λ₯Έ κ²°κ³Ό

같은 Claude Opus 4.6을 μ“°λŠ” 두 νŒ€μ΄ μžˆμŠ΅λ‹ˆλ‹€. AνŒ€μ˜ μ—μ΄μ „νŠΈλŠ” λ³΅μž‘ν•œ λ¦¬νŒ©ν„°λ§μ„ μ²™μ²™ ν•΄λ‚΄κ³ , BνŒ€μ˜ μ—μ΄μ „νŠΈλŠ” 파일 경둜 ν•˜λ‚˜ λͺ» μ°Ύμ•„ ν—€λ§΅λ‹ˆλ‹€. μ°¨μ΄λŠ” λͺ¨λΈμ΄ μ•„λ‹ˆλΌ 도ꡬ에 μžˆμŠ΅λ‹ˆλ‹€.

μ—μ΄μ „νŠΈ κ°œλ°œμžλ“€μ΄ κ°€μž₯ ν”νžˆ ν•˜λŠ” 착각은 "LLM이 λ˜‘λ˜‘ν•˜λ‹ˆκΉŒ λ„κ΅¬λŠ” λŒ€μΆ© λ§Œλ“€μ–΄λ„ μ•Œμ•„μ„œ μ“Έ 것"μ΄λΌλŠ” μƒκ°μž…λ‹ˆλ‹€. ν˜„μ‹€μ€ μ •λ°˜λŒ€μž…λ‹ˆλ‹€. 쒋은 λ„κ΅¬λŠ” λͺ¨ν˜Έν•œ μΆ”λ‘  문제λ₯Ό 결정적인 ν•¨μˆ˜ 호좜둜 λ°”κΏ‰λ‹ˆλ‹€. λ‚˜μœ λ„κ΅¬λŠ” μ„Έμƒμ—μ„œ κ°€μž₯ λ˜‘λ˜‘ν•œ λͺ¨λΈμ‘°μ°¨ λ°”λ³΄λ‘œ λ§Œλ“­λ‹ˆλ‹€.

도ꡬ μ„€κ³„μ˜ 원칙: μ—μ΄μ „νŠΈμ—κ²Œ "뭘 ν•  수 μžˆλŠ”μ§€"κ°€ μ•„λ‹ˆλΌ "μ–΄λ–»κ²Œ μ„±κ³΅ν•˜λŠ”μ§€"λ₯Ό κ°€λ₯΄μ³λΌ.


쒋은 λ„κ΅¬μ˜ 5κ°€μ§€ 원칙

1️⃣ λͺ…ν™•ν•œ 계약 (Clear Contract)

λ„κ΅¬μ˜ 이름, μ„€λͺ…, νŒŒλΌλ―Έν„° νƒ€μž…, λ°˜ν™˜ 값이 κ·Έ 자체둜 μ‚¬μš© μ„€λͺ…μ„œκ°€ λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. μ—μ΄μ „νŠΈλŠ” μ‚¬λžŒμ²˜λŸΌ μ§κ΄€μœΌλ‘œ μ±„μ›Œ λ„£μ§€ μ•ŠμŠ΅λ‹ˆλ‹€ — λ¬Έμ„œμ— μ—†μœΌλ©΄ λͺ¨λ¦…λ‹ˆλ‹€.

# ❌ λ‚˜μœ 예: λͺ¨ν˜Έν•œ 이름, νƒ€μž… μ—†μŒ, 무엇이 λŒμ•„μ˜€λŠ”μ§€ 뢈λͺ…
def run(cmd):
    """Run a command."""
    return os.system(cmd)

# βœ… 쒋은 예: μ˜λ„κ°€ λ“œλŸ¬λ‚˜λŠ” 이름, λͺ…μ‹œμ  νƒ€μž…, κ΅¬μ‘°ν™”λœ λ°˜ν™˜
def search_files(
    pattern: str,
    directory: str = "/workspace",
    max_results: int = 50
) -> SearchResult:
    """파일 λ‚΄μš©μ—μ„œ μ •κ·œμ‹ νŒ¨ν„΄μ„ κ²€μƒ‰ν•©λ‹ˆλ‹€.
    
    Args:
        pattern: Python re λͺ¨λ“ˆ λ¬Έλ²•μ˜ μ •κ·œμ‹
        directory: 검색 μ‹œμž‘ 디렉터리 (κΈ°λ³Έ: /workspace)
        max_results: λ°˜ν™˜ν•  μ΅œλŒ€ 맀치 수
    
    Returns:
        SearchResult(matches: list[Match], truncated: bool)
    """

2️⃣ μΉœμ ˆν•œ μ—λŸ¬ λ©”μ‹œμ§€ (Informative Errors)

μ—λŸ¬λŠ” λ””λ²„κΉ…μ˜ μ‹œμž‘μ μ΄ μ•„λ‹ˆλΌ μˆ˜μ • κ°€μ΄λ“œμ—¬μ•Ό ν•©λ‹ˆλ‹€. μ—μ΄μ „νŠΈλŠ” μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό 읽고 λ‹€μŒ 행동을 κ²°μ •ν•©λ‹ˆλ‹€.

상황  βŒ λ‚˜μœ μ—λŸ¬ βœ… 쒋은 μ—λŸ¬
파일 μ—†μŒ FileNotFoundError File not found: /x/config.yml. Did you mean /x/config.yaml? (3 similar files in directory)
κΆŒν•œ λΆ€μ‘± PermissionError: [Errno 13] Cannot write to /etc/hosts. This path is outside the sandbox. Allowed paths: /workspace, /tmp
잘λͺ»λœ 인자 TypeError: expected int Parameter 'timeout' must be int (seconds), got str "30s". Try: timeout=30

 

핡심: μ—λŸ¬ λ©”μ‹œμ§€μ—λŠ” 항상 "κ·Έλž˜μ„œ 뭘 ν•˜λ©΄ λ˜λŠ”μ§€"κ°€ λ“€μ–΄ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

3️⃣ μ μ ˆν•œ 좔상화 μˆ˜μ€€ (Right Granularity)

λ„κ΅¬λŠ” λ„ˆλ¬΄ μ €μˆ˜μ€€μ΄λ©΄ μ—μ΄μ „νŠΈκ°€ 맀번 λ³΅μž‘ν•œ μ‹œν€€μŠ€λ₯Ό 쑰립해야 ν•˜κ³ , λ„ˆλ¬΄ κ³ μˆ˜μ€€μ΄λ©΄ μœ μ—°μ„±μ΄ μ‚¬λΌμ§‘λ‹ˆλ‹€.

❌ λ„ˆλ¬΄ μ €μˆ˜μ€€: open_file, read_bytes(1024), decode_utf8, close_file
βœ… μ μ ˆν•œ μˆ˜μ€€: read_file(path) -> str

❌ λ„ˆλ¬΄ κ³ μˆ˜μ€€: fix_all_bugs(repo)
βœ… μ μ ˆν•œ μˆ˜μ€€: run_tests(), edit_file(), search_code()

 

κ²½ν—˜μΉ™: μˆ™λ ¨λœ μ£Όλ‹ˆμ–΄ κ°œλ°œμžκ°€ ν•œ 번의 μ§€μ‹œλ‘œ μˆ˜ν–‰ν•  수 μžˆλŠ” λ‹¨μœ„κ°€ μ μ ˆν•œ 도ꡬ ν¬κΈ°μž…λ‹ˆλ‹€.

4️⃣ λ©±λ“±μ„± (Idempotency)

κ°€λŠ₯ν•œ 경우 같은 μž…λ ₯ → 같은 κ²°κ³Όκ°€ λ˜λ„λ‘ μ„€κ³„ν•˜μ„Έμš”. μ—μ΄μ „νŠΈλŠ” μž¬μ‹œλ„κ°€ μž¦μŠ΅λ‹ˆλ‹€.

# ❌ λΉ„λ©±λ“±: 맀번 μƒˆ 파일 생성
def create_log_file() -> str:
    path = f"/logs/{uuid4()}.log"
    open(path, 'w').close()
    return path

# βœ… λ©±λ“±: 같은 key둜 ν˜ΈμΆœν•˜λ©΄ 같은 κ²°κ³Ό
def get_or_create_log_file(key: str) -> str:
    path = f"/logs/{key}.log"
    if not os.path.exists(path):
        open(path, 'w').close()
    return path

5️⃣ κ΄€μΈ‘ κ°€λŠ₯μ„± (Observability)

도ꡬ ν˜ΈμΆœμ€ νŠΈλ ˆμ΄μŠ€μ— λ‚¨λŠ” μ΄λ²€νŠΈμž…λ‹ˆλ‹€. 디버깅 κ°€λŠ₯ν•œ 둜그λ₯Ό μžμ—°μŠ€λŸ½κ²Œ 남기도둝 μ„€κ³„ν•˜μ„Έμš”.

  • 호좜 μ „ν›„μ˜ μƒνƒœ diffλ₯Ό 기둝
  • μ‹€νŒ¨ μ‹œ μž¬ν˜„ κ°€λŠ₯ν•œ μž…λ ₯ 덀프
  • μ‹€ν–‰ μ‹œκ°„κ³Ό λ¦¬μ†ŒμŠ€ μ‚¬μš©λŸ‰

Tool Spec μž‘μ„± 체크리슀트

도ꡬ ν•˜λ‚˜λ₯Ό λ§Œλ“€ λ•Œλ§ˆλ‹€ λ‹€μŒμ„ μ κ²€ν•˜μ„Έμš”.

β–‘ 이름이 동사-λͺ…사 ν˜•νƒœμ΄κ³  μ˜λ„κ°€ λΆ„λͺ…ν•œκ°€?
β–‘ docstring만 읽고 νŒŒλΌλ―Έν„°λ₯Ό μ±„μšΈ 수 μžˆλŠ”κ°€?
β–‘ λͺ¨λ“  νŒŒλΌλ―Έν„°μ— νƒ€μž… νžŒνŠΈκ°€ μžˆλŠ”κ°€?
β–‘ 선택적 νŒŒλΌλ―Έν„°μ— 합리적 기본값이 μžˆλŠ”κ°€?
β–‘ λ°˜ν™˜ νƒ€μž…μ΄ κ΅¬μ‘°ν™”λ˜μ–΄ μžˆλŠ”κ°€? (dictκ°€ μ•„λ‹ˆλΌ TypedDict/dataclass)
β–‘ λͺ¨λ“  μ—λŸ¬ κ²½λ‘œμ— μˆ˜μ • κ°€μ΄λ“œκ°€ λ“€μ–΄ μžˆλŠ”κ°€?
β–‘ 같은 μž…λ ₯으둜 두 번 ν˜ΈμΆœν•΄λ„ μ•ˆμ „ν•œκ°€?
β–‘ 파괴적 μž‘μ—…(μ‚­μ œ/μ“°κΈ°)은 확인 ν”Œλž˜κ·Έκ°€ ν•„μš”ν•œκ°€?
β–‘ μ‹€μ œ μ—μ΄μ „νŠΈμ—κ²Œ "이 λ„κ΅¬λ‘œ Xλ₯Ό 해봐"라고 μ‹œμΌœλ΄€λŠ”κ°€?

 

λ§ˆμ§€λ§‰ ν•­λͺ©μ΄ κ°€μž₯ μ€‘μš”ν•©λ‹ˆλ‹€. Tool spec은 μ‹€μ œ μ—μ΄μ „νŠΈλ‘œ λ“œλΌμ΄λΈŒ ν…ŒμŠ€νŠΈν•˜κΈ° μ „κΉŒμ§€λŠ” μ™„μ„±λœ 게 μ•„λ‹™λ‹ˆλ‹€.


μ•ˆν‹°νŒ¨ν„΄ 가러리: ν”Όν•΄μ•Ό ν•  도ꡬ 섀계

🚫 μ•ˆν‹°νŒ¨ν„΄ 1: "λ§ˆλ²•μ˜ 도ꡬ"

def smart_refactor(code: str) -> str:
    """Intelligently refactor code."""

"Intelligent"κ°€ λ“€μ–΄κ°€λŠ” μˆœκ°„ μ˜μ‹¬ν•˜μ„Έμš”. 도ꡬ가 ν•΄μ•Ό ν•  일이 뢈λͺ…ν™•ν•  λ•Œ μ“°λŠ” λ‹¨μ–΄μž…λ‹ˆλ‹€. λŒ€μ‹  extract_function, rename_symbol, inline_variable처럼 ꡬ체적인 λ¦¬νŒ©ν„°λ§ μ•‘μ…˜μœΌλ‘œ μͺΌκ°œμ„Έμš”.

🚫 μ•ˆν‹°νŒ¨ν„΄ 2: "λ¬Έμžμ—΄ λΈ”λ‘­ λ°˜ν™˜"

def get_file_info(path: str) -> str:
    return f"File: {path}\nSize: {size}\nModified: {mtime}\n..."

μ—μ΄μ „νŠΈκ°€ λ‹€μ‹œ νŒŒμ‹±ν•΄μ•Ό ν•©λ‹ˆλ‹€. κ΅¬μ‘°ν™”λœ 객체λ₯Ό λ°˜ν™˜ν•˜μ„Έμš”.

🚫 μ•ˆν‹°νŒ¨ν„΄ 3: "μ‘°μš©ν•œ μ‹€νŒ¨"

def delete_file(path: str) -> bool:
    try:
        os.remove(path)
        return True
    except:
        return False

μ™œ μ‹€νŒ¨ν–ˆλŠ”μ§€ λͺ¨λ₯΄λ©΄ μˆ˜μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€. μ˜ˆμ™ΈλŠ” μ‚Όν‚€μ§€ 말고 κ΅¬μ‘°ν™”ν•΄μ„œ λŒλ €μ£Όμ„Έμš”.

🚫 μ•ˆν‹°νŒ¨ν„΄ 4: "30개의 νŒŒλΌλ―Έν„°"

def run_command(cmd, cwd, env, timeout, shell, stdin, stdout, 
                stderr, encoding, errors, universal_newlines, ...):

νŒŒλΌλ―Έν„°κ°€ 10개λ₯Ό λ„˜μœΌλ©΄ 도ꡬλ₯Ό μͺΌκ°œκ±°λ‚˜, μ„€μ • 객체둜 κ°μ‹Έμ„Έμš”. μ—μ΄μ „νŠΈλŠ” λ³΄μ΄λŠ” λͺ¨λ“  νŒŒλΌλ―Έν„°λ₯Ό μ±„μš°λ €λŠ” κ²½ν–₯이 μžˆμ–΄ λΆˆν•„μš”ν•œ 인지 λΆ€ν•˜κ°€ μƒκΉλ‹ˆλ‹€.

🚫 μ•ˆν‹°νŒ¨ν„΄ 5: "λŒμ΄ν‚¬ 수 μ—†λŠ” λΆ€μž‘μš©μ΄ κΈ°λ³Έκ°’"

def deploy(service: str, force: bool = True):  # ← forceκ°€ κΈ°λ³Έ True

파괴적 λ™μž‘μ€ λͺ…μ‹œμ μœΌλ‘œ μš”μ²­ν•  λ•Œλ§Œ μΌμ–΄λ‚˜μ•Ό ν•©λ‹ˆλ‹€. 기본값은 항상 μ•ˆμ „ν•œ μͺ½μœΌλ‘œ.


μ‹€μ „: λ‚˜μœ 도ꡬ → 쒋은 λ„κ΅¬λ‘œ λ¦¬νŒ©ν„°λ§

Before — μ „ν˜•μ μΈ "λŒ€μΆ© λ§Œλ“ " 도ꡬ:

def db_query(q):
    """Run a database query."""
    return db.execute(q).fetchall()

문제점: SQL μΈμ μ…˜ μœ„ν—˜, μ—λŸ¬ 처리 μ—†μŒ, λ°˜ν™˜ 포맷 뢈λͺ…, λŒ€μš©λŸ‰ 결과에 λŒ€ν•œ λ°©μ–΄ μ—†μŒ.

 

After — ν•˜λ„€μŠ€ κ΄€μ μ—μ„œ λ‹€μ‹œ 섀계:

def query_users(
    filter_by: Literal["email", "id", "status"],
    value: str,
    limit: int = 100
) -> QueryResult:
    """users ν…Œμ΄λΈ”μ„ μ•ˆμ „ν•˜κ²Œ μ‘°νšŒν•©λ‹ˆλ‹€.
    
    파괴적 μž‘μ—…(INSERT/UPDATE/DELETE)은 λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€.
    ν•„μš”ν•œ 경우 modify_users 도ꡬλ₯Ό μ‚¬μš©ν•˜μ„Έμš”.
    """
    if limit > 1000:
        return QueryResult(
            error="limit cannot exceed 1000. Use pagination with cursor parameter.",
            suggestion="query_users(filter_by=..., value=..., limit=1000, cursor=...)"
        )
    try:
        rows = db.execute(
            "SELECT id, email, status FROM users WHERE {} = %s LIMIT %s".format(filter_by),
            (value, limit)
        ).fetchall()
        return QueryResult(rows=rows, count=len(rows), truncated=len(rows) == limit)
    except DatabaseError as e:
        return QueryResult(
            error=f"Query failed: {e}",
            suggestion="Check if the users table exists and filter_by column is indexed."
        )

 

λ³€ν™”μ˜ 본질: μ œμ•½(SELECT만 ν—ˆμš©, limit μƒν•œ), λͺ…ν™•ν•œ 계약(κ΅¬μ‘°ν™”λœ λ°˜ν™˜), μΉœμ ˆν•œ μ—λŸ¬(suggestion 포함) — 1편의 ν•˜λ„€μŠ€ 원칙 κ·ΈλŒ€λ‘œμž…λ‹ˆλ‹€.


마치며: Tool Spec은 ν”„λ‘¬ν”„νŠΈμ˜ μ—°μž₯이닀

λ§Žμ€ νŒ€μ΄ ν”„λ‘¬ν”„νŠΈ μ—”μ§€λ‹ˆμ–΄λ§μ— μ‹œκ°„μ˜ 90%λ₯Ό μ“°κ³ , 도ꡬ 섀계에 10%λ₯Ό μ”λ‹ˆλ‹€. μ„±μˆ™ν•œ νŒ€μ€ μ •λ°˜λŒ€μž…λ‹ˆλ‹€.

κ·Έ μ΄μœ λŠ” κ°„λ‹¨ν•©λ‹ˆλ‹€. ν”„λ‘¬ν”„νŠΈλŠ” μ—μ΄μ „νŠΈμ—κ²Œ "무엇을 ν•˜λΌ"κ³  λ§ν•˜μ§€λ§Œ, λ„κ΅¬λŠ” "μ–΄λ–»κ²Œ ν•  수 μžˆλŠ”μ§€"λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€. 쒋은 도ꡬ μœ„μ—μ„œλŠ” 짧은 ν”„λ‘¬ν”„νŠΈλ‘œλ„ μ—μ΄μ „νŠΈκ°€ λ˜‘λ°”λ‘œ 움직이고, λ‚˜μœ 도ꡬ μœ„μ—μ„œλŠ” 아무리 μ •κ΅ν•œ ν”„λ‘¬ν”„νŠΈλ„ μ‹€νŒ¨λ₯Ό 막을 수 μ—†μŠ΅λ‹ˆλ‹€.

"μ—μ΄μ „νŠΈκ°€ 멍청해 보인닀면, λ¨Όμ € μ˜μ‹¬ν•΄μ•Ό ν•  건 λͺ¨λΈμ΄ μ•„λ‹ˆλΌ 당신이 λ§Œλ“  도ꡬ닀."

 

이둜써 ν•˜λ„€μŠ€ μ—”μ§€λ‹ˆμ–΄λ§ 3λΆ€μž‘μ„ λ§ˆμΉ©λ‹ˆλ‹€. 1νŽΈμ—μ„œ 큰 그림을 그리고, 2νŽΈμ—μ„œ μΈ‘μ • 체계λ₯Ό μ„Έμš°κ³ , 3νŽΈμ—μ„œ κ°€μž₯ μ‹€μš©μ μΈ λ ˆλ²„μΈ 도ꡬ 섀계λ₯Ό λ‹€λ€˜μŠ΅λ‹ˆλ‹€. μ„Έ 편 λͺ¨λ‘ ν•œ λ¬Έμž₯으둜 μš”μ•½ν•˜λ©΄ μ΄λ ‡μŠ΅λ‹ˆλ‹€.

"μ—μ΄μ „νŠΈλ₯Ό 더 λ˜‘λ˜‘ν•˜κ²Œ λ§Œλ“€λ € ν•˜μ§€ 말고, κ·Έ 주변을 더 λ‹¨λ‹¨ν•˜κ²Œ λ§Œλ“€μ–΄λΌ."

 

λ‹€μŒ μ‹œλ¦¬μ¦ˆμ—μ„œλŠ” μ‹€μ „ μΌ€μ΄μŠ€ μŠ€ν„°λ”” — Claude Code, Devin, Cursorκ°€ 각자 μ–΄λ–€ ν•˜λ„€μŠ€ νŒ¨ν„΄μ„ μ„ νƒν–ˆλŠ”μ§€ 비ꡐ 뢄석 — 둜 μ°Ύμ•„λ΅™κ² μŠ΅λ‹ˆλ‹€.


#AIμ—μ΄μ „νŠΈ #ToolDesign #LLMOps #AgentInfrastructure #ν•˜λ„€μŠ€μ—”μ§€λ‹ˆμ–΄λ§

728x90