Hamutaro

긴 3일의 연휴가 끝나고 화요일이 찾아왔습니다.

저는 어제 롯데월드를 갔다왔구요..

일부로 오후에 가서 생각보다 많이 돌아다니지는 않았어요.

마지막 퍼레이드까지 야무지게 즐기고 왔답니다. 그럼 시작..!

 

 

아침과제 30분,,, 날아갔어요,,,, 왜지,,,?

다시 ㅜㅜ레츠고

 

1. 왜 이걸 하는지 찾아봅니다

1.1 LLM 을 API 로 부를 때 왜 키를 요구할까요?

> LLM API 키는 사용자를 식별하고, 요청을 승인하며, 사용한 만큼의 비용을 부과하기 위한 고유한 암호화 문자열입니다.

LLM API 키가 필요한 이유는 1) 프로젝트 및 앱 식별 2) 사용량 추적 및 과금 3) 속도 제한 및 남용 방지 4) 보안 및 접근 제어

API 키 유출 사고 주요 원인은 하드코딩(소스 코드 내에 API 키를 직접 적어둔 후 GitHub에 업로드), 클라이언트 번들 노출, 로그 파일 노출 등이 있다.

 

1.2 보안 담당자가 다루는 서버는 어떤 운영체제를 가장 많이 쓸까요? 윈도우 PC 에서 PowerShell 대신 깃배시를 쓰면 무엇이 좋을까요?

> 보안 담장자가 가장 많이 다루는 서버 운영체제는 리눅스(Linux)이다.

윈도우 PC에서 PowerShell 대신 깃배시(Git Bash)를 사용할 때의 장점은

1) 서버 환경과의 일치 : 다루는 대부분의 서버가 리눅스라 Bash 명령어를 그대로 연습하고 활용할 수 있다.

2) Mac/Linux와의 호환성 : 작성한 쉘 스크립스(.sh)를 수정없이 그대로 실행할 수 있다.

3) 오픈소스 도구 친화성 : 많은 오픈소스 들이 Linux/Bash 환경을 기준으로 설명되어 있다.

Bash(Bourne Again Shell)는 Linux 및 macOS의 기본 쉘로 사용되는 가장 표준적이고 널리 쓰이는 명령어 인터페이스(CLI)이자 스크립트 언어이다.

Git Bash는 텍스트(Text) 기반 흐름 제어이고, PowerShell은 객체(Object) 기반 데이터 처리이다.

2. 이 말들이 무슨 뜻인지 찾아봅니다

2.1 LLM

> LLM(대형 언어 모델)은 기본적으로 인간의 언어(텍스트)를 입력받아 다음으로 이어질 가장 자연스러운 언어(텍스트)를 내놓는 프로그램이다.

 

2.2 무료 등급 (free tier)

> 클라우드 서비스 나 SaaS(Software as a Service) 프로그램의 무료 등급(Free Tier)은 대개 기능, 사용량, 사용 기간, 이용 대상에 제한을 둔다.

1) 사용량 및 용량 제한 2) 기능 제한 3) 시간 및 성능 제한 4) 사용자 및 협업 제한

 


 

오늘의 순서

  • 아침 과제 5 — Gemini API 키 발급 · .env · .gitignore
  • LLM을 API로 호출한다 — 주소 · 헤더 · 본문 · 응답에서 답 꺼내기
  • 프롬프트 — 역할 · 지시 · 출력 형식 · 예시
  • JSON으로 받기 — llm_client.py
  • AI 에이전트와 도구 호출 — 도구를 만들고 LLM이 고르게 한다
  • 라우터 — 이름으로 함수를 찾아 실행한다 · tool_router.py
  • 승인 게이트 — 위험한 도구 앞에 사람을 둔다 · agent_result.json
  • 오늘 만든 것 확인 · 남은 도전

LLM을 API로 호출한다

아래 셀을 먼저 실행합니다. requests를 설치합니다.

.env는 security-agent-toolkit 폴더에 있습니다.

「시작하기」의 위치 확인 셀이 지금 폴더에서 위쪽 폴더로 올라가며 .env를 찾아, 그 경로를 변수 ENV_PATH에 담아 두었습니다.

코드에서는 open(ENV_PATH, …)로 읽습니다.

⚠ NameError: name 'ENV_PATH' is not defined가 나오면 위치 확인 셀을 실행하지 않은 것입니다.

「시작하기」로 올라가 그 셀을 실행합니다.

%pip install -q requests

토큰

텍스트를 인식하고 처리하는 가장 기본적인 문자 단위입니다.

인간이 글을 단어, 형태소, 또는 음절 단위로 읽는 것처럼, AI는 텍스트를 토큰이라는 조각으로 잘라서 숫자로 변환한 뒤 학습하고 이해한다.

환각

단순히 '모른다'고 말하는 것이 아니라, 틀린 정보를 마치 완벽한 사실인 것처럼 매우 그럴듯하고 확신에 찬 어조로 출력하는 형태를 띤다.


2 · LLM을 API로 호출한다

왜 필요한가

지금까지 만든 룰은 우리가 정한 조건에 맞는 로그만 찾았습니다.

조건에 없는 로그는 그대로 지나갑니다.

LLM은 로그 한 줄을 읽고 무슨 일인지 문장으로 설명합니다.

사람이 한 줄씩 읽던 일을 대신합니다.

LLM을 쓰는 방법은 새로 배우지 않습니다.

10/2에 배운 requests.post로 요청을 보내고 JSON 응답을 받습니다.


2교시에 사용하는 용어

용어 (읽는 법)뜻2교시의 예

LLM (엘엘엠) 다음에 올 토큰을 확률로 예측하는 언어 모델 "model": "gemini-3.5-flash-lite"
토큰 (token) 모델이 글을 자르는 최소 단위. 사용량과 한도를 토큰으로 센다 "total_tokens": 50
환각 (hallucination · 할루시네이션) 사실이 아닌 것을 그럴듯하게 지어내는 것 LLM SK텔레콤 ↔ 조회 API SamsungSDS Inc.

2.1 LLM의 응답은 딕셔너리로 옵니다

「한 문장으로 답하세요. 피싱이란 무엇입니까?」라고 물었을 때 실제로 받은 응답입니다.

오늘 쓰지 않는 키는 줄였습니다.

data = {
    "choices": [
        {
            "message": {
                "role": "assistant",
                "content": "피싱이란 신뢰할 수 있는 기관이나 사람으로 위장하여 타인의 개인정보나 금융정보를 속여 빼내는 전자 금융 사기 수법입니다."
            }
        }
    ],
    "model": "gemini-3.5-flash-lite",
    "usage": {
        "prompt_tokens": 15,
        "completion_tokens": 35,
        "total_tokens": 50
    }
}
print(data["choices"][0]["message"]["content"])

답 문장은 choices → 0 → message → content 순서로 들어가야 나옵니다.

choices는 리스트라서 숫자 0으로 꺼냅니다.

message와 content는 딕셔너리의 키라서 이름으로 꺼냅니다.

usage에는 토큰 수가 있습니다.

질문이 15토큰, 답이 35토큰, 합계가 50토큰입니다.


2.2 호출에는 주소 · 헤더 · 본문이 필요합니다

9/30과 10/2에 쓴 requests 그대로입니다.

넣는 값만 다릅니다.

import requests

api_key = "발급받은_키"      # 실제로는 .env 에서 읽습니다

url = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"

headers = {
    "Authorization": "Bearer " + api_key,
    "Content-Type": "application/json"
}

body = {
    "model": "gemini-3.5-flash-lite",
    "messages": [
        {
            "role": "user",
            "content": "한 문장으로 답하세요. 피싱이란 무엇입니까?"
        }
    ]
}

response = requests.post(
    url,
    headers=headers,
    json=body,
    timeout=30
)

print(response.status_code)          # 200

주소 — LLM 서비스가 요청을 받는 곳입니다.

헤더 — Authorization에 Bearer와 공백 한 칸, 그 뒤에 키를 붙입니다. 9/29 오후에 배운 인증 방식입니다.

본문 — model에는 모델 이름을, messages에는 질문을 넣습니다. 질문은 role이 "user"인 딕셔너리 하나입니다.

무료 등급은 1분에 15번까지 호출할 수 있습니다.

넘으면 상태코드 429가 오고, 1분쯤 기다리면 다시 됩니다.


🐍 문법 상자 · requests.post에 넣는 네 가지

response = requests.post(
    url,
    headers=headers,
    json=body,
    timeout=30
)

print(response.status_code)          # 200 — 성공

쓰는 것뜻

url 보낼 주소 — 맨 앞에 이름 없이 쓴다
headers=headers 요청 헤더 — 키를 여기에 넣는다
json=body 본문 딕셔너리를 JSON으로 바꿔 보낸다 (9/30)
timeout=30 30초 안에 답이 없으면 기다리지 않는다

⚠ requests.post(url, body)처럼 이름 없이 쓰면 body가 json=이 아닌 다른 자리로 들어가 요청이 실패합니다.

json=을 꼭 붙입니다.


🐍 문법 상자 · 안으로 한 단계씩 꺼내기

data = {
    "choices": [
        {
            "message": {
                "content": "포트 스캔입니다."
            }
        }
    ]
}

print(data["choices"][0]["message"]["content"])
# 포트 스캔입니다.

쓰는 것꺼낸 것모양

data["choices"] 답 목록 리스트
data["choices"][0] 첫 번째 답 딕셔너리
…[0]["message"] 메시지 딕셔너리
…["message"]["content"] 답 문장 문자열

⚠ [0]을 빠뜨리면 리스트에 글자 키를 쓴 것이라 TypeError: list indices must be integers가 납니다.


2.3 LLM은 조회하지 않고 문장을 만듭니다

LLM은 어딘가를 찾아보고 답하지 않습니다.

그럴듯한 단어를 이어 붙여 문장을 만듭니다.

그래서 모르는 것도 아는 것처럼 답합니다.

이것을 환각이라고 합니다.

같은 질문 「IP 211.45.12.9는 어느 통신사의 주소입니까?」에 실제로 받은 답입니다.

누구에게 물었나받은 답

LLM · 첫 번째 LG Uplus
LLM · 두 번째 SK텔레콤
조회 API ipwho.is (9/30에 쓴 서비스) SamsungSDS Inc.

LLM의 답은 물을 때마다 달라졌고, 둘 다 틀렸습니다.

문장은 자연스러워서 틀린 줄 알기 어렵습니다.

조회 API는 실제 등록 정보를 찾아서 반환합니다.

그래서 사실 확인은 조회 API와 로그로 하고, LLM에게는 요약과 설명을 맡깁니다.

최종 판단은 사람이 합니다.


2.4 되풀이되는 코드를 함수로 묶습니다 — post_to_llm

문제 2-2와 2-3의 셀에는 미리 채워 둔 줄이 열 줄쯤 있었습니다.

키를 읽고, 주소와 헤더를 만들고, 요청을 보내는 줄입니다.

LLM을 호출할 때마다 똑같이 되풀이됩니다.

되풀이되는 코드는 함수 하나로 묶습니다.

9/23 오후에 배운 방법입니다.

아래 셀의 함수 post_to_llm이 그 일을 합니다.

함수 안의 줄하는 일앞에서 직접 쓴 곳

read_api_key() .env 파일(ENV_PATH)에서 API 키를 읽는다 문제 2-2의 미리 채워 둔 여섯 줄
headers = {"Authorization": "Bearer " + api_key, …} 요청 헤더에 키를 넣는다 문제 2-2의 미리 채워 둔 줄
requests.post(URL, headers=headers, json=body, timeout=30) 요청을 보낸다 문제 2-2
return 받은 response를 돌려준다 —

쓰는 법은 한 줄입니다.

본문 body만 만들어서 넣으면 됩니다.

body = {
    "model": "gemini-3.5-flash-lite",
    "messages": [
        {
            "role": "user",
            "content": "피싱이란 무엇입니까?"
        }
    ]
}

response = post_to_llm(body)

print(response.status_code)          # 200

함수가 대신 해 주는 것은 키 읽기 · 헤더 만들기 · 요청 보내기입니다.

본문 body는 우리가 만듭니다.

