음... 오랫동안 프로젝트를 방치하였는데 사실 방치라기보단 국세청 자료를 가져오기 위한 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로 직접 들어가 보면 아래와 같이 뜬다.

다음 목표는 데이터출처명이 "국세청" 인 경우, 이전 포스트 처럼 크로미움으로 띄우고, 정보를 가져오는 방법을 구현하고자 한다. (과연 개발 시간이 될까...?) 일단 해 보자!
'🗜 MCP' 카테고리의 다른 글
| 대입 면접 상담 MCP 만들기(FastAPI로 DB 서버 만들기) (0) | 2025.11.18 |
|---|---|
| 대입 면접 상담 MCP 만들기 (0) | 2025.11.12 |
| [국가법령 MCP 서버 만들기] - 2. 판례 목록, 판례 본문 조회하기 (8) | 2025.08.29 |
| [국가법령 MCP 서버 만들기] - 1. API 발급, 환경 구성 (0) | 2025.08.28 |
| [gemini cli + 커스텀 mcp 만들기] - 5. 기상청 mcp 서버 배포하기 (6) | 2025.08.10 |