본문 바로가기
🗜 MCP

[국가법령 MCP 서버 만들기] - 3. 판례, 헌재결정례, 법령해석례, 행정심판례 조회하기

by Majestyblue 2025. 11. 16.

음... 오랫동안 프로젝트를 방치하였는데 사실 방치라기보단 국세청 자료를 가져오기 위한 HTML 파싱으로 구현하던 중 "이게 정말 맞는걸까? 왜 굳이 브라우저를 띄우고 긁어와야하나? JSON 응답으로 받는게 속도도 훨씬 빠르고 부하가 덜 걸릴텐데? 국세청 자료만 HTML로 받으면 되는거 아닌가? 어떻게 해야하지? 다시 해야하나?" 많은 고민이 있었다.

 

MCP 프로젝트를 여럿 진행하면서 다시 생각을 정리하고 시작해보기로 했다. 

 

일단 기본적으로

 

목록 조회 → 일렬번호 정보 얻기 → 상세 조회 

 

이런식으로 해야 하는데, 기존 코드에서 조금 더 필요한 과정을 구현해야 나중에 @mcp.tool()로 구현하기 편할 것 같다느 생각이 들었다.

 

1. 판례, 헌재결정례, 법령해석례, 행정심판례 목록 조회하기

 

일단, 목록 조회 요청 함수를 비동기(async)로 구현하고, 받는 인자를 더 추가해 보자.

  • query : 검색할 키워드
  • target : 검색대상('prec': 판례, detc': 헌재결정례, 'expc': 법령해석례, 'decc': 행정심판례)
  • search option: 검색범위(1은 제목, 2는 본문)
  • oc_key: API 인증 키
import httpx
import json
import asyncio

async def search_law_list_async(query: str, target: str, search_option: int = 2, oc_key: str = '<your_oc_key>'):
    """
    국가법령정보 API를 사용하여 법령 정보 목록을 비동기적으로 검색합니다.

    Args:
        query (str): 검색할 키워드.
        target (str): 검색 대상 ('prec': 판례, 'detc': 헌재결정례, 'expc': 법령해석례, 'decc': 행정심판례).
        search_option (int, optional): 검색 범위 (1: 제목, 2: 본문). 기본값은 2.
        oc_key (str, optional): API 인증 키. 기본값은 '<your_oc_key>'.

    Returns:
        dict: API로부터 받은 JSON 응답 데이터. 오류 발생 시 None을 반환합니다.
    """
    # API 요청을 보낼 URL
    url = 'http://www.law.go.kr/DRF/lawSearch.do'

    # API 요청에 필요한 파라미터 설정
    params = {
        'OC': oc_key,
        'target': target,
        'type': 'JSON',
        'search': str(search_option),  # API는 search 값을 문자열로 받습니다.
        'query': query
    }

    # httpx.AsyncClient를 사용하여 비동기적으로 HTTP 요청을 보냅니다.
    async with httpx.AsyncClient() as client:
        try:
            # GET 방식으로 API 요청 보내기
            response = await client.get(url, params=params)

            # 응답 상태 코드가 200 (성공)일 경우, JSON 데이터를 반환
            if response.status_code == 200:
                return response.json()
            # 실패했을 경우, 오류 메시지를 출력하고 None을 반환
            else:
                print(f"API 요청에 실패했습니다. 상태 코드: {response.status_code}")
                print(f"응답 내용: {response.text}")
                return None

        except httpx.RequestError as e:
            print(f"HTTP 요청 중 오류가 발생했습니다: {e}")
            return None

# --- 함수 사용 예시 ---
async def main():
    # '손해배상' 키워드로 '판례(prec)'의 '본문(2)'을 검색하는 예시
    print("--- '손해배상' 판례 목록 검색 시작 ---")
    search_results = await search_law_list_async(query="손해배상", target="prec")

    # 결과를 성공적으로 받았을 경우
    if search_results:
        # 받아온 JSON 데이터를 보기 좋게 출력
        print(json.dumps(search_results, indent=4, ensure_ascii=False))

        # 다음 단계를 위해 '일련번호'를 추출하는 방법 (예시)
        # 결과 데이터가 리스트 형태이고, 각 항목이 딕셔너리라고 가정
        if isinstance(search_results.get(list(search_results.keys())[0]), list) and len(search_results.get(list(search_results.keys())[0])) > 0:
            first_item = search_results.get(list(search_results.keys())[0])[0]
            serial_number = first_item.get('판례일련번호')
            if serial_number:
                print(f"\n[다음 단계 준비] 첫 번째 결과의 판례일련번호: {serial_number}")

    print("\n--- 검색 종료 ---")


if __name__ == "__main__":
    asyncio.run(main())

 

<출력결과>

--- '손해배상' 판례 목록 검색 시작 ---
{
    "PrecSearch": {
        "키워드": "손해배상",
        "page": "1",
        "target": "prec",
        "prec": [
            {
                "id": "1",
                "사건번호": "2023다221885",
                "데이터출처명": "대법원",
                "사건종류코드": "400101",
                "사건종류명": "민사",
                "선고": "선고",
                "선고일자": "2025.09.18",
                "판례일련번호": "609489",
                "판결유형": "전원합의체 판결",
                "법원종류코드": "",
                "법원명": "대법원",
                "판례상세링크": "/DRF/lawService.do?OC=inomeant&target=prec&ID=609489&type=HTML&mobileYn=", 
                "사건명": "손해배상(기)[중도상환수수료가 이자제한법 제4조 제1항에 따른 간주이자에 해당하여  
이자제한법상 최고이자율 제한에 관한 규정이 적용되는지 문제된 사건]"
            },
            {
                "id": "2",
                "사건번호": "2021두59908",
                "데이터출처명": "대법원",
                "사건종류코드": "400108",
                "사건종류명": "세무",
                "선고": "선고",
                "선고일자": "2025.09.18",
                "판례일련번호": "609463",
                "판결유형": "전원합의체 판결",
                "법원종류코드": "",
                "법원명": "대법원",
                "판례상세링크": "/DRF/lawService.do?OC=inomeant&target=prec&ID=609463&type=HTML&mobileYn=", 
                "사건명": "경정거부처분취소[국내 미등록 특허권 사용료가 한미조세협약상 국내원천소득에 해당하
는지 문제된 사건]"
            },
            
...
중략
...

            {
                "id": "19",
                "사건번호": "2024가합93129",
                "데이터출처명": "대법원",
                "사건종류코드": "400101",
                "사건종류명": "민사",
                "선고": "선고",
                "선고일자": "2025.06.13",
                "판례일련번호": "607817",
                "판결유형": "판결 : 항소",
                "법원종류코드": "",
                "법원명": "서울중앙지방법원",
                "판례상세링크": "/DRF/lawService.do?OC=inomeant&target=prec&ID=607817&type=HTML&mobileYn=",
                "사건명": "손해배상(기)"
            },
            {
                "id": "20",
                "사건번호": "2021다256696",
                "데이터출처명": "대법원",
                "사건종류코드": "400101",
                "사건종류명": "민사",
                "선고": "선고",
                "선고일자": "2025.06.12",
                "판례일련번호": "606821",
                "판결유형": "판결",
                "법원종류코드": "",
                "법원명": "대법원",
                "판례상세링크": "/DRF/lawService.do?OC=inomeant&target=prec&ID=606821&type=HTML&mobileYn=",
                "사건명": "주주대표소송·주주대표소송[법령 위반 행위로 인한 이득에 대하여 손익상계를 할 수 있는지 문제된 사건]"
            }
        ],
        "totalCnt": "16392",
        "section": "bdyText"
    }
}

 

 