무엇을 물을지는 본문에 들어 있기 때문입니다.

돌려받은 response는 문제 2-2의 response와 같은 것입니다.

.status_code와 .json()을 그대로 씁니다.

이 아래 문제부터는 셀에 이 함수만 나옵니다.

셀이 짧아지고, 그 문제에서 새로 배우는 줄만 남습니다.

⚠ 아래 셀을 실행하지 않으면 뒤의 문제에서 NameError: name 'post_to_llm' is not defined가 나옵니다.

노트북을 다시 열었을 때도 아래 셀을 다시 실행합니다.

아래 셀을 실행합니다.

함수를 만들어 두기만 합니다.

LLM을 호출하지 않습니다.

import requests                                               # 인터넷으로 요청을 보내는 도구 (9/30)

# Gemini 에 요청을 보낼 주소
URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"


def read_api_key():                                           # .env 에서 키를 읽어 돌려주는 함수
    """.env 파일(ENV_PATH)에서 GEMINI_API_KEY 의 값을 읽어 반환합니다."""
    api_key = None                                            # 키를 못 찾으면 None 으로 남는다

    with open(ENV_PATH, encoding="utf-8") as f:               # .env 파일을 읽기로 연다
        for line in f:                                        # 파일을 한 줄씩 꺼낸다
            parts = line.strip().split("=", 1)                # 줄 끝 공백을 지우고 = 에서 둘로 나눈다

            if parts[0] == "GEMINI_API_KEY":                  # = 앞부분이 키 이름이면
                api_key = parts[1]                            # = 뒷부분이 키 값이다

    return api_key                                            # 읽은 키를 돌려준다


def post_to_llm(body, api_key=None):                          # 본문을 받아 Gemini 로 보내는 함수. 키를 주지 않으면 .env 의 키를 쓴다
    """body 를 Gemini API 로 보내고 response 를 반환합니다."""

    if api_key is None:                                       # 키를 따로 주지 않았으면
        api_key = read_api_key()                              # .env 의 키를 쓴다

    headers = {
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.post(
        URL,
        headers=headers,
        json=body,
        timeout=60
    )

    if response.status_code == 429:                           # 한도(1분에 15번)를 넘었으면
        print("[한도 초과] 429 — 1분 기다린 뒤 이 셀을 다시 실행합니다")

    return response                                           # 응답을 그대로 돌려준다


print("post_to_llm 함수를 만들었습니다.")

한눈에

하려는 일쓰는 코드

LLM에 요청을 보낸다 requests.post(url, headers=headers, json=body, timeout=30)
키를 싣는다 {"Authorization": "Bearer " + api_key}
위 두 줄을 함수로 줄여 쓴다 response = post_to_llm(body)
질문을 담는다 "messages": [{"role": "user", "content": question}]
답 문장을 꺼낸다 data["choices"][0]["message"]["content"]
쓴 토큰을 본다 data["usage"]["total_tokens"]
성공했는지 확인한다 response.status_code == 200

LLM의 답은 문장이 자연스러워도 틀릴 수 있습니다.

사실 확인은 조회 API와 로그로 합니다.


프롬프트로 답의 모양을 정한다

프롬프트

LLM(대형 언어 모델)에게 좋은 답변을 받으려면 프롬프트를 구체적이고 구조적으로 작성해야 합니다.

프롬프트 엔지니어링의 핵심은 AI에게 명확한 '역할'과 '맥락'을 부여하는 것입니다.

temperature

생성형 AI 모델(대형 언어 모델 등)에서 temperature(온도)는 출력 텍스트의 무작위성과 창의성의 수준을 결정하는 하이퍼파라미터이다.


3 · 프롬프트로 답의 모양을 정한다

왜 필요한가

로그 한 줄만 보내면 LLM은 영어로, 항목을 여러 개 나열해 길게 답했습니다.

실제로 229토큰이 나왔습니다.

답의 길이와 모양이 매번 다르면, 뒤에 오는 파이썬 코드가 값을 꺼낼 수 없습니다.

LLM에게 보내는 글 전체를 프롬프트라고 합니다.

프롬프트에 역할 · 지시 · 출력 형식 · 예시를 적으면 답의 모양이 정해집니다.


3교시에 사용하는 용어

용어 (읽는 법)뜻3교시의 예

프롬프트 (prompt) 모델에게 건네는 지시문 전체 messages에 담는 글
system · user (시스템 · 유저) 역할 지시와 이번 질문을 나눠 담는 자리 {"role": "system", ...} · {"role": "user", ...}
few-shot (퓨샷) 본보기를 예시로 주는 방식 예시: {"severity": "high", "summary": "root 로그인 실패"}
temperature (템퍼러처) 답을 얼마나 다양하게 낼지 정하는 값 "temperature": 0

3.1 역할은 system에, 질문은 user에 적습니다

messages 리스트에 딕셔너리를 둘 넣습니다.

앞의 것이 역할, 뒤의 것이 이번 질문입니다.

messages = [
    {
        "role": "system",
        "content": "당신은 보안관제 애널리스트입니다. 한 문장으로만 답합니다."
    },
    {
        "role": "user",
        "content": "2026-10-06 03:22:40 WARN failed login for ops.kim from 203.0.113.61"
    }
]

body = {
    "model": "gemini-3.5-flash-lite",
    "messages": messages
}

response = post_to_llm(body)

같은 로그를 보냈을 때 실제로 받은 답입니다.

보낸 것받은 답토큰

로그만 영어로 항목 여러 개를 나열한 긴 설명 271
역할 + 로그 「203.0.113.61 IP에서 ops.kim 계정으로의 로그인 실패(WARN)를 나타내며 … 모니터링이 필요합니다.」 132

역할을 정해 주면 쓰는 단어와 판단 기준이 보안 직무 쪽으로 좁혀집니다.

「한 문장으로만」 같은 규칙도 system에 적습니다.

답이 짧아지면 토큰도 줄어듭니다.


3.2 지시와 출력 형식을 분명하게 적습니다

무엇을 할지(지시)와 어떤 모양으로 답할지(출력 형식)를 질문에 적습니다.

같은 로그 ops.kim에 실제로 받은 답입니다.

질문에 적은 것받은 답

「심각도를 high, medium, low 중 하나로만 답하세요.」 low
「JSON으로만 답하세요. 키는 severity와 summary 둘입니다.」 아래
{
  "severity": "medium",
  "summary": "IP 주소 203.0.113.61에서 계정 ops.kim으로 로그인 실패 발생"
}
  1. 고를 수 있는 값을 나열해 주면 그 안에서만 답합니다.
  2. JSON으로 받으면 뒤에 오는 코드가 severity 같은 키 이름으로 값을 꺼낼 수 있습니다.
  3. 받은 글의 앞뒤에 json``과 가 붙어 있습니다. 4교시에 이 표시를 지우고 읽습니다.
  4. 같은 로그인데 한 단어로 물으면 low, JSON으로 물으면 medium이 나왔습니다. LLM의 판단은 묻는 방식에 따라 달라집니다.

3.3 예시를 보여 주면 그 모양을 따라 합니다

본보기를 한두 개 보여 주는 방식을 few-shot이라고 합니다.

질문에 예시 한 줄을 넣었을 때 실제로 달라진 것입니다.

질문받은 summary

예시 없음 IP 주소 203.0.113.61에서 사용자 계정 ops.kim으로의 로그인 시도가 실패했습니다.
예시: {"severity": "high", "summary": "root 로그인 실패"}를 넣음 ops.kim 계정 로그인 실패

예시가 짧으면 답도 짧아집니다.

길이를 글로 설명하는 것보다 예시 하나가 더 잘 통합니다.

예시의 키 이름과 순서, 쓰는 언어도 그대로 따라 합니다.

예시가 없으면 요약이 영어로 올 때가 있습니다.

예시를 넣어도 `````json`` 표시는 붙을 때도 있고 안 붙을 때도 있었습니다.

세 번 가운데 두 번 붙었습니다.


🐍 문법 상자 · 문자열을 이어 붙일 때는 공백도 직접 넣습니다

a = "JSON 으로만 답하세요."
b = "예시: high"

print(a + b)
print(a + " " + b)

# JSON 으로만 답하세요.예시: high
# JSON 으로만 답하세요. 예시: high

쓰는 것결과

a + b 두 문장이 붙어 버린다
a + " " + b 사이에 공백 한 칸

⚠ +는 공백을 넣어 주지 않습니다.

붙어 버린 질문은 LLM도 읽기 어렵습니다.


3.4 temperature를 0으로 두어도 답은 달라질 수 있습니다

본문에 "temperature"를 넣으면 답을 얼마나 다양하게 낼지를 정할 수 있습니다.

보안 자동화에서는 낮게 0으로 둡니다.

body = {
    "model": "gemini-3.5-flash-lite",
    "temperature": 0,
    "messages": messages
}

그런데 0으로 두어도 답이 똑같지는 않았습니다.

「로그인 실패가 다섯 번 이어진 사용자에게 보낼 경고 문구를 한 줄로 쓰세요.」를 temperature 0으로 세 번 물었을 때 실제로 받은 답입니다.

몇 번째받은 답

1 비밀번호 오류 5회 초과로 계정이 임시 잠겼습니다. 본인 확인 후 잠금을 해제해 주세요.
2 로그인 5회 실패로 보안을 위해 계정이 잠겼으니 비밀번호를 재설정해 주세요.
3 비밀번호를 5회 연속 잘못 입력하여 계정이 일시 잠금되었습니다.

같은 temperature 0으로 「심각도를 high, medium, low 중 하나로만 답하세요.」라고 세 번씩 물었을 때입니다.

로그받은 답 세 개

ops.kim 로그인 실패 medium · medium · medium
root 로그인 실패 high · medium · high

temperature를 0으로 두어도 답은 매번 달라질 수 있습니다.

문장도, 심각도 판단도 그렇습니다.

그래도 출력 형식을 좁히면 답이 세 단어 가운데 하나로 모입니다.

문장은 비교할 수 없지만 단어는 코드로 세고 비교할 수 있습니다.

같은 로그에 high와 medium이 섞여 나왔습니다.

그래서 LLM의 판단을 그대로 믿지 않고 사람이 최종 확인합니다.

temperature를 0으로 해도 환각은 사라지지 않습니다.

2교시의 통신사 질문은 0으로 물어도 KT, LG Uplus로 틀린 답이 나왔습니다.


🐍 문법 상자 · temperature는 본문 딕셔너리의 키입니다

body = {
    "model": "gemini-3.5-flash-lite",
    "temperature": 0,
    "messages": []
}

print(list(body.keys()))

# ['model', 'temperature', 'messages']

키넣는 것

model 모델 이름
temperature 0 — 답을 매번 비슷하게
messages 역할 · 질문 리스트

⚠ temperature를 messages 안의 딕셔너리에 넣지 않습니다.

model · messages와 같은 자리입니다.


한눈에

프롬프트의 조각어디에 적나무엇이 달라지나

역할 messages의 system 쓰는 단어와 판단 기준
지시 user의 content 무엇을 할지
출력 형식 user의 content 답의 모양 (한 단어 · JSON)
예시 user의 content 답의 길이와 키 이름
temperature 본문 body 답의 다양함. 자동화에서는 0으로 둔다

temperature를 0으로 두어도 답은 달라질 수 있고 환각도 남습니다.

출력 형식을 좁히면 답이 달라져도 코드가 다룰 수 있는 값이 됩니다.


JSON 답을 안전하게 읽는다 — llm_client.py

파싱

컴퓨터가 이해할 수 없는 단순한 통글자(문자열)를 규칙에 따라 쪼개고 분석해서, 프로그램이 즉시 계산하거나 사용할 수 있는 의미 있는 데이터 형식으로 바꾸는 과정.

구조화된 출력

프로그램이 바로 읽고 처리할 수 있어서 데이터 연동과 자동화가 훨씬 안정적이다.


4 · JSON 답을 안전하게 읽는다

왜 필요한가

3교시에 JSON으로 답하라고 했더니, 답의 앞뒤에 json``과 가 붙어서 왔습니다.

세 번 물으면 세 번 다 붙었습니다.

그 문자열을 json.loads에 그대로 넣으면 JSONDecodeError로 프로그램이 멈춥니다.

로그 백 건을 처리하다가 한 건에서 멈추면 남은 아흔아홉 건을 잃습니다.

읽지 못한 한 건은 건너뛰고 계속 가야 합니다.


4교시에 사용하는 용어

용어 (읽는 법)뜻4교시의 예

파싱 (parsing) 문자열을 프로그램이 쓰는 값으로 바꾸는 일 json.loads(clean)
코드 블록 표시 답의 앞뒤에 붙는 json``과 text.replace("```json", "")
call_llm 질문을 받아 LLM의 응답 문자열을 반환하는 함수 call_llm(question)
parse_llm_json 응답 문자열을 딕셔너리로 바꾸는 함수. 못 읽으면 None parse_llm_json(text)

4.1 표시를 지우고 나서 읽습니다

3교시에 실제로 받은 답은 이런 문자열이었습니다.

{
  "severity": "medium",
  "summary": "IP 주소 203.0.113.61에서 계정 ops.kim으로 로그인 실패 발생"
}

이 문자열을 그대로 json.loads에 넣으면 실제로 이런 오류가 납니다.

JSONDecodeError: Expecting value: line 1 column 1 (char 0)

첫 글자가 {가 아니라 `이기 때문입니다.

그래서 표시를 먼저 지웁니다.

import json

text = '```json{"severity": "medium", "summary": "ops.kim 로그인 실패"}```'

clean = text.replace("```json", "")
clean = clean.replace("```", "")
clean = clean.strip()

result = json.loads(clean)

print(result["summary"])          # ops.kim 로그인 실패

replace는 9/23에, strip은 9/28에, json.loads는 9/29 오후에 배웠습니다.

새 문법은 없습니다.

json``을 먼저 지우고 을 나중에 지웁니다.

순서를 바꾸면 json 문자열이 남습니다.

strip은 앞뒤에 남은 공백과 줄바꿈을 지웁니다.


4.2 읽지 못해도 멈추지 않게 합니다

LLM이 JSON이 아닌 문장으로 답할 때가 있습니다.

그때 json.loads는 JSONDecodeError를 냅니다.

9/28에 배운 try · except로 감쌉니다.

import json

text = "이 로그는 분석할 수 없습니다."

try:
    result = json.loads(text)

except json.JSONDecodeError:
    result = None

print(result)          # None

읽는 데 성공하면 result에 딕셔너리가 담깁니다.

실패하면 result에 None이 담기고, 프로그램은 다음 줄로 넘어갑니다.

부르는 쪽은 if result:로 확인합니다.

9/30의 call_with_retry와 같은 약속입니다.


🐍 문법 상자 · try · except — 읽지 못해도 멈추지 않게

import json

for s in ['{"a": 1}', "그럴듯한 문장"]:

    try:
        print(json.loads(s))

    except json.JSONDecodeError:
        print("읽지 못함")

# {'a': 1}
# 읽지 못함

들어온 글실행되는 곳

JSON 모양 try 안 — 딕셔너리가 된다
그냥 문장 에러가 나서 except 안으로 (9/28)

⚠ except가 없으면 LLM이 문장으로 답한 순간 프로그램이 멈춥니다.


4.3 호출도 함수로 묶습니다 — call_llm

post_to_llm은 요청을 보내는 데까지만 합니다.

본문을 만들고, 상태코드를 확인하고, 답 문장을 꺼내는 줄은 3교시 내내 되풀이했습니다.

이 줄들도 함수 하나에 넣습니다.

질문을 주면 응답 문자열을 반환합니다.

def call_llm(question):

    body = {
        "model": "gemini-3.5-flash-lite",
        "temperature": 0,
        "messages": [
            {
                "role": "user",
                "content": question
            }
        ]
    }

    response = post_to_llm(body)

    if response.status_code != 200:
        print(
            "[LLM 호출 실패] 상태 코드",
            response.status_code
        )
        return ""

    data = response.json()

    return data["choices"][0]["message"]["content"]

실패하면 빈 문자열 ""를 반환합니다.

빈 문자열은 parse_llm_json이 읽지 못해서 None이 됩니다.

두 함수가 그렇게 이어집니다.

temperature는 0으로 고정합니다.

함수로 묶으면 부르는 쪽은 한 줄이면 됩니다.

call_llm("…")

🐍 문법 상자 · %%writefile은 셀 맨 첫 줄에

%%writefile llm_client.py
import json
...

할 일결과

셀 첫 줄에 %%writefile 파일이름 셀 내용이 그 파일로 저장된다
파일을 고친 뒤 이 셀을 다시 실행해야 파일이 바뀐다

⚠ %%writefile 위에 빈 줄이나 주석이 있으면 저장되지 않고 에러가 납니다.


4.4 만든 파일을 불러 씁니다

llm_client.py는 같은 폴더에 있으므로 import로 불러 씁니다.

9/23 오후에 배운 모듈 불러오기입니다.

from llm_client import call_llm, parse_llm_json

text = call_llm(
    "JSON 으로만 답하세요. 키는 severity 하나입니다. 로그: …"
)

result = parse_llm_json(text)

if result:
    print(result["severity"])

else:
    print("읽지 못했습니다")

call_llm이 응답 문자열을 주고, parse_llm_json이 그 문자열을 딕셔너리로 바꿉니다.

result가 None일 수 있으므로 꺼내기 전에 if result:로 확인합니다.

llm_client.py를 고친 뒤에는 문제 4-4 셀을 다시 실행해 저장하고, 이 문제 셀을 다시 실행합니다.


🐍 문법 상자 · if result: — 값이 있을 때만

for result in [{"severity": "high"}, None]:

    if result:
        print("있음", result["severity"])

    else:
        print("없음")

# 있음 high
# 없음

resultif result:

딕셔너리 참 — 키로 꺼낸다
None 거짓 — else로 간다

⚠ 확인 없이 None["severity"]를 쓰면 TypeError: 'NoneType' object is not subscriptable입니다.


한눈에

하려는 일쓰는 코드

코드 블록 표시를 지운다 text.replace("```json", "").replace("```", "").strip()을 세 줄로 나눠 쓴다
문자열을 딕셔너리로 바꾼다 json.loads(clean)
읽지 못해도 멈추지 않는다 try: … except json.JSONDecodeError: … return None
질문을 보내 응답 문자열을 받는다 call_llm(question)
응답 문자열을 딕셔너리로 바꾼다 parse_llm_json(text)
꺼내기 전에 확인한다 if result:

오전 산출물은 agent_core/llm_client.py입니다.

오후에는 이 파일의 두 함수로 AI 에이전트를 만듭니다.


AI 에이전트와 도구 호출

AI 에이전트

AI 에이전트(Agent)와 일반 챗봇의 가장 큰 차이점은 '독자적인 행동 능력'.

단순히 질문에 답을 하는 챗봇과 달리, AI 에이전트는 사용자가 목표를 주면 스스로 계획을 세우고 외부 도구를 사용해 업무를 직접 수행함.

도구 호출

LLM(대형 언어 모델)이 사용자의 컴퓨터에 있는 함수를 직접 실행하는 것은 아니다.

LLM이 수행하는 역할은 직접 실행이 아니라 어떤 함수를 어떤 인자(값)로 실행해야 할지 결정하고 요청서(JSON 형태)를 만들어주는 것.


5 · 도구를 만들고 LLM이 고르게 한다

왜 필요한가

오전의 LLM은 로그를 설명하기만 했습니다.

로그 파일을 세거나 IP를 조회하지는 못합니다.

2교시에 봤듯이 LLM은 조회하지 않고 지어냅니다.

사실은 우리 코드가 확인해야 합니다.

그래서 일을 나눕니다.

우리가 도구(함수)를 만들어 두고, LLM은 어느 도구를 쓸지 고르기만 합니다.

실행은 우리 코드가 합니다.


5교시에 사용하는 용어

용어 (읽는 법)뜻5교시의 예

에이전트 (agent) 목표를 받아 스스로 순서를 정하고 도구를 쓰는 프로그램 목표 → 도구 고르기 → 실행 → 답
도구 (tool) LLM이 고를 수 있게 우리가 만들어 둔 함수 count_failed_logins(args)
도구 호출 (tool calling) 모델이 어떤 함수를 어떤 값으로 부를지 정하는 구조 {"tool": "count_failed_logins", "args": {"user": "admin"}}
도구 목록 쓸 수 있는 함수의 이름·설명·인자를 적은 글 tools_text

5.1 도구는 딕셔너리 하나를 받는 함수입니다

도구는 평범한 파이썬 함수입니다.

다만 인자를 딕셔너리 하나(args)로 받게 만듭니다.

LLM이 인자를 JSON으로 주기 때문입니다.

import json

def count_failed_logins(args):

    with open("normalized_logs.json", encoding="utf-8") as f:
        rows = json.load(f)

    count = 0

    for row in rows:

        if (
            row["user"] == args["user"]
            and row["level"] == "WARN"
        ):
            count = count + 1

    return count


print(
    count_failed_logins(
        {"user": "kim.cs"}
    )
)
# 1

args["user"]로 어느 계정을 셀지 받습니다.

9/29 오전의 룰 ①과 같은 코드입니다.

계정이 같고 등급이 WARN인 기록을 셉니다.

결과는 return으로 반환합니다.

화면에 출력하지 않습니다.

부른 쪽이 그 값을 다시 씁니다.


5.2 도구 목록을 보내면 LLM이 하나를 고릅니다

프롬프트에 도구 목록과 목표를 함께 넣습니다.

LLM은 어느 도구를 어떤 값으로 부를지를 JSON으로 답합니다.

tools_text = "쓸 수 있는 도구는 둘입니다. "
tools_text = tools_text + "1) count_failed_logins: 한 계정의 로그인 실패 횟수를 센다. 인자는 user. "
tools_text = tools_text + "2) lookup_ip: IP 주소의 나라와 통신사를 조회한다. 인자는 ip. "
tools_text = tools_text + "목표를 이루는 데 쓸 도구 하나를 골라 JSON 으로만 답하세요. "
tools_text = tools_text + '예시: {"tool": "count_failed_logins", "args": {"user": "kim01"}} '

text = call_llm(
    tools_text
    + "목표: admin 계정이 로그인에 몇 번 실패했는지 알려 주세요."
)

실제로 받은 답입니다.

{
  "tool": "count_failed_logins",
  "args": {
    "user": "admin"
  }
}
  1. LLM은 고르기만 했습니다. 로그 파일을 열지도, 함수를 실행하지도 않았습니다.
  2. 고른 결과는 문자열입니다. 오전에 만든 parse_llm_json으로 딕셔너리로 바꿉니다.
  3. 실행은 우리 파이썬 코드가 합니다. 그래서 실행할지 말지를 우리가 정할 수 있습니다.

🐍 문법 상자 · 딕셔너리 안의 딕셔너리

choice = {
    "tool": "count_failed_logins",
    "args": {
        "user": "kim.cs"
    }
}

print(choice["tool"])
print(choice["args"]["user"])

# count_failed_logins
# kim.cs

쓰는 것결과

choice["tool"] 고른 도구 이름 — 문자열
choice["args"] 인자 — 딕셔너리
choice["args"]["user"] 그 안의 계정 이름

⚠ LLM이 고른 것은 아직 이름일 뿐입니다.

함수는 우리 코드가 실행합니다.


5.3 되풀이되는 줄을 함수 choose_tool로 묶습니다

문제 5-3에서 쓴 「질문을 만들어 보내고 → 답을 딕셔너리로 바꾸는」 줄은 오후 내내 되풀이됩니다.

매번 다시 쓰지 않도록 함수 하나로 묶습니다.

def choose_tool(goal):

    text = call_llm(
        tools_text + "목표: " + goal
    )

    if text == "":
        print(
            "LLM 의 답을 받지 못했습니다. "
            "1분 기다린 뒤 이 셀을 다시 실행합니다 "
            "(한도를 넘으면 429 가 옵니다)"
        )

    return parse_llm_json(text)

줄하는 일문제 5-3에서 쓴 줄

def choose_tool(goal): 목표 하나를 받는 함수를 만든다 —
text = call_llm(tools_text + "목표: " + goal) 도구 목록과 목표를 이어 붙여 LLM에 보낸다 1번 · 2번
if text == "": 답을 받지 못했으면 안내를 출력한다 —
return parse_llm_json(text) 답을 딕셔너리로 바꿔 반환한다 3번

쓰는 법은

choice = choose_tool(goal)

한 줄입니다.

문제 5-3의 네 줄이 한 줄이 됩니다.

반환값은

{
    "tool": "…",
    "args": {
        …
    }
}

모양의 딕셔너리입니다.

LLM의 답을 읽지 못하면 None입니다.


5.4 목표가 바뀌면 다른 도구를 고릅니다

같은 도구 목록에 목표만 바꿔 보냈을 때 실제로 받은 답입니다.

목표LLM이 고른 것

admin 계정이 로그인에 몇 번 실패했는지 알려 주세요. count_failed_logins · {"user": "admin"}
185.220.101.34는 어느 나라의 주소인지 알려 주세요. lookup_ip · {"ip": "185.220.101.34"}
admin 계정을 잠가 주세요. count_failed_logins · {"user": "admin"}
오늘 점심 메뉴를 추천해 주세요. 「관련된 도구가 없습니다」라는 문장, 또는 {"tool": null, "args": {}}

목표에 맞는 도구가 목록에 있으면 그것을 고릅니다.

목록에 없는 일(계정 잠금)을 시켰더니, 있는 도구 가운데 하나를 억지로 골랐습니다.

목표는 이뤄지지 않습니다.

도구와 상관없는 목표에는 JSON이 아닌 문장이 올 때도 있습니다.

parse_llm_json이 None을 반환하는 경우입니다.

그래서 LLM이 고른 것을 그대로 실행하지 않고 확인합니다.

6교시와 7교시가 그 확인입니다.


🐍 문법 상자 · if · elif — 갈래 고르기

tool = "lookup_ip"

if tool == "count_failed_logins":
    print("세기")

elif tool == "lookup_ip":
    print("조회")

# 조회

쓰는 것뜻

if 조건: 첫 갈래
elif 조건: 위가 거짓일 때 다음 갈래

⚠ 두 조건이 다 거짓이면 아무 줄도 실행되지 않습니다.

그 뒤에 result를 쓰면 NameError입니다.


5.5 실행 결과를 다시 LLM에 넘겨 답을 만듭니다

도구가 반환한 값은 숫자나 짧은 문자열입니다.

그 값을 다시 LLM에 넘겨 사람이 읽을 문장으로 만듭니다.

prompt2 = "목표: admin 계정이 로그인에 몇 번 실패했는지 알려 주세요. "
prompt2 = prompt2 + "도구 count_failed_logins 를 실행한 결과는 4 입니다. "
prompt2 = prompt2 + "이 결과로 목표에 한 문장으로 답하세요."

print(call_llm(prompt2))

# admin 계정의 로그인 실패 횟수는 총 4번입니다.

목표 → 도구 고르기 → 실행 → 결과로 답하기.

이 한 바퀴가 에이전트의 기본 흐름입니다.

숫자 4는 LLM이 지어낸 것이 아니라 우리 도구가 로그 파일에서 센 값입니다.

숫자를 문자열에 이어 붙일 때는 9/22에 배운 str()로 문자열로 바꿉니다.


🐍 문법 상자 · 숫자를 문자열에 붙일 때는 str()

result = 4

print(
    "결과는 "
    + str(result)
    + "번"
)

# 결과는 4번

쓰는 것결과

"결과는 " + str(result) "결과는 4"
"결과는 " + result TypeError: can only concatenate str (not "int") to str

⚠ print("결과는", result)처럼 쉼표로 넘기면 str()이 없어도 됩니다.

이어 붙이는 +에서만 필요합니다.


한눈에

하려는 일쓰는 코드

도구를 만든다 def count_failed_logins(args): … return count
LLM에게 고르게 한다 call_llm(tools_text + "목표: " + goal)
고른 것을 읽는다 choice = parse_llm_json(text) · choice["tool"] · choice["args"]
위 두 줄을 한 번에 choice = choose_tool(goal)
우리 코드로 실행한다 if choice["tool"] == "…": result = 도구(choice["args"])
결과로 답을 만든다 call_llm("목표: … 실행한 결과는 … 한 문장으로 답하세요.")

LLM은 고르기만 합니다.

실행은 우리 코드가 합니다.


라우터 — 이름으로 함수를 찾아 실행한다

라우터

사용자가 요청한 URL(또는 네트워크 경로)을 그에 맞는 특정 코드나 화면(핸들러/컴포넌트)으로 이어 주는 일.

레지스트리

레지스트리(Registry)처럼 이름과 실제 대상을 매핑(짝짓기)해 두는 구조는 컴퓨터 시스템과 소프트웨어 아키텍처에서 매우 핵심적인 역할.

  1. 찾기과 관리가 쉬워짐(추상화)
  2. 대상이 바뀌어도 쓰는 사람은 알 필요 없음(느슨한 결합)
  3. 중복을 방지하고 한 곳에서 통제할 수 있음(중앙 집중 관리)

6 · 이름으로 함수를 찾아 실행한다

왜 필요한가

5교시에는 도구마다 if · elif를 한 갈래씩 썼습니다.

도구가 열 개면 갈래도 열 개입니다.

LLM이 목록에 없는 이름을 답하면 어느 갈래에도 걸리지 않고 조용히 지나갑니다.

왜 아무 일도 안 일어났는지 아무도 모릅니다.

그래서 이름과 함수를 짝지은 딕셔너리를 만들고, 이름으로 함수를 찾아 실행하는 코드를 한 곳에 둡니다.

없는 이름이면 그 자리에서 알립니다.


6교시에 사용하는 용어

용어 (읽는 법)뜻6교시의 예

레지스트리 (registry) 이름과 함수를 짝지어 둔 딕셔너리 tool_registry = {"lookup_ip": lookup_ip}
라우터 (router) 이름을 보고 실제 함수를 찾아 실행하는 코드 route_tool_call(choice)

6.1 함수도 딕셔너리의 값으로 담을 수 있습니다

함수 이름을 괄호 없이 쓰면 실행하지 않고 함수 자체를 가리킵니다.

그래서 딕셔너리의 값으로 담을 수 있습니다.

def say_ok(args):
    return "ok " + args["name"]


registry = {
    "ok": say_ok
}


func = registry.get("ok")

print(
    func(
        {"name": "deploy"}
    )
)

# ok deploy


print(
    registry.get(
        "없는이름"
    )
)

# None

say_ok는 함수 자체이고, say_ok(…)는 함수를 실행한 결과입니다.

10/2 오후 스케줄러에 함수를 넘길 때도 괄호 없이 썼습니다.

.get()은 9/22에 배웠습니다.

키가 없으면 오류를 내지 않고 None을 반환합니다.

도구가 늘어도 딕셔너리에 한 줄만 더하면 됩니다.

실행하는 코드는 바뀌지 않습니다.


🐍 문법 상자 · 함수를 딕셔너리에 담기 — 괄호 없이

def double(args):
    return args["n"] * 2


registry = {}

registry["double"] = double

print(
    registry["double"](
        {"n": 5}
    )
)

# 10

쓰는 것뜻

double 함수 자체 — 담아 둘 수 있다
double({"n": 5}) 함수를 실행한 결과 — 10
registry["double"]({"n": 5}) 꺼낸 함수를 실행한다

⚠ registry["double"] = double()처럼 괄호를 붙이면 담는 순간 실행되어 TypeError가 납니다.


6.2 등록되지 않은 이름은 무시하지 않습니다

5교시에 봤듯이 LLM은 목록에 없는 일을 시키면 엉뚱한 도구를 고르거나, 없는 이름을 답할 수 있습니다.

func = tool_registry.get(
    "delete_all_logs"
)
# None


if func:
    result = func(args)

else:
    print(
        "[오류] 등록되지 않은 도구: delete_all_logs"
    )

레지스트리에 없는 이름이면 .get()이 None을 반환합니다.

if func:로 확인합니다.

없는 이름을 무시하지 않고 알립니다.

LLM이 도구를 지어냈거나 레지스트리를 잘못 만든 것이라, 어느 쪽이든 사람이 알아야 합니다.

조용히 넘어가면 「왜 조치가 안 됐는지」가 어디에도 남지 않습니다.


🐍 문법 상자 · .get() — 없는 키는 None

registry = {
    "count_failed_logins": 1
}

print(
    registry.get("count_failed_logins"),
    registry.get("block_everything")
)

# 1 None

쓰는 것키가 없을 때

registry[name] KeyError로 멈춘다
registry.get(name) None — if func:로 확인할 수 있다

⚠ LLM이 목록에 없는 도구 이름을 보낼 수 있습니다.

그래서 []가 아니라 .get()으로 꺼냅니다.


🐍 문법 상자 · 도구를 더해도 라우터는 그대로

def count_by_ip(args):
    return 3


registry = {}

registry["count_by_ip"] = count_by_ip

print(
    list(
        registry.keys()
    )
)

# ['count_by_ip']

바꾸는 곳바꾸지 않는 곳

도구 함수 하나 · 등록 한 줄 route_tool_call

⚠ 라우터는 이름으로 찾을 뿐이라 도구가 늘어도 고칠 필요가 없습니다.

이름만 LLM에게 알려 준 것과 같아야 합니다.


한눈에

하려는 일쓰는 코드

도구를 등록한다 tool_registry["lookup_ip"] = lookup_ip (괄호 없이)
이름으로 함수를 꺼낸다 func = tool_registry.get(name)
꺼낸 함수를 실행한다 func(choice["args"])
없는 이름을 알린다 print("[오류] 등록되지 않은 도구:", name)
한 줄로 실행한다 route_tool_call(choice)

오후 산출물 agent_core/tool_router.py가 생겼습니다.

도구가 늘어도 라우터는 고치지 않습니다.


승인 게이트 — 위험한 도구 앞에 사람을 둔다

사람 개입

AI나 자동화 흐름에서 AI가 처리하기 어렵거나 위험도가 높은 지점, 혹은 중요한 판단과 승인이 필요한 단계에 배치한다.

되돌릴 수 없는 조치

시스템 상태나 데이터를 이전으로 복구할 수 없는 '되돌릴 수 없는 조치'를 자동 실행하는 것은 비즈니스 연속성에 치명적인 위험을 초래할 수 있다.


7 · 위험한 도구 앞에 사람을 둔다

왜 필요한가

로그를 세거나 IP를 조회하는 도구는 틀려도 다시 하면 됩니다.

계정 잠금은 다릅니다.

잘못 잠그면 그 사람은 일을 못 하고, 되돌리는 데 사람 손이 듭니다.

5교시에 봤듯이 LLM은 엉뚱한 도구를 고를 수 있습니다.

그래서 되돌리기 어려운 도구 앞에는 사람의 확인을 둡니다.

이것을 승인 게이트라고 합니다.


7교시에 사용하는 용어

용어 (읽는 법)뜻7교시의 예

승인 게이트 (approval gate) 위험한 실행 앞에 사람 확인을 두는 것 if choice["tool"] in need_approval:
사람 개입 (human-in-the-loop · 휴먼 인 더 루프) 자동 흐름 중간에 사람이 끼는 설계 담당자의 답 answer = "y"

7.1 승인이 필요한 도구를 목록으로 정해 둡니다

도구를 되돌릴 수 있는가로 나눕니다.

승인이 필요한 도구의 이름을 리스트에 적어 둡니다.

need_approval = [
    "lock_account"
]

print(
    "lock_account"
    in need_approval
)
# True

print(
    "lookup_ip"
    in need_approval
)
# False

바로 실행해도 되는 도구승인을 받아야 하는 도구

count_failed_logins · lookup_ip lock_account
틀려도 다시 하면 된다 잘못하면 사람이 일을 못 한다

in은 9/23에 배웠습니다.

리스트 안에 그 값이 있으면 True입니다.

승인 여부를 LLM이 정하게 하지 않습니다.

규칙은 코드에 적혀 있어야 언제나 같게 동작합니다.

오늘의 lock_account는 실습이라 화면에 출력만 합니다.

실제 계정을 잠그지 않습니다.


7.2 담당자의 답을 보고 실행할지 정합니다

승인이 필요한 도구는 담당자의 답을 확인한 뒤에 실행합니다.

실습에서는 그 답을 변수 answer에 직접 적습니다.

answer = "n"          # 담당자의 답

if answer == "y":
    print("실행합니다")

else:
    print("보류합니다")

# 보류합니다

"y"일 때만 실행합니다.

"y"가 아닌 모든 답은 실행하지 않습니다.

잘못 적은 답이 실행으로 이어지면 안 됩니다.

보류했다는 것은 문자열 "held"로 남깁니다.

실제 서비스에서는 담당자의 답을 승인 화면이나 메신저에서 받습니다.

5과목에서 다룹니다.

오늘은 답을 확인하고 나서 실행한다는 순서만 만듭니다.


🐍 문법 상자 · in과 !=

need_approval = [
    "lock_account",
    "block_ip"
]

tool = "lock_account"
answer = "n"

print(
    tool in need_approval,
    answer != "y"
)

# True True

쓰는 것뜻

tool in need_approval 목록 안에 있으면 True (9/23)
answer != "y" "y"가 아니면 True — 「같지 않다」

⚠ "Y"처럼 대문자로 답하면 != "y"가 True라서 보류됩니다.

그대로 두는 것이 안전한 쪽입니다.


7.3 무엇을 실행했는지 기록으로 남깁니다

에이전트가 한 일은 실행했든 보류했든 기록으로 남깁니다.

9/28 오후에 배운 json.dump로 agent_result.json에 저장합니다.

records = []

records.append(
    {
        "goal": "admin 계정이 몇 번 실패했나",
        "tool": "count_failed_logins",
        "result": 4
    }
)

with open(
    "agent_result.json",
    "w",
    encoding="utf-8"
) as f:

    json.dump(
        records,
        f,
        ensure_ascii=False,
        indent=2
    )

기록 한 건은 딕셔너리입니다.

목표 · 고른 도구 · 결과를 담습니다.

보류한 것도 "result": "held"로 남깁니다.

남기지 않으면 「왜 조치가 안 됐는지」를 설명할 수 없습니다.

ensure_ascii=False는 한글을 그대로 저장하고, indent=2는 두 칸 들여쓰기입니다.


🐍 문법 상자 · 기록을 리스트에 모아 JSON으로

import json

records = []

records.append(
    {
        "goal": "잠가 주세요",
        "tool": "lock_account",
        "result": "held"
    }
)

print(
    json.dumps(
        records,
        ensure_ascii=False,
        indent=2
    )
)

결과:

[
  {
    "goal": "잠가 주세요",
    "tool": "lock_account",
    "result": "held"
  }
]

쓰는 것뜻

records.append({…}) 리스트 끝에 딕셔너리 하나를 더한다 (9/22)
ensure_ascii=False 한글을 그대로 쓴다
indent=2 두 칸 들여쓰기 — 사람이 읽기 좋게
json.dump(records, f, …) 같은 모양을 파일 f에 쓴다

⚠ ensure_ascii=False가 없으면 한글이 \uc7a0 같은 기호로 저장됩니다.


한눈에

하려는 일쓰는 코드

승인이 필요한 도구를 정한다 need_approval = ["lock_account"]
승인이 필요한지 확인한다 if choice["tool"] in need_approval:
담당자의 답을 확인한다 if answer == "y":
승인하지 않으면 보류한다 if answer != "y": return "held"
한 일을 기록한다 records.append({"goal": …, "tool": …, "result": …})
파일로 남긴다 json.dump(records, f, ensure_ascii=False, indent=2)

 

 

 

오늘은 LLM과 함께한 즐거운 시간입ㄴㅣ다...^_^

이제 AI 자동화 기초가 얼마 남지 않았는데,, 떨리네요 다음주에는 또 어떤 내용이 시작될까,,,?

3일간의 연휴를 끝내고 오니 정말 정신없구 좋은 것 같아요.

오늘 점심으로는 집에서 불닭김밥을 싸왔는데, 가끔 불닭쌈이나 다양한 밥을 싸오는게 목표입니다.

다만,, 휴게실에서 라면이나 냄새가 너무 심한건 못먹어서 아쉽긴 한데 어쩔 수 없죠,,

그럼 빠잇 아됴,,,,

안녕하세요

벌써 6일째네용,,,

오늘은 무려 금요일,,!!!!!

화이팅 해봅시다.

 

오늘 아침 30분 과제는 간단했어용

vscode에서 Python이랑 Jupyter 확장 깔구,,

Git Bash로 default 터미널 바꾸고,,

flask, request, schedule, ipykernel 패키지 설치하고 끝났습니다~!

 


 

 

오늘의 순서

  • 폴링 ↔ 웹훅 · Flask로 서버 만들기
  • curl로 요청 보내기 · test_webhook.sh
  • argparse · webhook_server.py 조립
  • 트리거·조건·액션 — 지금까지 만든 게 사실 한 모양이었다
  • 멱등성 — 같은 알림이 세 번 가면 아무도 안 본다
  • schedule · cron · scheduler_job.py 조립

1. 받는 쪽이 된다

폴링(polling)

폴링 방식의 가장 큰 약점은 불필요한 서버 부하와 트래픽 낭비, 그리고 실시간성 저하이다.

웹훅(webhook)

서버에서 이벤트가 발생했을 때 클라이언트에게 실시간으로 데이터를 보내주는 방식이다.

두 방식은 데이터를 주고받는 주체와 방향에서 가장 큰 차이가 있다.


2. 웹훅 수신 서버를 만든다

왜 필요한가

지금까지는 내가 필요할 때 물어보는 방식이었습니다.

파일을 열고, API를 부르고.

그런데 침해 시도는 내가 물어보는 시각에 맞춰 일어나지 않습니다.

방법이 둘입니다.

  • 십 분마다 계속 물어보거나 → 폴링
  • 일이 생기면 상대가 알려 주게 하거나 → 웹훅

웹훅을 받으려면 이쪽도 서버가 되어야 합니다.


이 시간에 나오는 말

말뜻

폴링 일정 간격으로 계속 물어보는 방식
웹훅 일이 생기면 상대가 먼저 알려 주는 방식
서버 요청을 기다렸다가 답하는 프로그램
Flask 파이썬으로 작은 서버를 만드는 패키지
라우트 어느 주소로 온 요청을 어느 함수가 받을지 정한 것

2.1 폴링 — 계속 물어본다

폴링은 새 일이 없어도 계속 물어봅니다.

그 낭비를 눈으로 봅니다.

inbox = ["", "", "경보!", "", ""]      # 다섯 번 열어 보는데 한 번만 있다

for box in inbox:
    if box:
        print("일이 있다:", box)
    else:
        print("비어 있다")

다섯 번 물어서 한 번 건졌습니다.

네 번은 낭비입니다.


2.2 Flask — 서버를 만든다

서버는 요청을 기다렸다가 답하는 프로그램입니다.

Flask로 열 줄이면 만듭니다.

from flask import Flask, request

app = Flask(__name__)

@app.route("/webhook", methods=["POST"])     # 이 주소로 POST가 오면
def webhook():                                # 이 함수가 받는다
    event = request.get_json()                # 본문을 딕셔너리로
    return {"status": "ok"}, 200              # 답과 상태코드를 돌려준다

app.run(port=5000)

@app.route(...) 한 줄이 주소와 함수를 잇습니다.

이것을 라우트라고 합니다.

앞에 붙은 @는 오늘 처음 나옵니다. 뜻은 나중에 배우고, 오늘은 이 모양 그대로 씁니다.

돌려주는 200이 9/29에 배운 그 상태코드입니다.

받았다고 알리는 것입니다.

app.run()은 끝나지 않습니다.

그래서 파일로 만들어 백그라운드로 실행합니다.


정리 📋 한눈에

쓰는 법뜻

폴링 계속 물어본다. 빨리 알려면 헛걸음이 는다
웹훅 생기면 상대가 알려 준다. 받을 문이 있어야 한다
@app.route("/webhook", methods=["POST"]) 그 주소로 온 POST를 이 함수가 받는다
request.get_json() 본문을 딕셔너리로
return {...}, 200 답과 상태코드를 돌려준다
app.run() 끝나지 않는다. 백그라운드로 실행한다

3. 손으로 두드린다

curl

터미널(명령줄)에서 서버와 데이터를 주고받기 위해 사용하는 데이터 전송 명령행 도구.

주요 용도는 API 테스트 및 개발, 웹페이지 및 파일 다운로드, 서버 통신 확인이다.

GUI와 CLI

컴퓨터와 소통하는 방식(입력)과 컴퓨터가 보여주는 방식(출력).


4. 서버가 도는지 확인한다

왜 필요한가

서버를 실행했는데 제대로 도는지 알 방법이 있어야 합니다.

확인하려고 또 파이썬 파일을 만드는 것은 번거롭습니다.

터미널에서 곧바로 보냅니다.

그 명령이 curl입니다.

9/29에 배운 메서드·헤더·본문 세 조각이 옵션 셋으로 그대로 옵니다.


이 시간에 나오는 말

말뜻

CLI 글자로 명령을 넣어 쓰는 방식
GUI 마우스로 눌러 쓰는 방식
curl 터미널에서 요청을 보내는 명령
-X · -H · -d 메서드 · 헤더 · 본문

손에 익혀 둘 명령 넷

터미널에서 쓰는 명령입니다.

cd · ls는 9/28·9/29 아침에 이미 쳐 봤습니다.

명령무엇을예

cd 폴더를 옮긴다 cd agent_core · cd ..는 한 칸 위로
ls 지금 폴더에 무엇이 있는지 본다 ls · ls -a
cat 파일 내용을 화면에 펼친다 cat agent.log
grep 특정 낱말이 든 줄만 찾는다 grep ERROR agent.log

3.1 curl — 세 조각을 옵션으로

옵션무엇9/29에 배운 것

-X POST 메서드 GET / POST
-H "Content-Type: application/json" 헤더 요청에 덧붙이는 정보
-d '{"rule":"…"}' 본문 보낼 내용
-s 진행 막대를 숨긴다 —
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"rule":"brute_force"}' http://127.0.0.1:5001/webhook

이 명령은 노트북 셀이 아니라 터미널(Git Bash)에 입력합니다.

줄 끝의 \는 명령이 다음 줄에 이어진다는 표시입니다.


3.2 test_webhook.sh — 확인을 파일로 묶는다

alert(경보)가 세 가지면 curl을 세 번 칩니다.

매번 손으로 치면 오타가 납니다.

한 파일에 묶어 두고 한 줄로 부릅니다.

save_script("test_webhook.sh", r"""
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"rule":"brute_force"}' http://127.0.0.1:5001/webhook
echo

curl -s -X POST -H "Content-Type: application/json" \
  -d '{"rule":"night_login"}' http://127.0.0.1:5001/webhook
echo
""")

echo는 줄을 바꿔 주는 것뿐입니다.

없으면 답이 한 줄에 붙습니다.

save_script("파일이름", r"""…""")는 따옴표 세 개 사이의 내용을 그 파일로 저장합니다.

맨 위 준비 셀에 있는 함수입니다.

실행은 터미널에서 다음과 같이 합니다.

bash test_webhook.sh

셸 스크립트 문법은 오늘 배우지 않습니다.

curl을 줄줄이 적어 둔 파일로만 씁니다.


정리 📋 한눈에

쓰는 법뜻

curl -X POST 메서드를 정한다
-H "Content-Type: application/json" 요청 본문 형식을 알린다. 빼면 415
-d '{...}' 본문을 실어 보낸다
-o /dev/null -w "%{http_code}" 본문은 버리고 상태코드만
bash 파일.sh curl을 묶어 둔 파일을 한 번에 돌린다

상태코드

코드언제

200 갖춰 보냈다
404 그런 주소가 없다
405 주소는 있는데 메서드가 다르다
415 요청 본문 형식을 처리할 수 없다

4. 설정을 밖으로

argparse

명령(스크립트) 뒤에 붙는 인자(값)를 받으려면 add_argument() 메서드를 사용하면 된다.

인자는 크게 두 가지 종류로 나뉩니다.

  • 위치에 따라 순서대로 들어가는 위치 인자(Positional Arguments)
  • - 또는 --를 붙여 옵션으로 지정하는 선택적 인자(Optional Arguments)

기본값(default)

기본값 인자(Default)를 설정하지 않은 상태로, 함수를 호출할 때 값을 전달하지 않으면 에러(Error)가 발생.


5. 포트를 코드 밖으로

왜 필요한가

지금 서버는 포트가 코드에 적혀 있습니다.

바꾸려면 파일을 고쳐야 합니다.

같은 서버를 두 개 실행하려면 포트가 달라야 하는데, 그때마다 파일을 고칠 수는 없습니다.

명령 뒤에 붙여 넘기면 됩니다.

python webhook_server.py --port 5001

9/30에 .env로 비밀 값을 밖에 뺐습니다.

오늘은 설정 값을 뺍니다.

같은 생각입니다.


이 시간에 나오는 말

말뜻

CLI 인자 명령 뒤에 붙여 넘기는 값
argparse 그 값을 받아 주는 파이썬 기본 도구
--port 이름을 붙인 인자. 순서를 안 외워도 된다
기본값 안 줬을 때 대신 쓰는 값

4.1 argparse — 명령 뒤의 값을 받는다

import argparse

parser = argparse.ArgumentParser(description="경보를 받는 웹훅 서버")
parser.add_argument("--port", type=int, default=5000, help="열어 둘 포트 번호")
args = parser.parse_args()

print(args.port)

type=int를 빼면 글자로 들어옵니다.

숫자로 쓰려면 적어야 합니다.

default=5000 덕분에 안 줘도 됩니다.

--help를 붙이면 적어 둔 설명이 그대로 나옵니다.

공짜로 얻는 사용법입니다.


4.2 webhook_server.py 조립

오늘 만든 조각을 한 파일로 잇습니다.

새 문법은 없습니다.

조각어디서

Flask 라우트 2교시
argparse --port 이 교시
받은 것을 파일로 2교시 문제 2-9

이름은 webhook_server.py로 씁니다.


정리 📋 한눈에

쓰는 법뜻

parser.add_argument("--port", type=int, default=5000) 이름 붙인 인자를 받는다
args.port 받은 값을 꺼낸다
--help 적어 둔 설명이 그대로 사용법이 된다
type=int 빼면 글자로 들어온다

5. 트리거 · 조건 · 액션

워크플로(workflow)

업무의 효율성을 극대화하고 실수를 줄이기 위해서.

트리거(trigger)

테이블에 대한 특정 데이터 변경 이벤트(DML).

트리거는 개발자가 직접 호출하는 것이 아니라, 설정된 조건이 충족되면 DBMS가 자동으로 감지하여 실행한다.


지금까지 만든 게 사실 한 모양이었다

왜 필요한가

나흘 동안 만든 것을 늘어놓으면 겉모습이 다 다릅니다.

파일을 읽는 것, API를 부르는 것, 웹훅을 받는 것.

그런데 세 조각으로 쪼개 보면 전부 같은 모양입니다.

  • 무엇이 시작을 알렸는가 → 트리거
  • 어떤 경우에만 움직이는가 → 조건
  • 그래서 무엇을 하는가 → 액션

나흘치를 한 표에 놓으면

트리거조건액션언제 만들었나

파일이 있다 깨진 줄인가 건너뛰고 기록 9/28 log_parser.py
로그가 쌓였다 실패 3회 이상인가 alert(경보)를 출력한다 9/29 룰 ①
alert가 났다 처음 보는 IP인가 조회한다 9/30 api_client.py
시각이 됐다 처음 보는 사건인가 알린다 오늘

거창한 것이 아닙니다.

**「이런 모양이면 이렇게 하라」**를 코드로 적은 것뿐입니다.


이 시간에 나오는 말

말뜻

워크플로 일이 흘러가는 정해진 순서
트리거 일을 시작하게 만드는 사건
조건 그중 어떤 경우에만 움직일지
액션 그래서 실제로 하는 일

5.1 세 조각으로 갈라 적는다

9/29에 쓴 룰 ① 코드를 세 조각으로 갈라 보면 이렇습니다.

for row in rows:                          # ← 트리거 : 로그가 있다
    if row["level"] == "WARN":            # ← 조건 : 실패인가
        count[row["user"]] = ...          # ← 액션 : 센다

갈라 적으면 바꿀 곳이 분명해집니다.

기준을 바꾸려면 조건만, 알림 방식을 바꾸려면 액션만 고칩니다.

아래 셀을 먼저 실행합니다.

오늘 오후 내내 이 events를 씁니다.

9/29 룰 세 개가 찾아낸 alert(경보)입니다.

events = [
    {"id": "brute_force:admin", "rule": "brute_force", "user": "admin", "count": 4},
    {"id": "password_spraying:185.220.101.34", "rule": "password_spraying", "ip": "185.220.101.34", "accounts": 4},
    {"id": "night_login:admin:03:17:09", "rule": "night_login", "user": "admin", "time": "03:17:09"},
]

print(f"경보 {len(events)}건")

5.2 액션을 함수로 — 웹훅 수신 서버로 전송한다

액션을 함수로 떼어 두면 알림 방식을 바꿀 때 한 곳만 고칩니다.

오늘은 그 액션이 오전에 만든 웹훅 서버로 보내는 것입니다.

import requests

def send_alert(event):
    response = requests.post(
        "http://127.0.0.1:5005/webhook",
        json=event,
        timeout=5
    )
    return response.json()

requests.post는 9/30에 배운 get의 짝입니다.

보낼 내용은 json=에 담습니다.


정리 📋 한눈에

조각코드에서 어디

트리거 반복을 여는 줄 — 무엇이 시작을 알렸나
조건 if 줄 — 어떤 경우에만 움직이나
액션 안쪽 줄 — 그래서 무엇을 하나

쓰는 법뜻

requests.post(주소, json=값) 보낼 내용을 본문에 담아 보낸다
액션을 함수로 알림 방식을 바꿀 때 한 곳만 고친다

6. 같은 알림이 세 번 가면

멱등성(idempotency)

같은 작업을 여러 번 실행해도 최종 결과가 한 번 실행했을 때와 똑같은 성질을 뜻함.

불안정한 네트워크 환경에서 시스템의 안정성과 신뢰성을 지키는 데 매우 중요하다.

중복 알림

시스템, 비즈니스, 사용자 경험(UX) 측면 전반에 걸쳐 심각한 부작용 발생.


이미 보낸 것은 다시 보내지 않는다

왜 필요한가

스케줄러가 5분마다 로그를 읽는다고 해 봅시다.

그런데 같은 로그가 파일에 그대로 있습니다.

다음 실행에서도 같은 사건을 보고 또 알림을 보냅니다.

하루면 288번입니다.

같은 알림이 세 번 가면 아무도 안 봅니다.

진짜 alert(경보)가 그 사이에 묻힙니다.

같은 일을 두 번 해도 결과가 한 번 한 것과 같아야 합니다.

이 성질을 멱등성이라고 합니다.


이 시간에 나오는 말

말뜻

멱등성 같은 일을 여러 번 해도 결과가 한 번 한 것과 같은 성질
사건 번호 사건 하나를 가리키는 고유한 이름
processed_ids.json 이미 처리한 사건 번호를 적어 두는 파일

6.1 중복이 나는 장면을 먼저 본다

막는 법을 배우기 전에 실제로 벌어지는 것을 봅니다.

for turn in [1, 2]:                  # 스케줄러가 두 번 실행되었다고 하면
    for event in events:
        print(f"{turn}회차 [전송] {event['rule']}")

같은 경보가 두 번씩 나갑니다.

5분마다면 하루에 288번입니다.


6.2 processed_ids.json — 파일에 적어 둔다

리스트에만 담아 두면 프로그램이 꺼지는 순간 사라집니다.

파일에 남겨야 다음 실행이 압니다.

import json
import os

def load_done():
    if os.path.exists("processed_ids.json"):
        with open("processed_ids.json", encoding="utf-8") as f:
            return json.load(f)

    return []                                  # 처음이면 빈 목록

os는 오늘 처음 쓰는 도구입니다.

파이썬에 들어 있어 import os만 하면 됩니다.

json·re와 같습니다.

os.path.exists는 파일이 있는지 보는 명령입니다.

처음 돌 때는 파일이 없습니다.

저장은 9/28에 배운 json.dump 그대로입니다.


정리 📋 한눈에

쓰는 법뜻

사건 번호(id) 같은 사건이면 언제나 같은 값이어야 한다
if event["id"] not in done: 이미 보낸 것은 건너뛴다
os.path.exists(파일) 파일이 있는지 본다. 처음엔 없다
processed_ids.json 다음 실행이 읽을 전송 기록

⚠ 전송 기록이 파일에 있습니다.

파일을 잃으면 중복이 다시 납니다.


7. 정해진 간격마다 자동으로 실행한다

스케줄러(scheduler)

파이썬 내부 라이브러리/패키지를 사용하는 방법과 운영체제(OS)의 스케줄러를 활용하는 방법.

cron

분 시 일 월 요일
*  *  *  *  *

실행 버튼을 없앤다

왜 필요한가

지금까지 만든 것은 사람이 실행해야 동작합니다.

새벽 3시에 누를 사람이 없습니다.

방법이 둘입니다.

  • 파이썬 프로그램 안에서 실행하는 것 → schedule
  • 운영체제가 실행하는 것 → cron

둘의 차이는 하나입니다.

schedule은 그 프로그램이 실행 중일 때만 동작하고, cron은 프로그램이 종료되어 있어도 운영체제가 실행합니다.


이 시간에 나오는 말

말뜻

스케줄러 정해진 때에 일을 시키는 것
schedule 파이썬 코드로 간격을 적는 패키지
cron 운영체제가 가진 정기 실행 도구
cron 표현식 분·시·일·월·요일 다섯 칸

7.1 schedule — 파이썬 안에서 돈다

import schedule
import time

def job():
    print("점검합니다")

schedule.every(2).seconds.do(job)      # 2초마다 job을 부른다

for i in range(6):                     # 여섯 번 확인하는 동안만
    schedule.run_pending()
    time.sleep(1)

다음처럼 읽기 쉽게 적습니다.

schedule.every(10).minutes.do(job)

run_pending()을 계속 불러 줘야 합니다.

혼자 도는 게 아닙니다.

실무에서는

while True:

로 계속 반복합니다.

여기서는 셀이 끝나야 하니 정해진 횟수만 반복합니다.


7.2 cron 다섯 칸 · scheduler_job.py 조립

schedule은 그 프로그램이 실행 중이어야 동작합니다.

창을 닫거나 컴퓨터가 꺼지면 멈춥니다.

운영체제가 실행하면 프로그램이 종료되어 있어도 정해진 시각에 실행됩니다.

그것이 cron입니다.

cron은 빈칸으로 나뉜 다섯 칸으로 시각을 적습니다.

앞에서부터 다음 순서입니다.

분 · 시 · 일 · 월 · 요일

표현식언제

0 6 * * * 매일 오전 6시 정각
30 2 * * * 매일 오전 2시 30분
*/10 * * * * 10분마다

읽는 요령이 있습니다.

별표가 아닌 칸만 읽고 나머지는 **「매번」**으로 읽습니다.

⚠ cron은 리눅스와 맥에 들어 있는 프로그램입니다.

윈도우에는 없습니다.

윈도우는 작업 스케줄러를 씁니다.

오늘은 표현식을 읽는 것까지만 합니다.

위 세 가지 모양만 읽을 수 있으면 됩니다.


 

오늘은 뭔가,, 파이썬으로 서버를 열어본 경험이 거의 없는데,,

좋은 경험이였던 것 같ㅇ아요..

별개로 같이 수업듣는 사람들은 하나도 이해 못했다는게 함정,,

저두 우겨넣는 중...

 

일단 오늘까지 하고 3일이나 쉬는데 너무 행복하고 리프레쉬해서 다시 올게요,, 굿,, 아됴

오늘 늦으 ㄹ뻔햇어요 휴

지각안한 나 나이스.

시작 .

 

 

오늘 아침 과제는 vscode에 새 프로젝트 폴더를 만들어서 지금까지 했던 코랩 파일을 넣는 활동을 가졌어요,,

추가로 GitHub 리포지토리도 만들었습니당,,, 새로만든 리포지토리에도 지금까지 했던 코랩파일을 업로드했답니당.. 

끝~~

 


오늘의 순서

  • requests.get()으로 실제 요청 보내기
  • params로 쿼리 매개변수 전달하기
  • headers로 인증 정보 보내기
  • timeout으로 무한 대기 방지하기
  • raise_for_status()로 HTTP 오류 감지하기
  • RequestException으로 네트워크 예외 처리하기
  • 재시도 함수 call_with_retry() 만들기
  • .env로 API 키 분리하기
  • fetch_data()로 필요한 필드만 추출하기
  • api_client.py 조립
  • api_result.json 저장
  • GET과 POST 차이 이해하기

1. API 조회 서비스란?

어제 탐지 룰에서 특정 IP가 의심스럽다는 사실까지는 확인할 수 있었다.

예를 들어 다음 IP가 탐지됐다고 하자.

185.220.101.34

하지만 현재 가지고 있는 로그만으로는 다음 정보를 알 수 없다.

  • 어느 나라에 속한 IP인지
  • 어떤 네트워크 사업자가 사용하는지
  • 어떤 기관에서 관리하는지
  • 외부 보안 정보가 있는지

이때 IP 조회 API 서비스를 사용할 수 있다.

IP 주소를 API에 전달하면 서버가 해당 IP와 관련된 정보를 데이터 형태로 돌려준다.

내 프로그램
    ↓
IP 주소 전달
    ↓
IP 조회 API
    ↓
국가 / 네트워크 / 사업자 등의 정보

2. requests란?

파이썬에서 HTTP 요청을 보낼 때 많이 사용하는 외부 라이브러리가 requests다.

import requests

파이썬 기본 모듈에도 HTTP 요청 기능이 있지만, requests는 코드가 간결하고 읽기 쉽기 때문에 널리 사용된다.

로컬 환경에 설치되어 있지 않다면 다음 명령으로 설치할 수 있다.

pip install requests

코랩에는 대부분 이미 설치되어 있다.


3. requests.get()으로 요청 보내기

가장 기본적인 GET 요청은 다음과 같다.

import requests

response = requests.get("https://ipwho.is/8.8.8.8")

requests.get()을 실행하면 서버로 요청을 보내고 응답을 받아 response 변수에 저장한다.

구조는 다음과 같다.

requests.get()
     ↓
HTTP 요청
     ↓
서버
     ↓
HTTP 응답
     ↓
response

4. status_code 확인하기

응답을 받았다면 먼저 상태코드를 확인한다.

print(response.status_code)

예:

200

200은 요청이 정상적으로 처리됐다는 뜻이다.

대표적인 상태코드는 다음과 같다.

코드의미

200 요청 성공
201 데이터 생성 성공
401 인증 문제
403 권한 문제
404 요청한 자원을 찾을 수 없음
500 서버 내부 오류

중요한 습관은 다음과 같다.

본문을 읽기 전에 상태코드부터 확인한다.

실패한 요청의 응답 본문은 우리가 기대하는 구조가 아닐 수 있기 때문이다.


5. response.json()

API가 JSON 형태로 응답했다면 .json()으로 파이썬 값으로 변환할 수 있다.

data = response.json()

print(data)

예를 들어 서버가 다음과 같은 JSON을 보냈다고 하자.

{
  "ip": "8.8.8.8",
  "country": "United States"
}

.json()을 호출한 결과는 파이썬 딕셔너리다.

print(data["country"])

결과:

United States

6. json.loads()와 response.json() 비교

어제는 다음처럼 JSON 문자열을 직접 파싱했다.

import json

text = '{"country": "Korea"}'

data = json.loads(text)

오늘은 HTTP 응답 객체에서 바로 사용한다.

data = response.json()

역할을 비교하면 다음과 같다.

방식역할

json.loads(text) JSON 문자열 → 파이썬 값
response.json() HTTP 응답의 JSON 본문 → 파이썬 값

즉, .json()이 응답 본문을 파싱하는 작업까지 대신해 준다.


7. params란?

API를 사용할 때 URL 뒤에 조건을 붙이는 경우가 있다.

예:

?fields=country,connection

직접 문자열을 이어 붙일 수도 있다.

url = "https://ipwho.is/8.8.8.8?fields=country,connection"

하지만 이렇게 직접 조립하면 공백이나 한글 같은 특수문자의 URL 인코딩을 직접 신경 써야 한다.

그래서 requests에서는 params를 사용한다.

response = requests.get(
    "https://ipwho.is/8.8.8.8",
    params={
        "fields": "country,connection"
    }
)

8. params를 사용하는 이유

params에 딕셔너리를 넘기면 requests가 알아서 URL을 조립하고 인코딩한다.

params = {
    "fields": "country,connection"
}

개념적으로는 다음 주소가 만들어진다.

https://ipwho.is/8.8.8.8?fields=country,connection

따라서 다음 습관이 좋다.

URL 뒤에 ?key=value를 직접 이어 붙이지 말고 params를 사용한다.


9. headers란?

HTTP 요청에는 주소와 쿼리뿐 아니라 헤더(Header) 라는 정보도 붙일 수 있다.

헤더에는 요청과 관련된 부가 정보를 담는다.

대표적으로 다음과 같은 정보가 들어갈 수 있다.

  • 인증 정보
  • 토큰
  • API 키
  • 데이터 형식
  • 클라이언트 정보

예:

headers = {
    "X-Api-Key": "demo-key-1234"
}

요청에 넣으면 다음과 같다.

response = requests.get(
    "https://postman-echo.com/headers",
    headers=headers
)

10. 인증 정보는 왜 headers에 넣을까?

API 키나 토큰 같은 인증 정보는 일반적인 검색 조건과 성격이 다르다.

예를 들어

?city=seoul

같은 값은 조회 조건이다.

반면

API_KEY
TOKEN

은 사용자의 인증 정보다.

따라서 보통 다음처럼 구분한다.

종류위치

조회 조건 params
인증 정보 headers

11. params와 headers 비교

구분paramsheaders

목적 조회 조건 전달 요청 부가정보 전달
대표 예 검색어, 필터 API 키, 토큰
URL 표시 일반적으로 URL에 포함됨 URL에는 직접 나타나지 않음

예:

requests.get(
    url,
    params={"fields": "country"},
    headers={"X-Api-Key": api_key}
)

12. 요청 기본 구조 한눈에 보기

response = requests.get(
    url,
    params=params,
    headers=headers
)

각 부분의 역할은 다음과 같다.

url
 ↓
어디로 요청할지

params
 ↓
무엇을 조회할지

headers
 ↓
누가 요청하는지 등의 부가정보

13. 그런데 네트워크는 항상 성공하지 않는다

HTTP 요청은 파일을 읽는 것과 다르다.

내 컴퓨터 밖에 있는 서버와 통신하기 때문에 여러 가지 문제가 발생할 수 있다.

예:

  • 인터넷 연결 끊김
  • 상대 서버 점검
  • 서버 과부하
  • 요청 제한
  • 응답 지연
  • 잘못된 주소
  • 인증 실패
  • 서버 내부 오류

따라서 API 코드를 작성할 때는

성공할 것이라고 가정하는 것보다 실패에 대비하는 것이 중요하다.


14. timeout이 필요한 이유

다음처럼 요청을 보냈다고 하자.

requests.get(url)

상대 서버가 응답하지 않는다면 프로그램이 오랫동안 기다릴 수 있다.

그래서 timeout을 지정한다.

response = requests.get(
    url,
    timeout=5
)

뜻은

최대 5초까지만 응답을 기다린다.

이다.


15. timeout의 목적

timeout을 지정하는 이유는 크게 두 가지다.

무한 대기 방지

서버가 응답하지 않는 상태로 프로그램이 계속 멈춰 있는 것을 방지한다.

시스템 자원 보호

하나의 느린 요청 때문에 프로그램 전체가 계속 잡혀 있는 상황을 줄인다.

따라서 네트워크 요청에는 가능하면 timeout을 명시하는 것이 좋다.


16. raise_for_status()

상태코드가 404나 500이어도 requests.get() 자체가 항상 파이썬 오류를 발생시키는 것은 아니다.

이때 사용하는 것이

response.raise_for_status()

다.

예:

response = requests.get(
    "https://ipwho.is/8.8.8.8",
    timeout=3
)

response.raise_for_status()

17. raise_for_status()의 역할

응답이 정상이라면 아무 일도 하지 않는다.

200
 ↓
그대로 다음 코드 실행

하지만 4xx 또는 5xx 상태코드라면 예외를 발생시킨다.

404
 ↓
raise_for_status()
 ↓
예외 발생
500
 ↓
raise_for_status()
 ↓
예외 발생

그래야 try · except로 처리할 수 있다.


18. try·except와 함께 사용하기

import requests

try:
    response = requests.get(
        "https://ipwho.is/8.8.8.8",
        timeout=5
    )

    response.raise_for_status()

    data = response.json()

except requests.RequestException:
    print("요청에 실패했습니다.")

네트워크 요청에서 발생하는 다양한 예외를 한 번에 처리할 때 requests.RequestException을 사용할 수 있다.


19. RequestException이란?

requests에서는 상황에 따라 여러 네트워크 예외가 발생할 수 있다.

예:

  • 연결 실패
  • 시간 초과
  • HTTP 상태코드 오류

이런 requests 관련 예외들의 상위 개념으로 사용할 수 있는 것이

requests.RequestException

이다.

예:

except requests.RequestException:
    print("API 요청 실패")

20. 재시도가 필요한 이유

API 요청이 한 번 실패했다고 해서 계속 실패한다는 뜻은 아니다.

일시적인 네트워크 문제일 수도 있고 서버가 잠깐 바빴을 수도 있다.

따라서 일정 횟수 다시 시도할 수 있다.

예:

1번째 요청 → 실패
2번째 요청 → 실패
3번째 요청 → 성공

이런 상황이라면 첫 번째 실패에서 프로그램을 끝낼 필요가 없다.


21. call_with_retry()

재시도 기능을 함수로 만들 수 있다.

import requests

def call_with_retry(url, tries=3):

    for i in range(tries):

        try:
            response = requests.get(
                url,
                timeout=5
            )

            response.raise_for_status()

            return response.json()

        except requests.RequestException:
            print(f"{i + 1}번째 실패")

    return None

22. call_with_retry() 실행 흐름

함수의 흐름은 다음과 같다.

요청 시작
   ↓
성공?
 ┌───────┴───────┐
Yes              No
 ↓                ↓
JSON 반환      실패 출력
                  ↓
             다시 시도
                  ↓
          모든 시도 실패?
                  ↓
              return None

23. range(tries)

다음 코드는

for i in range(tries):

tries 횟수만큼 반복한다.

예:

tries = 3

이면

i = 0
i = 1
i = 2

총 세 번 실행된다.

사용자에게 표시할 때는 0번째 실패가 어색하므로 1을 더한다.

print(f"{i + 1}번째 실패")

24. 성공하면 return

다음 코드가 중요하다.

return response.json()

요청이 성공하는 순간 함수가 종료된다.

예를 들어 첫 번째 실패 후 두 번째 요청에서 성공했다면

1회차 → 실패
2회차 → 성공 → return
3회차 → 실행 안 됨

이 된다.

남은 재시도 횟수는 사용하지 않는다.


25. 모두 실패하면 None

모든 시도가 실패하면 반복문이 끝난다.

그다음

return None

을 실행한다.

따라서 이 함수를 사용하는 쪽에서는 결과를 확인해야 한다.

data = call_with_retry(url)

if data:
    print(data)

26. API 키를 코드에 직접 쓰면 안 되는 이유

다음 코드를 생각해 보자.

API_KEY = "abcd1234-secret-key"

프로그램이 혼자 사용하는 동안에는 문제가 없어 보인다.

하지만 코드를 GitHub에 올리면 API 키도 함께 공개될 수 있다.

소스코드 공개
     ↓
API 키 노출
     ↓
다른 사람이 키 사용 가능

그래서 비밀값은 코드와 분리하는 것이 좋다.


27. 환경변수란?

환경변수는 프로그램 설정값이나 비밀값을 코드가 아닌 실행 환경 쪽에서 관리하는 방식이다.

대표적으로 다음 데이터를 코드 밖에 둘 수 있다.

  • API 키
  • 데이터베이스 비밀번호
  • 토큰
  • 서버 주소
  • 개발·운영 환경 설정

장점은 다음과 같다.

장점설명

보안 코드가 공개돼도 비밀값 노출 위험 감소
환경 분리 개발·테스트·운영 값 변경이 쉬움
협업 각자 자신의 키를 사용할 수 있음
유지보수 코드를 수정하지 않고 설정 변경 가능

28. .env 파일

학습 단계에서는 환경변수 형태의 값을 .env 파일에 둘 수 있다.

예:

API_KEY=demo-key-1234

코드에는 실제 키를 직접 작성하지 않는다.


29. .env 읽기

오늘은 별도의 라이브러리를 사용하지 않고 파일 읽기와 split()을 이용한다.

api_key = None

with open(".env", encoding="utf-8") as f:

    for line in f:

        parts = line.strip().split("=", 1)

        if parts[0] == "API_KEY":
            api_key = parts[1]

이제 실제 키는 api_key 변수에 들어간다.


30. split("=", 1)이 중요한 이유

다음 코드를 보자.

line.split("=", 1)

뒤의 1은

처음 발견한 =에서 딱 한 번만 나눈다.

는 뜻이다.

예:

API_KEY=abc=123=xyz

다음처럼 나누면

line.split("=")

결과는

[
    "API_KEY",
    "abc",
    "123",
    "xyz"
]

가 된다.

API 키가 여러 조각으로 잘려 버린다.


31. split("=", 1)의 결과

line.split("=", 1)

을 사용하면

[
    "API_KEY",
    "abc=123=xyz"
]

처럼 첫 번째 =만 기준으로 나뉜다.

따라서 실제 비밀값 안에 =가 있어도 유지된다.


32. .env.example

실제 .env 파일은 비밀값이 있기 때문에 Git에 올리면 안 된다.

하지만 프로그램을 사용하는 사람은 어떤 환경변수가 필요한지 알아야 한다.

그래서 .env.example 파일을 만들 수 있다.

API_KEY=

실제 값은 넣지 않고 변수 이름만 알려 준다.


33. .env와 .env.example 비교

파일내용Git 업로드

.env 실제 API 키 하지 않음
.env.example 변수 이름만 가능

예:

.env

API_KEY=demo-key-1234

.env.example

API_KEY=

34. .gitignore

Git이 추적하지 않아야 하는 파일은 .gitignore에 적는다.

예:

.env

그러면 일반적인 Git 사용 흐름에서 .env가 버전 관리 대상에서 제외된다.

비밀값이나 불필요한 파일을 저장소에 올리지 않기 위해 사용하는 파일이다.


35. .gitignore 예시

.env
__pycache__/
*.log

예를 들어 이렇게 작성하면

  • .env
  • 파이썬 캐시 폴더
  • .log 파일

을 Git이 추적하지 않도록 설정할 수 있다.


36. fetch_data()

API 응답에는 우리가 사용하지 않는 값도 많이 포함될 수 있다.

그 응답 전체를 그대로 저장할 필요는 없다.

필요한 값만 골라 새 딕셔너리를 만든다.

def fetch_data(ip):

    data = call_with_retry(
        f"https://ipwho.is/{ip}"
    )

    if data and data["success"]:

        return {
            "ip": ip,
            "country": data["country"],
            "isp": data["connection"]["isp"]
        }

    else:
        return None

37. fetch_data()가 하는 일

함수의 흐름은 다음과 같다.

IP 입력
 ↓
call_with_retry()
 ↓
API 호출
 ↓
성공?
 ↓
필요한 값만 선택
 ↓
새 딕셔너리 반환

예를 들어 API가 매우 많은 데이터를 반환하더라도

{
    "ip": "8.8.8.8",
    "country": "United States",
    "isp": "Google LLC"
}

처럼 필요한 데이터만 남긴다.


38. if data and data["success"]

다음 조건을 보자.

if data and data["success"]:

두 가지를 확인한다.

data

data

API 호출 자체가 성공했는지를 확인한다.

모든 재시도가 실패했다면

None

이 들어 있을 수 있다.


data["success"]

HTTP 요청은 성공했어도 API 서비스 자체가 실패 결과를 반환하는 경우가 있다.

예를 들어 HTTP 상태코드가 200이어도 본문이 다음과 같을 수 있다.

{
  "success": false,
  "message": "invalid ip"
}

즉,

상태코드 200 = API 업무 처리도 무조건 성공

은 아니다.

따라서 서비스에서 제공하는 success 값도 확인해야 한다.


39. 상태코드 200인데 실패할 수 있다

HTTP 수준에서는 요청을 정상적으로 받았다는 뜻일 수 있다.

HTTP 200

하지만 API의 실제 처리 결과는 다음처럼 실패일 수 있다.

{
  "success": false
}

따라서 두 단계를 구분해야 한다.

HTTP 요청 성공?
      ↓
API 업무 처리 성공?

40. 필요한 필드만 골라 저장하는 이유

응답 전체를 저장하는 대신 필요한 값만 선택하면 다음 장점이 있다.

장점설명

단순성 데이터 구조가 작고 이해하기 쉬움
일관성 필요한 필드만 동일한 형태로 유지
저장 공간 불필요한 데이터 감소
유지보수 이후 코드가 필요한 필드만 알면 됨
보안 필요 없는 정보 저장을 줄일 수 있음

41. api_client.py 조립

오늘 만든 기능은 크게 세 부분으로 나눌 수 있다.

.env 읽기
     ↓
call_with_retry()
     ↓
fetch_data()
     ↓
JSON 저장

한 파일로 조립하면 api_client.py가 된다.


42. api_client.py 예시 구조

import json
import requests


# 1. API 키 읽기

api_key = None

with open(".env", encoding="utf-8") as f:

    for line in f:

        parts = line.strip().split("=", 1)

        if parts[0] == "API_KEY":
            api_key = parts[1]


# 2. 재시도 함수

def call_with_retry(url, tries=3):

    for i in range(tries):

        try:
            response = requests.get(
                url,
                timeout=5
            )

            response.raise_for_status()

            return response.json()

        except requests.RequestException:
            print(f"{i + 1}번째 요청 실패")

    return None


# 3. 필요한 데이터만 선택

def fetch_data(ip):

    data = call_with_retry(
        f"https://ipwho.is/{ip}"
    )

    if data and data["success"]:

        return {
            "ip": ip,
            "country": data["country"],
            "isp": data["connection"]["isp"]
        }

    return None


# 4. API 조회

result = fetch_data("8.8.8.8")


# 5. 결과 저장

if result:

    with open(
        "api_result.json",
        "w",
        encoding="utf-8"
    ) as f:

        json.dump(
            result,
            f,
            ensure_ascii=False,
            indent=2
        )

43. 전체 실행 흐름

.env
 ↓
API_KEY 읽기
 ↓
IP 입력
 ↓
fetch_data()
 ↓
call_with_retry()
 ↓
requests.get()
 ↓
timeout 적용
 ↓
raise_for_status()
 ↓
response.json()
 ↓
API success 확인
 ↓
필요한 값만 추출
 ↓
api_result.json

44. api_result.json

정상적으로 조회됐다면 다음과 같은 파일을 만들 수 있다.

{
  "ip": "8.8.8.8",
  "country": "United States",
  "isp": "Google LLC"
}

이제 프로그램을 종료해도 결과가 파일에 남는다.

다음 프로그램에서 다시 읽을 수도 있다.

with open(
    "api_result.json",
    encoding="utf-8"
) as f:

    data = json.load(f)

45. GET과 POST

오늘은 실제로 GET 요청을 사용했다.

GET과 POST의 핵심 차이는 데이터를 다루는 목적이다.

메서드주요 목적

GET 데이터를 가져온다
POST 데이터를 보내 새 데이터를 만들거나 처리한다

46. GET

GET은 조회할 때 사용한다.

예:

  • IP 정보 조회
  • 검색
  • 게시글 읽기
  • 사용자 정보 조회

일반적으로 조회 조건은 params로 전달한다.

requests.get(
    url,
    params={
        "keyword": "python"
    }
)

47. POST

POST는 서버에 데이터를 보내 새로운 작업을 만들거나 처리할 때 많이 사용한다.

예:

  • 회원가입
  • 게시글 작성
  • 티켓 생성
  • 주문 생성

보낼 데이터는 보통 요청 본문에 넣는다.

payload = {
    "title": "서버 오류",
    "priority": "high"
}

예를 들어 앞으로는 다음과 같은 형태를 사용할 수 있다.

requests.post(
    url,
    json=payload
)

오늘은 직접 POST 요청을 구현하지 않고 개념만 익혔다.


48. GET과 POST 비교

구분GETPOST

목적 조회 생성·제출
데이터 위치 주로 params 주로 json=payload
예 IP 조회 티켓 생성
반복 요청 일반적으로 같은 데이터를 다시 조회 같은 작업이 중복 생성될 수 있음

49. 오늘 나온 주요 문법

코드의미

requests.get(url) GET 요청을 보낸다
response.status_code HTTP 상태코드 확인
response.json() JSON 응답을 파이썬 값으로 변환
params={...} 조회 조건 전달
headers={...} 인증 정보 등 헤더 전달
timeout=5 최대 5초 기다림
raise_for_status() 4xx·5xx 응답을 예외로 변환
except requests.RequestException requests 관련 예외 처리
for i in range(tries) 지정한 횟수만큼 재시도
return None 모든 시도가 실패했음을 표시
split("=", 1) 첫 번째 =에서만 나눔
json.dump() 결과를 JSON 파일로 저장

50. 파일별 역할

오늘 결과물을 파일 기준으로 보면 다음과 같다.

project/
 ├─ api_client.py
 ├─ api_result.json
 ├─ .env
 ├─ .env.example
 └─ .gitignore

파일역할

api_client.py API 요청·재시도·데이터 가공
api_result.json API 조회 결과
.env 실제 비밀값 저장
.env.example 필요한 환경변수 이름 안내
.gitignore Git에서 제외할 파일 지정

51. 어제부터 오늘까지 연결하기

어제는 로그에서 의심스러운 IP를 탐지했다.

비정형 로그
 ↓
정규표현식
 ↓
정규화
 ↓
탐지 룰
 ↓
의심 IP 발견

하지만 여기까지는 우리 로그 안의 정보만 사용한 것이다.

오늘은 그 IP를 외부 서비스에 보내 추가 정보를 가져왔다.

의심 IP 발견
 ↓
requests.get()
 ↓
외부 IP API
 ↓
국가 / ISP 정보
 ↓
필요한 값 추출
 ↓
api_result.json

즉, 로그 분석 프로그램이 처음으로 외부 시스템과 연결되기 시작한 단계라고 볼 수 있다.


52. 실패까지 포함한 전체 흐름

실제 프로그램은 성공 경로만 생각해서는 안 된다.

API 요청
  ↓
응답이 왔는가?
 ┌──────┴──────┐
No            Yes
 ↓              ↓
timeout      HTTP 상태 확인
 ↓              ↓
재시도       4xx / 5xx?
 ↓           ┌─────┴─────┐
             Yes         No
              ↓           ↓
         예외 처리      JSON 읽기
              ↓           ↓
           재시도     API success 확인
                          ↓
                     필요한 값 추출
                          ↓
                       파일 저장

오늘은 오전만 수업을 하고 오후에는 특강을 듣는 시간입니당~~~!
남는 시간에 과제랑 자격증 공부도 좀 해야겠어용
그럼 아됴

+ Recent posts