이런식으로 출력하게 할 수 있다.

 

 

 

2. 판례, 헌재결정례, 법령해석례, 행정심판례 상세 조회하기

 

위에서 받은 "일렬번호"를 이용하여 상세 조회를 할 수 있다. 상세 조회기능과 'FastMCP'를 결합하여 "korea_law_search"라는 MCP 서버를 만들어 보자

 

구현을 위한 가이드를 생각해보았고, 구체적인 계획은 아래와 같다.

  • FastMCP 라이브러리 사용
  • 인증을 위한 아이디(API)를 LAW_API_OC를 정의하고, 환경변수(os.getenv)로 처리
  • 판례, 헌재결정례, 법령해석례, 행정심판례의 "목록" 조회를 위한 함수를 'search_law_list'로 정의한다.
    query : 검색할 키워드
    target : 검색대상('prec': 판례, detc': 헌재결정례, 'expc': 법령해석례, 'decc': 행정심판례)
    search option: 검색범위(1은 제목, 2는 본문)
  • 판례, 헌재결정례, 법령해석례, 행정심판례의 "상세" 조회를 위한 함수를 'get_law_content'로 정의한다.
    target : 검색대상('prec': 판례, detc': 헌재결정례, 'expc': 법령해석례, 'decc': 행정심판례)
    serial_number: 조회할 정보의 교유 일렬번호
    case_name: (None, optional): 판례명. 필수는 아니며 기본값은 None

 

 

import os
import httpx
from mcp.server.fastmcp import FastMCP

# --- 1. MCP 서버 초기화 ---
mcp = FastMCP("korea_law_search")

# --- 2. 환경 변수에서 API 인증 키 로드 ---
LAW_API_OC = os.getenv("LAW_API_OC")
if not LAW_API_OC:
    raise ValueError("환경 변수 'LAW_API_OC'가 설정되지 않았습니다. setting.json 파일을 확인해주세요.")

# --- 3. MCP 도구(Tool) 정의 ---
@mcp.tool()
async def search_law_list(query: str, target: str, search_option: int = 2):
    """
    국가법령정보 API를 사용하여 법령 정보 목록을 검색합니다.

    Args:
        query (str): 검색할 키워드 (예: "손해배상", "임대차").
        target (str): 검색 대상. 'prec'(판례), 'detc'(헌재결정례), 'expc'(법령해석례), 'decc'(행정심판례) 중 하나를 입력합니다.
        search_option (int, optional): 검색 범위. 1은 '제목' 검색, 2는 '본문' 검색입니다. 기본값은 2 (본문)입니다.

    Returns:
        dict: API로부터 받은 JSON 형식의 검색 결과. 오류 발생 시, 원인을 포함한 dict를 반환합니다.
    """
    url = 'http://www.law.go.kr/DRF/lawSearch.do'
    params = {
        'OC': LAW_API_OC,
        'target': target,
        'type': 'JSON',
        'search': str(search_option),
        'query': query
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, params=params)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            error_details = {"error": "API 요청 실패", "status_code": e.response.status_code}
            try:
                error_details["details"] = e.response.json()
            except Exception:
                error_details["details"] = e.response.text
            return error_details
        except httpx.RequestError as e:
            return {"error": "HTTP 요청 중 오류 발생", "details": str(e)}
        
# 도구 2: 상세 본문 조회
@mcp.tool()
async def get_law_content(target: str, serial_number: str, case_name: str | None = None):
    """
    [2단계] 목록 검색에서 얻은 '일련번호'를 사용하여 법령 정보의 '상세 본문'을 조회합니다.

    Args:
        target (str): 조회 대상. 'prec'(판례), 'detc'(헌재결정례), 'expc'(법령해석례), 'decc'(행정심판례) 중 하나를 입력합니다.
        serial_number (str): 조회할 정보의 고유 일련번호. (예: '608445')
        case_name (str | None, optional): 판례명(LM 파라미터). 필수는 아니며, 기본값은 None입니다.

    Returns:
        dict: API로부터 받은 상세 본문 JSON 데이터.
    """
    url = 'http://www.law.go.kr/DRF/lawService.do'
    params = {
        'OC': LAW_API_OC,
        'target': target,
        'type': 'JSON',
        'ID': serial_number,  # API 파라미터 'ID'에 serial_number 값을 전달
    }

    # case_name 인자가 제공된 경우에만 params에 추가
    if case_name:
        params['LM'] = case_name

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, params=params)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            error_details = {"error": "API 요청 실패", "status_code": e.response.status_code}
            try:
                error_details["details"] = e.response.json()
            except Exception:
                error_details["details"] = e.response.text
            return error_details
        except httpx.RequestError as e:
            return {"error": "HTTP 요청 중 오류 발생", "details": str(e)}

# --- 4. MCP 서버 실행 코드 (가장 중요!) ---
# 이 스크립트가 메인으로 실행될 때, 서버를 시작하고 요청을 기다리도록 합니다.
if __name__ == "__main__":
    mcp.run()

 

 

3. 구현결과

먼저 "목록" 조회를 해야 한다. 그리고 어떤 정보를 원하는지 확인한 다음, 그 정보를 중심으로 알려달라고 해야 한다.

 

 

일렬번호를 확인했다면, 상세 본문 조회를 할 수 있다.

 

 

(교사인 본인의 경우) 이걸 읽어봐도 뭔 내용인지 잘 모르겠으므로(...) 아래와 같이 해석해달라고 할 수 있다.

 

 

 

근데 여기에 문제점이 하나 있긴 하다. 바로 "국세청" 자료가 조회되지 않는다는 것이다. 정확하게는, JSON 방식으로 파싱을 할 수 없다. 시스템이 달라서 그런가?

 

 

 

예를 들어 아래와 같이 데이터 출처명이 "대법원" 인 경우, 상세 본문 조회가 가능하지만

 

 

 

데이터 출처명이 "국세청"인 경우 본문 상세 조회가 (JSON)으로 불가능하다.

 

 

HTML로 직접 들어가 보면 아래와 같이 뜬다.

 

 

다음 목표는 데이터출처명이 "국세청" 인 경우, 이전 포스트 처럼 크로미움으로 띄우고, 정보를 가져오는 방법을 구현하고자 한다. (과연 개발 시간이 될까...?) 일단 해 보자!