<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ycseong07.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ycseong07.github.io/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-05-16T00:17:16+09:00</updated><id>https://ycseong07.github.io/feed.xml</id><title type="html">Moondb Blog</title><subtitle>개인 블로그입니다. 기술 노트, 생각, 서평 등을 남깁니다.
</subtitle><author><name>moondb</name><email>ycseong07@gmail.com</email></author><entry><title type="html">RAG vs Long Context</title><link href="https://ycseong07.github.io/2026/0415_rag-vs-long-context/" rel="alternate" type="text/html" title="RAG vs Long Context" /><published>2026-04-15T09:00:00+09:00</published><updated>2026-04-15T09:00:00+09:00</updated><id>https://ycseong07.github.io/2026/RAG-vs-Long-Context</id><content type="html" xml:base="https://ycseong07.github.io/2026/0415_rag-vs-long-context/"><![CDATA[<p>LLM 컨텍스트 윈도우가 커지고 있다. 2023년에는 GPT-3.5가 4K~8K, 2024년 초에는 GPT-4 Turbo가 128K, 2024년 Claude 3가 200K로 늘어났던 것이 바로 엊그제 같은데, 2026년 들어서는 Claude Opus와 Gemini가 1M~2M, Llama 4 Scout가 10M까지 광고하고 있는 상황이다.</p>

<p>이렇게 컨텍스트 윈도우가 커지자 ‘RAG is dead’와 같은 주장이 X와 기술 블로그 곳곳에서 다시 고개를 들고 있다. 사실 1M 토큰이면 단행본 한 권을 통째로 집어넣고도 자리가 남는다. 그런데 굳이 임베딩 모델을 돌리고, 벡터 DB를 운영하고, chunking 전략을 고민하고, retrieval 파이프라인을 디버깅하면서 RAG 스택을 통째로 들고 갈 이유가 있을까. 그냥 문서를 통째로 컨텍스트에 부어 넣고 모델에게 알아서 찾게 하는 편이 더 단순해 보일 수 있다. 그러나 실제로는 그렇게 간단히 문제가 풀리지는 않는다.</p>

<h1 id="rag가-풀던-문제">RAG가 풀던 문제</h1>

<p>RAG(Retrieval-Augmented Generation)는 LLM이 모르는 정보를 외부 지식으로 보충하는 가장 표준적인 패턴이다. 사내 문서, 최신 뉴스, 사용자의 개인 메모처럼 모델 학습 시점에 포함되지 않은 데이터를 다루려면 어떤 식으로든 모델 바깥에서 정보를 끌어와야 한다. 2023년경의 컨텍스트 윈도우로는 위키피디아 한 페이지조차 통째로 넣기 어려웠으니, 질문과 관련 있는 일부 조각만 추려서 모델에 전달하는 것이 거의 유일한 선택지였다.</p>

<p>이 시기 표준화된 구성은 비교적 단순하다. 문서를 일정 크기로 자르고(chunking), 각 조각을 임베딩 모델로 벡터화해 벡터 DB에 적재한다. 사용자가 질문을 하면 질문도 같은 임베딩 공간에 사상한 뒤, 코사인 유사도로 가까운 chunk 몇 개를 골라 와 프롬프트에 끼워 넣는다. 그 위에 키워드 검색(BM25)과 reranker를 얹은 하이브리드 구조까지가 2024~2025년 사이에 굳어진 형태다.</p>

<p>RAG가 풀어 준 문제는 명확하다. 용량이 부족한 컴퓨터에 외장하드를 연결하듯, 모델은 작아도 외부 지식은 임의로 키울 수 있고, 데이터가 바뀌면 인덱스만 갱신하면 된다. 답변에 출처를 붙일 수 있어 hallucination을 그나마 통제할 수 있고, 토큰 비용도 필요한 chunk만 보내니 비교적 저렴하다. ‘LLM에 회사 데이터 붙이고 싶다’는 요구의 90%는 이 패턴으로 풀렸고, 그래서 한동안 LLM 애플리케이션의 표준이 됐다.</p>

<h1 id="llm-모델의-발전">LLM 모델의 발전</h1>

<p>그런데 2025년 이후 모델의 컨텍스트 크기가 커지고, 토큰 비용이 저렴해지면서 RAG의 필요성이 흔들리기 시작했다. Gemini 1.5 Pro가 1M을 들고 나오고, Claude가 200K에서 1M으로 확장하고, 일부 오픈 모델은 10M을 광고하기 시작했다. 동시에 토큰 단가가 떨어졌다. Gemini 1.5 Flash 기준 입력 1M 토큰이 약 $0.075 수준이고, 프롬프트 캐싱이 보편화되면서 같은 컨텍스트를 반복해서 보내는 비용도 한 자릿수 분의 1로 줄었다. 200K 토큰을 한 번 보내고 그 위에서 여러 번 질문하는 구성이라면, 두 번째 호출부터는 거의 무료에 가깝게 굴러간다.</p>

<p>또한, Claude Code, Codex, Cursor 같은 코딩 에이전트가 보여 준 패턴은 RAG의 대안이 될 수 있음을 보여주었다. 이들은 벡터 DB를 쓰지 않고, 대신 ripgrep으로 코드베이스를 lexical 검색하고, 결과를 모델 컨텍스트에 그대로 넣는다. 임베딩이 필요 없고, 인덱스도 필요 없으며, 파일이 바뀌면 다음 grep이 곧바로 새 결과를 가져온다. 이 단순한 구조가 실제로 잘 동작한다는 사실이 알려지자, ‘RAG라는 인프라를 짊어질 필요가 정말 있는가’라는 질문이 다시 진지하게 던져지기 시작했다.</p>

<p>2023년 정도만 하더라도 모델이 비싸고 작아서, 똑똑한 retriever로 정밀하게 골라 줘야 했다. 그런데 2026년에는 모델이 싸고 크다. 그러면 retriever는 오히려 단순한 편이 낫다. 정밀도가 떨어지더라도 recall만 충분히 높으면, 나머지 필터링은 모델 자신이 컨텍스트 안에서 처리한다. 임베딩 기반 검색이 흔히 일으키는 문제(가까운 듯 보이지만 실제로는 무관한 문서가 잡히는 false neighbor, chunk 경계가 표나 함수 정의를 끊어 놓는 destructive chunking, 검색이 왜 실패했는지조차 알기 어려운 opaque failure 등)가 grep + long context 구성에서는 대부분 사라진다.</p>

<h1 id="그럼에도-rag가-필요한-이유">그럼에도 RAG가 필요한 이유</h1>

<p>하지만 <a href="https://www.databricks.com/blog/long-context-rag-performance-llms">Databricks가 2024년 말에 13개 LLM을 대상으로 2,000회 이상 진행한 long context 실험</a>은 모델별 실패 양상을 꽤 디테일하게 짚는다. GPT-4o와 Claude 3.5 Sonnet은 긴 컨텍스트에서도 비교적 잘 버티지만, Llama 3.1 405B는 32K를 넘기면서 정확도가 떨어지기 시작한다. Claude 3 Sonnet은 64K 부근에서 답을 거부하는 비율이 3.7%에서 49.5%까지 뛰고, DBRX-instruct는 32K에서 답하는 대신 컨텍스트를 요약해 버리는 빈도가 50%를 넘는다. 컨텍스트 윈도우가 광고된 크기라고 해서 그 안에서 실제 추론 품질이 유지되는 게 아니라는 뜻이다.</p>

<p>이전 글에서도 언급했지만, Claude Opus의 1M 컨텍스트도 실사용에서는 300K 부근부터 결과 품질이 빠르게 무너진다. ‘Lost in the Middle’이라는 이름으로 알려진 현상도 같은 맥락이다. 관련 정보가 컨텍스트의 시작이나 끝이 아니라 중간에 위치하면, 같은 모델이 같은 질문에서 10~25%까지 정확도가 떨어진다는 보고가 여러 벤치마크에서 일관되게 나온다. 자기회귀 트랜스포머 구조에서 어쩌면 자연스러운 결과지만, ‘컨텍스트에 다 부어 넣으면 된다’는 가정이 그대로 통하지는 않는다는 점은 분명하다.</p>

<p>비용과 지연시간 문제도 남는다. 단일 호출의 토큰 단가가 떨어졌다고 해도, 1M 토큰을 매 요청마다 보내면 누적 비용은 빠르게 커진다. 실제 운영 사례에서 보면, 같은 질문 패턴을 RAG로 처리할 때와 long context로 처리할 때의 비용 차이가 1,000배 단위로 벌어지는 경우가 흔하다. 응답 시간도 마찬가지다. 200K~1M 토큰을 매번 처리하면 첫 토큰이 나오기까지 수십 초가 걸리는데, 챗봇이나 검색형 인터페이스에서는 사용자가 기다려 주지 않는다. 사내 정책 Q&amp;A처럼 한 번에 한 질문이면 모를까, 트래픽이 많은 서비스에서는 retrieval로 컨텍스트를 줄여 두는 편이 거의 항상 유리하다.</p>

<p>데이터 규모와 신선도 문제도 짚어야 한다. 1M 토큰이 단행본 한 권 분량이라고 해도, 실제 엔터프라이즈 지식 베이스는 테라바이트 단위인 경우가 보통이다. 컨텍스트 윈도우가 10M으로 늘어나도 전체의 1%를 못 본다. 데이터가 분 단위로 바뀌는 도메인에서는 매번 데이터를 컨텍스트에 다시 부어 넣는 것보다 인덱스를 갱신하는 편이 자연스럽다. 컴플라이언스나 감사 로그처럼 ‘어느 문서에서 이 답을 끌어왔는가’가 답변 자체보다 중요한 경우에도 retrieval 단계의 구분이 필요하다.</p>

<p>이러한 흐름의 결과로, 2025년 말부터 자리를 잡고 있는 패턴이 ‘Retrieval-Augmented Long Context’다. RAG와 long context를 경쟁 관계가 아니라 직렬로 잇는 구성이다. 거대한 코퍼스에서 retrieval 단계가 50K~300K 토큰 정도의 후보를 골라낸다. 이때 retrieval은 굳이 정밀할 필요가 없다. recall 위주로 후보를 넉넉히 잡고, 다음 단계의 long-context 모델이 그 안에서 진짜 관련 정보를 골라 추론한다. retrieval 쪽에서 BM25 같은 lexical 검색만 써도 큰 문제가 없는 경우가 많다. BEIR 벤치마크에서도 BM25가 zero-shot 환경에서 여전히 최상위권 임베딩 모델과 경쟁한다는 결과가 꾸준히 나오고 있다.</p>

<p>retrieval과 long context를 함께 쓸 때 자주 거론되는 보완책 중 하나가 Anthropic이 2024년에 공개한 <a href="https://www.anthropic.com/news/contextual-retrieval">Contextual Retrieval</a>이다. chunk를 그대로 임베딩하지 않고, 각 chunk를 원본 문서 전체의 맥락 안에서 한 번 요약/리라이팅한 뒤 임베딩한다. chunk 경계 문제로 retrieval이 헛다리를 짚는 비율을 35~50% 정도 줄였다는 보고가 함께 따라붙었다. lexical과 vector를 같이 쓰고, 마지막에 reranker를 한 단계 더 끼우는 구성도 같은 맥락의 보강이다.</p>

<p><a href="https://arxiv.org/abs/2407.16833">Google DeepMind가 EMNLP 2024에서 발표한 비교 평가</a>와 <a href="https://arxiv.org/abs/2502.09977">LaRA(ICML 2025)</a>의 결론도 이 그림과 잘 맞는다. 자원이 충분할 때 long context가 평균 품질에서 RAG를 앞서지만, 토큰 효율과 지연시간을 함께 보면 RAG가 여전히 유리하고, 어떤 쪽이 더 낫다고 단언하기 어려울 만큼 태스크와 모델에 따라 결과가 갈린다는 것이다.</p>

<h1 id="정리">정리</h1>

<p>적어도 아직까지는 RAG와 long context는 병행하여 사용되어야 하고, 그 비중은 아래의 기준들에 의해 결정된다고 보는 것이 현실적인 듯 하다.</p>

<p>1) 데이터 규모: 컨텍스트 윈도우 안에 들어갈 수 있는 분량이라면 long context로 단순화하는 편이 거의 항상 좋다. 인프라 부담이 사라지고, chunking 경계 문제도 사라진다. 사내 위키 한 섹션, 단일 제품 매뉴얼, 한 프로젝트의 코드베이스 정도라면 retrieval 없이 통째로 부어 넣고 시작해도 무방하다.</p>

<p>2) 호출 빈도와 지연 요건: 같은 컨텍스트 위에서 사용자 질문이 반복되는 구조라면 prompt caching과 long context의 조합이 유리하다. 반대로 매 요청마다 다른 도메인의 다른 데이터가 필요한 고트래픽 서비스라면, retrieval로 컨텍스트를 줄여 두는 편이 비용과 응답 시간 모두에서 유리하다.</p>

<p>3) 데이터가 만들어진 시점과 출처 추적: 데이터가 자주 바뀌고, 답변에 어느 문서를 참조했는지 명시해야 하는 도메인 — 법률, 금융, 의료, 보안 — 에서는 retrieval 단계 자체가 빠지기 어렵다. long context로 통째로 처리해도 출처를 모델 출력으로부터 사후 추적해야 하는데, 이 과정 자체가 retrieval과 비슷한 일을 한 번 더 하는 것이 된다.</p>

<p>4) retrieval 자체의 단순화 여부: 굳이 벡터 DB를 새로 세우지 않더라도, 파일 시스템 grep, SQLite FTS5, PostgreSQL의 full-text search만으로 충분한 경우가 많다. ‘RAG를 쓴다’와 ‘벡터 DB를 운영한다’는 같은 의미가 아니다. 코딩 에이전트들이 grep만으로 잘 돌아가는 것을 보면, 어떤 종류의 retrieval이 자기 도메인에 적합한지를 다시 점검해 보는 편이 좋다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="AI" /><category term="RAG" /><category term="LLM" /><category term="컨텍스트윈도우" /><summary type="html"><![CDATA[LLM 컨텍스트 윈도우가 커지고 있다. 2023년에는 GPT-3.5가 4K~8K, 2024년 초에는 GPT-4 Turbo가 128K, 2024년 Claude 3가 200K로 늘어났던 것이 바로 엊그제 같은데, 2026년 들어서는 Claude Opus와 Gemini가 1M~2M, Llama 4 Scout가 10M까지 광고하고 있는 상황이다.]]></summary></entry><entry><title type="html">에이전트, 그리고 엔지니어링</title><link href="https://ycseong07.github.io/2026/0402_%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81/" rel="alternate" type="text/html" title="에이전트, 그리고 엔지니어링" /><published>2026-04-02T09:00:00+09:00</published><updated>2026-04-02T09:00:00+09:00</updated><id>https://ycseong07.github.io/2026/%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81</id><content type="html" xml:base="https://ycseong07.github.io/2026/0402_%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81/"><![CDATA[<p>LLM을 다루는 일을 하다 보면, 같은 문제를 두고도 부르는 이름이 1~2년마다 바뀐다는 인상을 받게 된다. 2022년의 ‘프롬프트 엔지니어링’은 2024~2025년에 ‘컨텍스트 엔지니어링’이 되었고, 2026년에 들어서면서는 ‘하네스 엔지니어링(Harness Engineering)’이라는 표현이 빠르게 자리를 잡고 있다.</p>

<p>AI 분야가 빠르게 발전하면서, 사실 새 모델이 자꾸 나오는 것 만큼이나 개발자들을 가장 피곤하게 만드는 건, 명확히 정의되지 않은 용어들이 쏟아져 나와 막연한 채로 통용되는 일이다. 같은 단어를 다르게 이해하는 사람들이 많아지면서, 같은 단어를 사용하는데도 소통이 이상하게 엇나가는 경우도 많았다.</p>

<p>특히 ‘에이전트(Agent)’는 용어 정의가 명확하지 않았었지만, 지금까지 그 의미가 가장 많이 좁혀지기도 한 단어 중 하나다. 그래서 이 글에서는 먼저 에이전트를 어떻게 정의해야 하는지 정리하고, 그 위에서 프롬프트 엔지니어링, 컨텍스트 엔지니어링, 하네스 엔지니어링에 대해 차례로 정리한다.</p>

<h1 id="agent">Agent</h1>

<p>‘에이전트’라는 단어는 LLM 시대에 들어 의미가 가장 빠르게 변한 표현 중 하나다.</p>

<p>2023년경 학술 논문들에서 ‘agent’는 지금보다 훨씬 가볍게 쓰였다. 한 가지 예가 Stanford의 <a href="https://arxiv.org/abs/2304.03442">Generative Agents: Interactive Simulacra of Human Behavior (Park et al., 2023)</a>이다. 이 논문은 25명의 캐릭터에게 각자의 페르소나, 기억, 계획을 부여한 시뮬라크라를 ‘generative agent’라고 불렀다. 즉, LLM에 정체성과 메모리만 부여하면 에이전트라는 식의 비교적 느슨한 정의가 통용되던 시기다. 즉, 페르소나가 입혀진 LLM 인스턴스 정도로 정의된 것이다.</p>

<p>‘agent’의 정의가 지금과 같이 좁혀진 것은 2024년 12월 <a href="https://www.anthropic.com/research/building-effective-agents">Anthropic의 “Building Effective Agents”</a> 글을 기점으로 한다. 이 글의 정의를 그대로 옮기면 이렇다.</p>

<blockquote>
  <p><em>“Agents are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks.”</em></p>

  <p><em>“Workflows are systems where LLMs and tools are orchestrated through predefined code paths.”</em></p>
</blockquote>

<p>여기서 제안한 두 가지 키워드는 도구 사용(코드 실행, API 호출, 외부 시스템에 영향을 주는 행위를 할 수 있다는 것), 그리고 동적 제어 흐름(다음 단계를 사람이나 미리 정해진 코드가 아니라 모델 자신이 결정한다는 것)이다. 이 정의에 따르면 페르소나를 부여했을 뿐인 LLM은 더 이상 에이전트가 아니다. 에이전트라고 부르려면 외부 세계에 영향을 줄 수 있는 도구를 갖고, 그 도구를 언제 어떻게 쓸지 모델이 스스로 정하며, 관찰 - 추론 - 행동 - 다시 관찰의 루프(agentic loop) 안에서 동작해야 한다.</p>

<p>Anthropic은 한발 더 나아가 워크플로우(고정된 경로)와 에이전트(동적 경로)를 자율성의 스펙트럼으로 본다. 모든 작업을 풀 자율 에이전트로 풀 필요는 없고, 위험과 가치에 맞춰 자율성 수준을 고르는 것 자체가 시스템 설계자의 일이라는 뜻이다. 자율성이 높아질수록 위험도 함께 커진다. 페르소나가 입혀진 챗봇이 잘못된 답을 하면 사용자에게 잘못된 정보가 전달될 뿐이지만, 도구를 쥔 자율 에이전트가 잘못 행동하면 DB 테이블이 날아가거나, 잘못된 PR이 머지되거나, 외부 API에 비용이 청구된다. 그래서 ‘에이전트’가 “페르소나가 있는 LLM” 정도였던 시절에는 큰 안전장치 없이도 다룰 수 있었지만, 현재 정의의 에이전트를 다루려면 후술할 하네스 엔지니어링이 거의 필수가 된다.</p>

<h1 id="프롬프트-엔지니어링">프롬프트 엔지니어링</h1>

<p>Copilot, ChatGPT 같은 LLM 서비스가 출시되었을 때, 사용자들은 같은 질문이라도 어떻게 묻느냐에 따라 답변 품질이 크게 달라진다는 것을 알게 되었다. 여기에서 프롬프트 엔지니어링이 출발했고, 이는 LLM에게 원하는 결과를 얻기 위해 입력 문장을 설계하고 최적화하는 기술로 정의되었다.</p>

<p>당시 빠르게 표준이 된 유명한 기법으로는 페르소나 부여하기, Chain of Thought, ReAct 등이 있다. 특히 ReAct 논문에서는 추론과 행동을 번갈아 수행하게 만드는 구조를 제안했는데, 모델이 모르면 검색해서 확인할 수 있게 만든 이 패턴이 오늘날 Claude Code, Codex, Cursor 같은 모든 코딩 에이전트의 기반이 된다고 볼 수 있다. 다만 프롬프트 엔지니어링은 복잡한 문제를 해결하려면 긴 컨텍스트를 가진 프롬프트가 필요했고, 대화 턴을 오래 지속해야 한다는 문제에 맞닥뜨리게 되었다.</p>

<h1 id="컨텍스트-엔지니어링">컨텍스트 엔지니어링</h1>

<p>대화가 길어지거나 도구 호출 결과가 쌓이면 컨텍스트 윈도우가 가득 차고, 가득 찬 컨텍스트는 오히려 모델 성능을 떨어뜨린다. 대화 기록 전체가 매 턴마다 다시 모델에 입력되는 구조 때문에 입력이 길어질수록 중요한 정보가 흐려지는 현상도 자주 관찰됐다.</p>

<p>2025년 중반, 이 문제를 한 단어로 묶은 표현이 <a href="https://x.com/karpathy/status/1937902205765607626">Andrej Karpathy의 트윗</a>을 통해 널리 퍼졌다.</p>

<blockquote>
  <p><em>“+1 for ‘context engineering’ over ‘prompt engineering’. (…) Context engineering is the delicate art and science of filling the context window with just the right information for the next step.”</em></p>
</blockquote>

<p>비슷한 시기 Shopify CEO Tobi Lütke 역시 “LLM에 필요한 모든 맥락을 제공하는 일”이라는 표현으로 같은 개념을 강조했고, 두 사람의 영향력 덕분에 이 용어는 빠르게 표준어가 됐다. 이때부터 엔지니어의 역할은 운영체제처럼 이번 턴에 필요한 데이터와 코드만 RAM에 올려놓는 것으로 점점 변하기 시작했다. 관심의 단위가 문장에서 시스템 전체의 입력 파이프라인으로 확장된 것이다.</p>

<p>이 관점에서, 시스템 프롬프트, 채팅 히스토리, 장기 기억(memory), RAG로 끌어오는 외부 문서, MCP로 연결된 도구의 출력, 그리고 길어진 컨텍스트를 줄여서 다시 끼워넣는 압축(compaction)까지가 모두 같은 층위에서 컨텍스트의 일부로 다뤄지기 시작한다.</p>

<p>다만, 컨텍스트는 무조건 많이 넣을수록 좋은 게 아니다. 2026년 들어 Claude Opus가 1M까지 컨텍스트 윈도우를 늘렸지만, 실제로 사용해보면 약 300K를 넘어가면 결과물 품질이 빠르게 무너진다. 이건 트랜스포머 모델의의 자기회귀 구조상 어쩌면 당연한 일이기도 하다. 입력이 길어질수록 매 토큰마다 참조해야 할 양이 늘어나고, 연산량이 늘면 오류 확률도 함께 누적된다. Claude Code가 컨텍스트의 95%쯤이 차면 자동 압축을 거는 것도 같은 이유다. 실제로는 좀 더 일찍 압축하고 진행하는 것이 더 좋은 결과를 냈다.</p>

<p>그래서 실전에서는 “무엇을 처음부터 컨텍스트에 넣고, 무엇은 필요할 때 도구로 끌어올 것인가”를 나누는 일이 핵심 작업이 된다. 작업 목표와 제약, 코딩 컨벤션, 최근 아키텍처 변경, 절대 하면 안 되는 행동(보안 폴더 접근 금지 등)은 처음부터 주입할 필요가 있다. 반대로 디테일한 코드 본문, 대용량 로그, 외부 API 레퍼런스, 테스트 실행 결과 같은 것은 처음부터 들고 갈 필요가 없다. 모델이 필요하다고 판단할 때 도구로 가져오게 두는 편이 컨텍스트 예산을 훨씬 효율적으로 쓴다.</p>

<p>이 원칙은 OpenAI가 2026년 2월에 공개한 코딩 에이전트 회고에도 그대로 등장한다. 처음에는 하나의 큰 <code class="language-plaintext highlighter-rouge">AGENTS.md</code>에 모든 지침을 박아넣었는데, 분량이 늘어나자 모델이 앞부분만 대충 읽고 넘어가는 문제가 있었다. 결국 그들은 <code class="language-plaintext highlighter-rouge">AGENTS.md</code>를 책의 목차처럼 가벼운 진입점으로만 두고, 세부 규칙은 <code class="language-plaintext highlighter-rouge">docs/</code> 하위에 architecture, product-spec, references, execution-plans, security 같은 파일로 흩어 놓아 모델이 필요할 때 점진적으로 펼쳐 보도록 구조를 바꿨다. 컨텍스트 엔지니어링이 “많이 넣기”가 아니라 “필요할 때만 펼쳐지도록 설계하기”에 가깝다는 점을 잘 보여주는 사례다.</p>

<p>다만 컨텍스트를 잘 짜도 해결되지 않는 영역이 남는다. 컨텍스트 엔지니어링은 한 세션이 모델에게 주어지는 시점까지를 다루지만, 에이전트가 도구를 들고 자율적으로 움직이는 동안 발생하는 일탈, 실수를 잡지는 못한다. 그렇다면 문제는, 환경 전체를 어떻게 짜야 에이전트가 안전하게 돌아갈까로 옮겨가게 된다.</p>

<h1 id="하네스-엔지니어링">하네스 엔지니어링</h1>

<p>‘하네스(harness)’는 원래 마차를 끄는 말에 씌우던 안장, 고삐, 굴레를 가리키는 단어다. 방향을 지시하고, 실행 능력을 부여하고, 통제 범위를 강제하는 장치 일체. LLM 맥락에서 이 단어가 자리를 잡은 것은 2026년 2월, HashiCorp 공동 창업자이자 Terraform, Ghostty 개발자인 <a href="https://mitchellh.com/writing/my-ai-adoption-journey">Mitchell Hashimoto의 블로그 글</a>에서다. 그가 제시한 공식은 단순하고 아름답다.</p>

<blockquote>
  <p>Agent = Model + Harness (편안..)</p>
</blockquote>

<p>핵심 아이디어는 이렇다. 에이전트가 실수를 할 때마다, 같은 실수를 다시 하지 못하도록 환경을 영구적으로 고친다. 모델 자체의 업그레이드를 기다리는 게 아니라, 에이전트를 둘러싼 작업 환경(harness)을 한 단계씩 다듬어 가는 것이다. 그러니까 ‘에이전트’를 만드는 일은 사실 모델을 다루는 일이 아니라, 모델 바깥의 모든 것 — 컨텍스트, 도구, 실행 루프, 검증, 사람의 개입 지점 — 을 통합 설계하는 일이다.</p>

<p>이 흐름이 부각된 데는 비슷한 시기 OpenAI가 “5개월간 사람이 코드 한 줄도 안 치고 에이전트만으로 서비스를 만들었다”는 회고를 공개한 영향도 컸다. 실제 사례가 나오자 “과연 어디까지 가능한가”에 대한 막연함이 “이렇게 하면 된다”의 구체적 패턴들로 바뀌기 시작했다. Anthropic은 더 직접적인 비교 실험도 공개했다. 동일 작업을 하네스 없이 단일 에이전트에게 맡겼을 때는 약 9달러를 쓰며 20분간 동작했지만 결과물은 실행조차 되지 않는 코드였고, 멀티 에이전트와 잘 짜인 하네스를 결합한 구성에서는 200달러를 쓰고 6시간 동안 16개 기능을 만들어 실제로 플레이 가능한 게임을 완성했다. 비용은 늘지만 신뢰도가 비약적으로 올라간다는 점이 핵심이다.</p>

<p>이런 변화의 배경에는 모델 속도와 사람의 검토 속도의 격차가 있다. 모델이 코드를 만들어내는 속도는 빠른데, 매번 사람이 결과물을 열어보고 피드백을 주는 워크플로는 사람이 먼저 지친다. 그래서 자율 루프를 안전하게 돌릴 수 있도록 환경 자체를 깎는 일 — 즉 하네스를 짜는 일이 개발자의 핵심 업무가 되어 가고 있다. 과거에는 “직접 코드를 잘 쓰는 사람”이 좋은 개발자였다면, 이제는 “에이전트가 잘 돌아가는 환경을 잘 설계하는 사람”이 그 자리를 대체하고 있다.</p>

<p>하네스를 구성하는 축은 보통 셋으로 나뉜다. 컨텍스트, 도구, 평가다.</p>

<p>컨텍스트는 앞 섹션의 컨텍스트 엔지니어링의 정의를 그대로 가져오면 된다. 도구 측면에서는 모델에게 어떤 기본 도구를 허용하고, 어떤 도구를 별도로 쥐어줄지를 정한다. 기본 웹 검색 같은 도구는 성능이 아쉬워서, Tavily나 Exa Search 같은 외부 검색을 MCP로 붙여 쓰는 경우가 많고, 이때 도구간 기능 오버랩을 줄이지 않으면 모델이 잘못된 도구를 골라 호출하는 일이 잦다. 도구가 내는 에러 메시지는 가능하면 모델 컨텍스트로 그대로 알려줘야 모델이 다음 행동을 잘 결정할 수 있다. 평가는 ‘단지 현재와 다르게만 수정해 개선하는 것’을 거부한다는 원칙이다. 테스트 통과율, 린트, 빌드, 스크린샷 비교처럼 측정 가능한 신호를 통해서만 개선 여부를 판단한다. 그래야 작성-검증-재시도 루프를 자동화할 수 있다.</p>

<p>다만 같은 모델이 같은 세션 안에서 자기 결과물을 평가하면 거의 항상 “잘 짰다”고 평가한다는 문제가 있다. 그래서 보통은 작성자와 검증자를 다른 모델 또는 다른 세션으로 분리하는 패턴을 쓴다. 예를 들어 계획 수립은 Codex가, 1차 코드는 Claude, 1차 검증과 수정은 Codex .. 와 같이 작업단위를 교차로 평가하는 식이다.</p>

<p>하네스를 실제로 어떻게 구성하는지를 보면 패턴이 더 구체적으로 보인다. 가장 자주 보이는 격리 방식은 git worktree다. 메인 브랜치를 직접 건드리지 않고 별도의 워크트리에서 에이전트가 코드를 작성, 테스트하게 한 뒤, 검증을 통과한 결과만 메인으로 머지한다. 자율 실행 중에 사고가 나도 메인이 깨지지 않으니 사람이 끼어들 일이 줄어든다. 두 번째는 실행 계획을 마크다운 파일로 강제로 남기게 하는 방법이다. 코드는 어차피 git에 남지만 “왜 이렇게 짰는지의 계획”은 대화 세션에만 남고, 세션이 끝나면 사라진다. 회고할 수 있도록 계획 자체를 파일로 떨어뜨려 둬야 한다. 세 번째는 훅(hook)이다. 아무리 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>나 <code class="language-plaintext highlighter-rouge">AGENTS.md</code>에 “마스터 브랜치 직접 수정 금지” “테스트 없는 커밋 금지”라고 적어둬도, 모델이 무시하는 경우가 있다. 그래서 진짜 지켜져야 하는 규칙은 git pre-commit hook(예: Husky)이나 Claude Code의 hooks처럼 코드 레벨 게이트로 박아둔다. 마스터 브랜치 직접 수정은 hook이 차단하고, 테스트가 없는 변경은 커밋 자체가 거절되며, lint/build/테스트 통과가 커밋 전 강제로 실행된다. AI의 판단이 아니라 프로그래밍 로직으로 통과해야만 다음 단계로 갈 수 있도록 만드는 것이다.</p>

<p>엔지니어링 방식이 변화하면서 로그를 다루는 관점도 함께 바뀌었다. 예전엔 “쓸데없는 print 지워라”가 시니어의 기본 잔소리였지만, 지금은 에이전트가 자기 행동을 되짚어볼 수 있도록 충분한 로그를 남기는 쪽으로 가치가 역전됐다. 로그, 스크린샷, 에러 트레이스가 곧 에이전트의 ‘눈’이고, 같은 작업을 다시 시도할 때 무엇이 잘못됐는지를 모델이 직접 읽고 수정할 수 있게 된다.</p>

<p>하네스의 구성 단위를 코딩 에이전트(Claude Code 기준)에서 자주 쓰이는 네 도구로 매핑하면 이렇다. Commands는 자주 쓰는 프롬프트를 슬래시 커맨드로 모듈화하는 가장 가벼운 단계다. Rules(<code class="language-plaintext highlighter-rouge">CLAUDE.md</code>, <code class="language-plaintext highlighter-rouge">AGENTS.md</code> 등)는 매 세션에서 모델이 읽는 컨벤션 모음인데, 이건 유도이지 강제가 아니다. Skills는 예시, 템플릿, 스크립트를 패키지로 묶은 워크플로 단위로, 단일 프롬프트로 끝나지 않는 작업을 모듈화한다. 마지막으로 Hook은 코드 레벨에서 강제되는 게이트 역할을 한다. SuperClaude나 oh-my-claude-code 같은 잘 만든 프레임워크를 가져다 쓰는 것도 한 방법이지만, 팀마다 코딩 컨벤션과 워크플로가 달라서 보통은 자기 환경에 맞게 직접 깎아 가는 편이 잘 맞는다. 또한, 모델이 개선되면 기존 하네스의 일부는 필요없어지기도 한다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="AI" /><category term="LLM" /><category term="프롬프트엔지니어링" /><category term="컨텍스트엔지니어링" /><category term="하네스엔지니어링" /><summary type="html"><![CDATA[LLM을 다루는 일을 하다 보면, 같은 문제를 두고도 부르는 이름이 1~2년마다 바뀐다는 인상을 받게 된다. 2022년의 ‘프롬프트 엔지니어링’은 2024~2025년에 ‘컨텍스트 엔지니어링’이 되었고, 2026년에 들어서면서는 ‘하네스 엔지니어링(Harness Engineering)’이라는 표현이 빠르게 자리를 잡고 있다.]]></summary></entry><entry><title type="html">.claude/settings.json 퍼미션 시스템 이해하기</title><link href="https://ycseong07.github.io/2026/0321_claude-settings-json-%ED%8D%BC%EB%AF%B8%EC%85%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0/" rel="alternate" type="text/html" title=".claude/settings.json 퍼미션 시스템 이해하기" /><published>2026-03-21T09:00:00+09:00</published><updated>2026-03-21T09:00:00+09:00</updated><id>https://ycseong07.github.io/2026/claude-settings-json-%ED%8D%BC%EB%AF%B8%EC%85%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</id><content type="html" xml:base="https://ycseong07.github.io/2026/0321_claude-settings-json-%ED%8D%BC%EB%AF%B8%EC%85%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0/"><![CDATA[<p>Claude Code를 사용가는 거의 모든 사용자들이 맞닥뜨리는 문제가 있다. 처음에는 모든 도구 호출을 일일이 승인하다가, 같은 명령을 몇 번쯤 허용하고 나면 ‘Allow always’를 누르게 되고, 결국에는 <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code> 플래그를 검색하기 시작한다. 그리고 며칠 뒤, 에이전트가 잘못된 디렉토리에서 <code class="language-plaintext highlighter-rouge">git reset --hard</code>를 돌리거나, 운영 DB에 붙은 클라이언트로 마이그레이션을 다시 실행하는 사고가 한 번쯤 난다.</p>

<p>이 흐름의 본질은 퍼미션 시스템을 ‘귀찮은 게이트’로 다루는 데 있다. 이를 해결할 수 있는 파일이 Claude Code의 <code class="language-plaintext highlighter-rouge">settings.json</code> 파일이다. 이 파일은 어떤 도구 호출을 자동으로 허용하고, 어떤 호출은 매번 묻고, 어떤 호출은 아예 막을지를 선언해두는 정책 파일이라고 볼 수 있다. 이 글에서는 <code class="language-plaintext highlighter-rouge">settings.json</code>의 위치와 우선순위, 퍼미션 모드, 룰 작성법, 그리고 실전에서 자주 쓰는 패턴을 정리한다.</p>

<h1 id="settingsjson-위치">settings.json 위치</h1>

<p>Claude Code는 설정을 한 파일에 모아두지 않고 네 위치에 분산한다. 각각이 다른 신뢰 경계를 표현하기 때문이다.</p>

<p>가장 위에는 엔터프라이즈 정책 파일이 있다. macOS는 <code class="language-plaintext highlighter-rouge">/Library/Application Support/ClaudeCode/managed-settings.json</code>, Linux/WSL은 <code class="language-plaintext highlighter-rouge">/etc/claude-code/managed-settings.json</code>에 위치하고, 사용자가 임의로 풀 수 없는 조직 차원의 제약을 박아두는 자리다. 특정 명령을 무조건 막아야 한다면 여기에 들어간다.</p>

<p>그 다음이 사용자 설정인 <code class="language-plaintext highlighter-rouge">~/.claude/settings.json</code>이다. 모든 프로젝트에 공통으로 적용되는 개인 환경설정이 들어간다. 자주 쓰는 패키지 매니저 명령이나 git read-only 명령처럼, 어느 프로젝트에서 켜도 어차피 허용할 만한 것들이 이 자리에 들어가야 자연스럽다.</p>

<p>프로젝트 단위로는 두 파일이 있다. <code class="language-plaintext highlighter-rouge">.claude/settings.json</code>은 팀과 공유되도록 git에 커밋하는 파일이고, <code class="language-plaintext highlighter-rouge">.claude/settings.local.json</code>은 개인 환경에만 남기고 싶은 설정을 두는 파일이다. 후자는 기본 <code class="language-plaintext highlighter-rouge">.gitignore</code>에 자동으로 추가된다. 팀이 공유해야 할 코딩 컨벤션 관련 허용 규칙은 전자에, 본인 머신에만 있는 비밀키 경로나 사이드 도구 같은 건 후자에 두면 충돌이 줄어든다.</p>

<p>우선순위는 엔터프라이즈 &gt; 명령행 인자 &gt; <code class="language-plaintext highlighter-rouge">.claude/settings.local.json</code> &gt; <code class="language-plaintext highlighter-rouge">.claude/settings.json</code> &gt; <code class="language-plaintext highlighter-rouge">~/.claude/settings.json</code> 순이다. 같은 키가 여러 파일에 있다면 위 순서대로 덮어 쓴다. 다만 퍼미션 룰만큼은 단순한 덮어쓰기가 아니라 누적된다. <code class="language-plaintext highlighter-rouge">deny</code>에 한 번이라도 걸리면 어느 레이어에서든 막히고, <code class="language-plaintext highlighter-rouge">allow</code>는 모든 레이어의 합집합으로 동작하는 식이다. 즉 상위 레이어에서 <code class="language-plaintext highlighter-rouge">deny</code>로 박아둔 규칙을 하위 레이어가 <code class="language-plaintext highlighter-rouge">allow</code>로 풀어줄 수는 없다.</p>

<h1 id="퍼미션-모드">퍼미션 모드</h1>

<p><code class="language-plaintext highlighter-rouge">settings.json</code>의 <code class="language-plaintext highlighter-rouge">permissions.defaultMode</code> 키가 세션의 기본 모드를 결정한다. CLI에서 <code class="language-plaintext highlighter-rouge">--permission-mode</code>로 한 번만 다르게 띄울 수도 있고, 세션 안에서는 Shift+Tab으로 plan 모드와 acceptEdits 모드를 토글할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">default</code>는 가장 보수적인 모드다. 도구 호출 중 <code class="language-plaintext highlighter-rouge">allow</code>로 명시되지 않은 것이 나오면 사용자에게 매번 묻는다. 처음 프로젝트를 들여다볼 때 적합하다.</p>

<p><code class="language-plaintext highlighter-rouge">acceptEdits</code>는 파일 편집 도구(Read, Edit, Write, NotebookEdit 등)를 자동 승인하되 그 외 도구는 default와 동일하게 다룬다. 코드 작성을 본격적으로 맡길 때 가장 자주 켜는 모드다. Bash나 외부 호출은 여전히 사용자 확인을 거치므로, 파일 편집 루프만 빠르게 돌리고 싶을 때 적절하다.</p>

<p><code class="language-plaintext highlighter-rouge">plan</code> 모드는 모든 쓰기 도구를 막고 읽기 전용 도구만 허용한다. 모델이 작업 계획을 세우는 단계에서 코드를 건드리지 못하게 강제할 때 쓴다. 큰 리팩토링을 시작하기 전에 plan 모드로 의도를 충분히 확인하고, 합의된 계획만 acceptEdits로 옮겨 실행하는 흐름이 안정적이다.</p>

<p>마지막으로 <code class="language-plaintext highlighter-rouge">bypassPermissions</code>는 모든 퍼미션을 무시하는 모드다. 일회성 자동화 스크립트나 격리된 컨테이너 안에서 빠른 실험을 돌릴 때만 쓰는 게 안전하다. 메인 작업 환경의 <code class="language-plaintext highlighter-rouge">defaultMode</code>로 두면 사실상 하네스 없는 자율 에이전트가 되어버린다. CLI 플래그 <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code>도 같은 효과를 내는데, 이 플래그가 위험한 이름을 가진 데에는 이유가 있다.</p>

<h1 id="퍼미션-룰의-구조">퍼미션 룰의 구조</h1>

<p>퍼미션은 <code class="language-plaintext highlighter-rouge">permissions</code> 키 아래의 <code class="language-plaintext highlighter-rouge">allow</code>, <code class="language-plaintext highlighter-rouge">ask</code>, <code class="language-plaintext highlighter-rouge">deny</code> 세 배열로 표현한다. 각 배열은 도구 패턴 문자열의 리스트다. 같은 호출이 <code class="language-plaintext highlighter-rouge">deny</code>에 매칭되면 무조건 차단되고, <code class="language-plaintext highlighter-rouge">allow</code>에 매칭되면 자동 통과되며, <code class="language-plaintext highlighter-rouge">ask</code>에 매칭되거나 어디에도 매칭되지 않으면 사용자에게 묻는다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"defaultMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"acceptEdits"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"allow"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"Read"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Edit"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Bash(git status)"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Bash(git diff:*)"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Bash(npm run test:*)"</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"ask"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"Bash(git push:*)"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Bash(npm publish:*)"</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"deny"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"Bash(rm -rf:*)"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Bash(git push --force:*)"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Read(./.env)"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"Read(./secrets/**)"</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>도구 이름만 적으면 그 도구의 모든 호출에 매칭된다. <code class="language-plaintext highlighter-rouge">Read</code>라고만 두면 어떤 경로의 Read든 자동 허용이라는 뜻이다. 괄호 안의 인자는 도구마다 다른 매칭 규칙을 따른다.</p>

<p>Bash 룰은 명령행 문자열에 대한 prefix 매칭이다. <code class="language-plaintext highlighter-rouge">Bash(npm run test:*)</code>는 <code class="language-plaintext highlighter-rouge">npm run test</code>로 시작하는 명령 전체를 허용한다. 콜론과 별표를 붙이지 않으면 정확히 그 문자열만 허용되니, <code class="language-plaintext highlighter-rouge">Bash(git status)</code>는 <code class="language-plaintext highlighter-rouge">git status</code> 단독 호출만 통과시키고 <code class="language-plaintext highlighter-rouge">git status -s</code>는 다시 묻는다. 실제 운영에서는 거의 모든 룰을 <code class="language-plaintext highlighter-rouge">:*</code>로 끝맺게 된다. 다만 prefix 매칭의 한계를 이해해 두는 게 중요하다. <code class="language-plaintext highlighter-rouge">Bash(npm run test:*)</code>로 허용해 두면 <code class="language-plaintext highlighter-rouge">npm run test &amp;&amp; rm -rf /</code>도 prefix가 일치해 통과한다. 셸이 명령을 분리해서 실행하는 일까지 퍼미션이 막아주지는 않는다는 뜻이다. 그래서 위험한 명령은 <code class="language-plaintext highlighter-rouge">allow</code>로 푸는 대신 <code class="language-plaintext highlighter-rouge">deny</code>에 따로 박는 편이 안전하다.</p>

<p>Edit, Read 같은 파일 도구의 룰은 gitignore-style glob을 따른다. <code class="language-plaintext highlighter-rouge">Read(./src/**)</code>는 <code class="language-plaintext highlighter-rouge">src</code> 디렉토리 하위 전체를, <code class="language-plaintext highlighter-rouge">Read(~/.zshrc)</code>는 홈 디렉토리의 특정 파일을 가리킨다. 절대경로(<code class="language-plaintext highlighter-rouge">/</code>로 시작), 홈 상대경로(<code class="language-plaintext highlighter-rouge">~/</code>), 워크스페이스 상대경로(<code class="language-plaintext highlighter-rouge">./</code>)가 모두 지원된다. <code class="language-plaintext highlighter-rouge">.env</code>나 비밀키 같은 파일은 <code class="language-plaintext highlighter-rouge">deny</code> 쪽에 미리 넣어두는 패턴이 거의 표준이다.</p>

<p>WebFetch는 도메인 단위로 끊는다. <code class="language-plaintext highlighter-rouge">WebFetch(domain:docs.anthropic.com)</code> 형태로 도메인을 명시한다. 검색 결과를 그대로 따라가는 호출이 늘다 보면 잘 모르는 도메인을 모델이 부르는 일이 생기는데, allow 리스트로 신뢰 도메인만 열어 두는 편이 안전하다.</p>

<p>MCP 도구는 <code class="language-plaintext highlighter-rouge">mcp__&lt;server&gt;__&lt;tool&gt;</code> 형태로 노출되고, 서버 단위 또는 도구 단위로 룰을 작성할 수 있다. <code class="language-plaintext highlighter-rouge">mcp__github</code>만 적으면 GitHub MCP 서버가 노출하는 모든 도구를 의미하고, <code class="language-plaintext highlighter-rouge">mcp__github__create_pull_request</code>처럼 적으면 단일 도구만 가리킨다. 다만 MCP 도구는 prefix 매칭이나 인자 매칭을 지원하지 않는다. 같은 도구라도 위험한 호출과 안전한 호출을 구분하고 싶다면, MCP 서버 쪽에서 도구를 분리해 노출하거나 hook 단계에서 거르는 편이 현실적이다.</p>

<h1 id="그-외-자주-쓰는-키들">그 외 자주 쓰는 키들</h1>

<p><code class="language-plaintext highlighter-rouge">settings.json</code>은 퍼미션 외에도 몇 개의 키를 더 받는다. 운영에 직접 영향을 주는 것들만 추리면 다음과 같다.</p>

<p><code class="language-plaintext highlighter-rouge">additionalDirectories</code>는 워크스페이스 바깥의 디렉토리를 추가로 노출한다. 모노레포에서 인접 패키지를 함께 보게 하거나, 참조용 문서가 다른 위치에 있을 때 유용하다. 다만 여기에 노출된 경로는 퍼미션 규칙도 똑같이 받기 때문에, <code class="language-plaintext highlighter-rouge">Read(/path/to/extra/**)</code> 형태로 명시적으로 허용해 줘야 한다.</p>

<p><code class="language-plaintext highlighter-rouge">env</code>는 세션이 시작될 때 주입할 환경변수를 정의한다. 비밀키를 직접 박지 말고 <code class="language-plaintext highlighter-rouge">${VAR}</code> 형태의 expansion만 두는 패턴이 안전하다. <code class="language-plaintext highlighter-rouge">apiKeyHelper</code>도 비슷한 목적인데, 외부 명령을 실행해 키를 받아오게 한다. 보통은 1Password CLI나 macOS Keychain을 호출하는 헬퍼를 두는 식이다.</p>

<p><code class="language-plaintext highlighter-rouge">hooks</code>는 별도로 다룰 만한 주제지만 퍼미션과의 관계는 짚어둘 필요가 있다. <code class="language-plaintext highlighter-rouge">PreToolUse</code> hook은 모델의 도구 호출이 실제 실행되기 직전에 끼어들어 호출을 막거나 변형할 수 있다. 퍼미션이 정적인 룰 매칭이라면, hook은 코드로 평가되는 동적 게이트다. <code class="language-plaintext highlighter-rouge">Bash(*)</code>를 일괄 allow한 뒤, hook으로 ‘main 브랜치에서의 직접 push만 거절’ 같은 조건문 검사를 거는 식의 조합이 가능하다.</p>

<h1 id="allow로-둬도-되는-것들">allow로 둬도 되는 것들</h1>

<p>룰 설계의 핵심은 결국 ‘allow에 둘 것’과 ‘ask로 남길 것’의 경계를 어디에 그을지에 달려 있다. 너무 좁게 잡으면 매번 묻느라 작업이 끊기고, 너무 넓게 잡으면 사고 가능성이 커진다. 사용해 본 경험으로는 다음 기준이 무난했다.</p>

<p>읽기 전용 명령은 거의 다 allow로 둬도 무방하다. <code class="language-plaintext highlighter-rouge">git status</code>, <code class="language-plaintext highlighter-rouge">git diff</code>, <code class="language-plaintext highlighter-rouge">git log</code>, <code class="language-plaintext highlighter-rouge">ls</code>, <code class="language-plaintext highlighter-rouge">cat</code>, <code class="language-plaintext highlighter-rouge">pwd</code>, <code class="language-plaintext highlighter-rouge">which</code> 같은 명령은 부작용이 없다. <code class="language-plaintext highlighter-rouge">find</code>나 <code class="language-plaintext highlighter-rouge">grep</code>도 해당된다. 다만 <code class="language-plaintext highlighter-rouge">curl</code>, <code class="language-plaintext highlighter-rouge">wget</code>은 외부로 데이터를 보낼 수 있어 ask로 두는 편이 낫다.</p>

<p>빌드, 테스트, 린트는 경험상 프로젝트 단위 <code class="language-plaintext highlighter-rouge">.claude/settings.json</code>에 박아둬도 된다. <code class="language-plaintext highlighter-rouge">npm run test:*</code>, <code class="language-plaintext highlighter-rouge">npm run lint:*</code>, <code class="language-plaintext highlighter-rouge">npm run build</code> 같은 것들이 매번 사용자 확인을 거치면 acceptEdits 모드의 의미가 사라진다.</p>

<p>git 쓰기 작업은 보수적으로 다룬다. <code class="language-plaintext highlighter-rouge">git add</code>, <code class="language-plaintext highlighter-rouge">git commit</code>, <code class="language-plaintext highlighter-rouge">git checkout</code>은 로컬에 한정되니 allow에 둬도 큰 문제는 없지만, <code class="language-plaintext highlighter-rouge">git push</code>, <code class="language-plaintext highlighter-rouge">git reset --hard</code>, <code class="language-plaintext highlighter-rouge">git rebase</code>, <code class="language-plaintext highlighter-rouge">git branch -D</code>는 ask 또는 deny에 두는 편이 안전하다. 특히 force push는 deny에 명시적으로 박아두면 모델이 우회할 여지가 줄어든다.</p>

<p>파일 편집은 <code class="language-plaintext highlighter-rouge">acceptEdits</code> 모드와 deny 룰의 조합으로 다루는 게 깔끔하다. 기본은 자동 통과시키되, <code class="language-plaintext highlighter-rouge">.env</code>, 시크릿 디렉토리, <code class="language-plaintext highlighter-rouge">.git/</code> 내부, CI 워크플로 파일처럼 사고 영향이 큰 경로만 deny에 따로 박는다. CI 워크플로의 경우 모델이 디버깅 목적으로 <code class="language-plaintext highlighter-rouge">--no-verify</code>나 빌드 스킵 같은 옵션을 끼워 넣는 일이 종종 있다.</p>

<p>MCP 도구는 서버 단위로 한 번 검토하고 도구 단위로 좁혀 가는 편이 좋다. 처음에는 <code class="language-plaintext highlighter-rouge">mcp__&lt;server&gt;</code> 전체를 ask로 두고, 자주 쓰는 도구만 allow로 옮기면 운영 중 문제가 생긴 도구를 빠르게 격리할 수 있다.</p>

<h1 id="내가-겪은-오류">내가 겪은 오류</h1>

<p>내가 처음 설정할 때 겪었던 실수들을 정리해둔다.</p>

<p>첫 번째는 prefix 매칭이다. 앞서 언급한 대로 <code class="language-plaintext highlighter-rouge">Bash(npm run test:*)</code> 같은 룰은 명령 시작 부분만 본다. 셸 구문(<code class="language-plaintext highlighter-rouge">&amp;&amp;</code>, <code class="language-plaintext highlighter-rouge">;</code>, <code class="language-plaintext highlighter-rouge">|</code>, 백틱)으로 명령이 결합된 경우 뒤쪽이 어떤 명령이든 통과한다. 이 점 때문에 운영에서는 위험 명령을 deny에 명시하는 작업과, hook으로 셸 메타문자 사용을 검출하는 작업이 같이 가야 한다.</p>

<p>두 번째는 deny 우선의 구조를 잊어버리는 것이다. 전역 <code class="language-plaintext highlighter-rouge">~/.claude/settings.json</code>에 <code class="language-plaintext highlighter-rouge">Bash(git push:*)</code>를 deny로 박아뒀다가, 특정 프로젝트에서만 이를 풀고 싶다고 <code class="language-plaintext highlighter-rouge">.claude/settings.json</code>의 <code class="language-plaintext highlighter-rouge">allow</code>에 같은 항목을 추가하는 일이 있다. 이때는 deny가 이긴다. 풀고 싶다면 사용자 설정 쪽의 deny를 수정해야 한다.</p>

<p>세 번째는 <code class="language-plaintext highlighter-rouge">bypassPermissions</code> 모드의 위험성이다. 격리 컨테이너에서만 쓰려고 만든 모드인데, 일단 손에 익으면 메인 환경에서도 그냥 켜고 쓰는 사례가 늘어난다. 이 모드에서는 <code class="language-plaintext highlighter-rouge">deny</code> 룰조차 무시되므로, 본인이 의도하지 않은 명령이 실행될 가능성이 항상 열려 있다. 자율 실행이 필요한 상황이라면 권한을 푸는 대신 worktree나 컨테이너 격리를 함께 가져가는 편이 안전하다.</p>

<p>네 번째는 settings.local.json을 git에 올리는 실수다. 기본 <code class="language-plaintext highlighter-rouge">.gitignore</code>에 들어 있긴 하지만, 직접 만든 프로젝트의 경우 빠져 있을 수 있다. 본인 환경 전용 비밀키 경로나 자동 승인 룰이 팀원 환경에 적용되는 일은 가능하면 피해야 한다.</p>

<h1 id="정리">정리</h1>

<p>퍼미션 시스템은 모델이 무엇을 할 수 있는지의 경계를 정적으로 선언하는 역할을 한다. 어디까지 자동 허용할지를 정하는 작업은 곧 ‘AI에게 어디까지 위임할 것인가’의 결정이고, <code class="language-plaintext highlighter-rouge">settings.json</code>은 그 결정을 코드처럼 관리할 수 있게 해준다. 다만 정적 룰만으로 모든 케이스를 막을 수는 없다. 셸 결합이나 의미 단위의 위험 판단처럼 코드 평가가 필요한 영역은 hook이 채우게 되고, 둘은 같은 하네스의 안쪽과 바깥쪽을 나눠 맡는 관계다.</p>

<p>간단히 정리하면, 첫째, <code class="language-plaintext highlighter-rouge">defaultMode</code>는 작업의 위험도에 맞춰 plan/default/acceptEdits 사이에서 고르고 <code class="language-plaintext highlighter-rouge">bypassPermissions</code>는 격리 환경에서만 쓴다. 둘째, allow/ask/deny는 누적되며 deny가 항상 이긴다는 점을 인지하고, 위험 명령은 allow로 푸는 대신 deny에 박아 둔다. 셋째, 정적 룰로 표현되지 않는 동적 조건은 hook으로 옮긴다. 이 세 축을 분리해서 다루기 시작하면 설정 파일이 깔끔해지고, 같은 사고가 두 번 반복될 여지도 줄어든다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="AI" /><category term="ClaudeCode" /><category term="settings" /><category term="퍼미션" /><summary type="html"><![CDATA[Claude Code를 사용가는 거의 모든 사용자들이 맞닥뜨리는 문제가 있다. 처음에는 모든 도구 호출을 일일이 승인하다가, 같은 명령을 몇 번쯤 허용하고 나면 ‘Allow always’를 누르게 되고, 결국에는 --dangerously-skip-permissions 플래그를 검색하기 시작한다. 그리고 며칠 뒤, 에이전트가 잘못된 디렉토리에서 git reset --hard를 돌리거나, 운영 DB에 붙은 클라이언트로 마이그레이션을 다시 실행하는 사고가 한 번쯤 난다.]]></summary></entry><entry><title type="html">tmux 설치 및 단축키 정리</title><link href="https://ycseong07.github.io/2026/0203_tmux-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EB%8B%A8%EC%B6%95%ED%82%A4-%EC%A0%95%EB%A6%AC/" rel="alternate" type="text/html" title="tmux 설치 및 단축키 정리" /><published>2026-02-03T09:00:00+09:00</published><updated>2026-02-03T09:00:00+09:00</updated><id>https://ycseong07.github.io/2026/tmux-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EB%8B%A8%EC%B6%95%ED%82%A4-%EC%A0%95%EB%A6%AC</id><content type="html" xml:base="https://ycseong07.github.io/2026/0203_tmux-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EB%8B%A8%EC%B6%95%ED%82%A4-%EC%A0%95%EB%A6%AC/"><![CDATA[<p>Claude Code, Gemini CLI, Codex를 동시에 돌리다 보니, 여러 터미널 세션을 동시에 관리해야 할 필요성이 생겼다. <a href="https://www.youtube.com/watch?v=vlg8X0N8z08">개발동생 유튜브</a>에서도 같은 이슈를 다루고 있어서, 이를 참고해 <strong>Ghostty</strong>(터미널 에뮬레이터) + <strong>tmux</strong>(터미널 멀티플렉서) 조합으로 세팅했다.</p>

<hr />

<h2 id="tmux란">tmux란</h2>

<p>tmux는 터미널 멀티플렉서(terminal multiplexer)로, 하나의 터미널 창 안에서 여러 세션, 윈도우, 패널을 동시에 운영할 수 있게 해준다. 세션을 종료하지 않고 detach 할 수 있어서, 터미널을 닫아도 세션은 백그라운드에서 살아 있고, 나중에 다시 attach해서 이어서 작업할 수 있다. 이런 특징을 이용해 각 패널이나 윈도우 간 비동기 작업 후 결과를 서로 공유하는 것도 가능하다.</p>

<p>tmux는 Session - Window - Pane의 3계층으로 작업 공간을 구성한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Session
└── Window 0
│   ├── Pane 0 (좌)
│   └── Pane 1 (우)
└── Window 1
    └── Pane 0
...
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>단위</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Session</strong></td>
      <td>독립된 작업 맥락. 프로젝트 단위로 분리하기 좋다.</td>
    </tr>
    <tr>
      <td><strong>Window</strong></td>
      <td>세션 안의 탭. 브라우저 탭처럼 전환 가능.</td>
    </tr>
    <tr>
      <td><strong>Pane</strong></td>
      <td>윈도우 안을 분할한 터미널 패널.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="설치">설치</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># macOS</span>
brew <span class="nb">install </span>tmux

<span class="c"># Ubuntu/Debian</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>tmux
</code></pre></div></div>

<hr />

<h2 id="단축키">단축키</h2>

<p>tmux 세션 내에서, 모든 단축키는 <strong>prefix</strong> 입력 후 명령 키를 눌러야 한다(기본값: <code class="language-plaintext highlighter-rouge">Ctrl+b</code>).</p>

<hr />

<h3 id="세션-관련">세션 관련</h3>

<ul>
  <li>세션 밖(터미널 프롬프트)</li>
</ul>

<table>
  <thead>
    <tr>
      <th>명령어</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmux new</code></td>
      <td>새 세션 생성</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmux new -s dev</code></td>
      <td><code class="language-plaintext highlighter-rouge">dev</code>라는 이름으로 세션 생성</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmux attach</code></td>
      <td>마지막 세션에 재접속</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmux attach -t dev</code></td>
      <td><code class="language-plaintext highlighter-rouge">dev</code> 세션에 재접속 (이름 또는 번호)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmux ls</code></td>
      <td>열려 있는 세션 목록 확인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmux kill-session -t dev</code></td>
      <td><code class="language-plaintext highlighter-rouge">dev</code> 세션 강제 종료</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>세션 안(prefix 후 입력)</li>
</ul>

<table>
  <thead>
    <tr>
      <th>단축키</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d</code></td>
      <td>현재 세션 detach (세션은 유지, 터미널만 빠져나옴)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">$</code></td>
      <td>현재 세션 이름 변경</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">s</code></td>
      <td>열려 있는 세션 목록 표시 → 엔터로 이동 / <code class="language-plaintext highlighter-rouge">→</code>로 윈도우·패널 미리보기</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="윈도우-관련-prefix-후-입력">윈도우 관련 (prefix 후 입력)</h3>

<table>
  <thead>
    <tr>
      <th>단축키</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">c</code></td>
      <td>새 윈도우 생성</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">p</code></td>
      <td>이전 윈도우로 이동</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">n</code></td>
      <td>다음 윈도우로 이동</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0</code>~<code class="language-plaintext highlighter-rouge">9</code></td>
      <td>번호로 특정 윈도우 이동</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">,</code></td>
      <td>현재 윈도우 이름 변경</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w</code></td>
      <td>전체 윈도우 목록 펼쳐서 확인 → 좌측 번호 입력해서 이동</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="pane-관련-prefix-후-입력">Pane 관련 (prefix 후 입력)</h3>

<table>
  <thead>
    <tr>
      <th>단축키</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%</code></td>
      <td>현재 pane을 <strong>좌우</strong>로 분할</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"</code></td>
      <td>현재 pane을 <strong>상하</strong>로 분할</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">방향키</code></td>
      <td>인접한 pane으로 이동</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">z</code></td>
      <td>현재 pane 전체화면 토글 (다시 누르면 원복)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">x</code></td>
      <td>현재 pane 닫기 (확인 메시지 있음)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl+d</code></td>
      <td>현재 pane 즉시 닫기 (확인 없음, 마지막 pane이면 작동 안 함)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">x</code>와 <code class="language-plaintext highlighter-rouge">Ctrl+d</code>로 닫은 pane/window/session은 <code class="language-plaintext highlighter-rouge">attach</code>로 재접근 불가.</p>
</blockquote>

<hr />

<h2 id="tmux-send--capture">tmux send / capture</h2>

<p>AI 도구를 여러 pane에 나눠 돌릴 때 유용한 명령어다. 쉘 스크립트나 자동화 코드에서 특정 pane에 명령을 보내거나 출력을 가져올 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 특정 pane에 명령어 전송</span>
<span class="c"># -t 옵션: session:window.pane 형식</span>
tmux send-keys <span class="nt">-t</span> dev:0.1 <span class="s2">"ls -al"</span> Enter

<span class="c"># 특정 pane의 출력 내용 가져오기</span>
tmux capture-pane <span class="nt">-t</span> dev:0.1 <span class="nt">-p</span>
</code></pre></div></div>

<hr />

<h2 id="참고">참고</h2>

<ul>
  <li><a href="https://tmuxcheatsheet.com/">tmux cheat sheet</a></li>
  <li><a href="https://www.youtube.com/watch?v=vlg8X0N8z08">Ghostty + tmux 세팅 영상</a></li>
  <li><a href="https://m.blog.naver.com/PostView.naver?blogId=songsite123&amp;logNo=223809804101&amp;navType=by">네이버 블로그 참고</a></li>
</ul>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="tmux" /><category term="terminal" /><category term="개발환경" /><category term="ClaudeCode" /><summary type="html"><![CDATA[Claude Code, Gemini CLI, Codex를 동시에 돌리다 보니, 여러 터미널 세션을 동시에 관리해야 할 필요성이 생겼다. 개발동생 유튜브에서도 같은 이슈를 다루고 있어서, 이를 참고해 Ghostty(터미널 에뮬레이터) + tmux(터미널 멀티플렉서) 조합으로 세팅했다.]]></summary></entry><entry><title type="html">Ollama로 뭘 할 수 있고 뭘 못 하는가</title><link href="https://ycseong07.github.io/2025/1229_ollama%EB%A1%9C-%EB%AD%98-%ED%95%A0-%EC%88%98-%EC%9E%88%EA%B3%A0-%EB%AD%98-%EB%AA%BB-%ED%95%98%EB%8A%94%EA%B0%80/" rel="alternate" type="text/html" title="Ollama로 뭘 할 수 있고 뭘 못 하는가" /><published>2025-12-29T09:00:00+09:00</published><updated>2025-12-29T09:00:00+09:00</updated><id>https://ycseong07.github.io/2025/Ollama%EB%A1%9C-%EB%AD%98-%ED%95%A0-%EC%88%98-%EC%9E%88%EA%B3%A0-%EB%AD%98-%EB%AA%BB-%ED%95%98%EB%8A%94%EA%B0%80</id><content type="html" xml:base="https://ycseong07.github.io/2025/1229_ollama%EB%A1%9C-%EB%AD%98-%ED%95%A0-%EC%88%98-%EC%9E%88%EA%B3%A0-%EB%AD%98-%EB%AA%BB-%ED%95%98%EB%8A%94%EA%B0%80/"><![CDATA[<p>회사에서 개발하던 한 프로젝트의 기능 중, OpenAI API를 호출해 결과를 받아 오는 작업이 서버장애로 먹통이 된 적이 있다. 이를 계기로 OpenAI 쪽 장애가 나면 사내 서버에 띄워둔 Ollama로 자동 전환되도록 fallback을 걸어 두려는 시도를 한 적이 있다. OpenAI 호환 엔드포인트 덕에 <code class="language-plaintext highlighter-rouge">base_url</code>만 바꾸면 코드가 그대로 동작하니, ‘API 모양이 같으면 결과도 어느 정도 비슷하지 않을까’ 정도의 막연한 기대로 짜둔 구성이었다. 하지만 테스트를 해보니 문제가 많았다. 답변 품질이 눈에 띄게 떨어졌고 응답 속도도 사용자가 체감할 만큼 느려져, ‘fallback이 있긴 하지만 fallback 상태로는 정상 운영이 어렵다’는 결론에 도달했다.</p>

<p>이 경험을 계기로, Ollama가 잘 하는 일과 못 하는 일을 한 번 정리해 두는 편이 낫겠다는 생각이 들었다. 같은 OpenAI 호환 인터페이스를 노출한다고 해서 같은 자리에 그대로 끼워 넣을 수 있는 도구는 아니고, 잘 되는 영역과 굳이 Ollama를 고를 이유가 없는 영역이 모델 크기, 동시성, 사용 목적에 따라 꽤 분명하게 갈린다. 이 글에서는 2025년 12월 시점을 기준으로 Ollama가 어디까지 쓸 만하고 어디부터는 다른 선택지로 넘어가야 하는지를 정리한다.</p>

<h1 id="ollama가-자리한-위치">Ollama가 자리한 위치</h1>

<p>Ollama는 llama.cpp 추론 엔진 위에 패키지 매니저, 모델 레지스트리, HTTP 서버, GUI를 덧씌운 형태다. 사용자가 보는 UX는 Docker와 비슷하다. <code class="language-plaintext highlighter-rouge">ollama pull</code>, <code class="language-plaintext highlighter-rouge">ollama run</code>, <code class="language-plaintext highlighter-rouge">ollama list</code> 같은 서브커맨드로 모델을 가져오고 실행하며, <code class="language-plaintext highlighter-rouge">Modelfile</code>로 시스템 프롬프트와 파라미터를 박아 커스텀 모델을 만들 수도 있다. 백그라운드에서는 11434 포트의 HTTP 서버가 떠 있고, 자체 <code class="language-plaintext highlighter-rouge">/api/chat</code> 엔드포인트와 함께 <code class="language-plaintext highlighter-rouge">/v1/chat/completions</code> OpenAI 호환 엔드포인트를 같이 노출한다. 클라이언트 라이브러리를 새로 만들 필요 없이, OpenAI 파이썬 SDK의 <code class="language-plaintext highlighter-rouge">base_url</code>만 바꿔 끼우면 그대로 동작한다는 점이 도입 비용을 크게 떨어뜨렸다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">openai</span> <span class="kn">import</span> <span class="n">OpenAI</span>

<span class="n">client</span> <span class="o">=</span> <span class="nc">OpenAI</span><span class="p">(</span><span class="n">base_url</span><span class="o">=</span><span class="sh">"</span><span class="s">http://localhost:11434/v1</span><span class="sh">"</span><span class="p">,</span> <span class="n">api_key</span><span class="o">=</span><span class="sh">"</span><span class="s">ollama</span><span class="sh">"</span><span class="p">)</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">qwen3:8b</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">messages</span><span class="o">=</span><span class="p">[{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">한 문장으로 자기 소개</span><span class="sh">"</span><span class="p">}],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>2025년 7월 macOS와 Windows용 데스크톱 앱이 정식으로 추가되면서, 터미널을 거치지 않고도 모델을 골라 채팅 창을 띄울 수 있게 되었다. 같은 해 하반기에는 ‘Turbo’라는 이름의 클라우드 백엔드가 도입되어, 로컬에서 돌리기 버거운 대형 모델(예: gpt-oss-120B)은 Ollama 계정과 연결된 클라우드 GPU 위에서 실행하고 결과만 받아오는 옵션도 생겼다. 즉 Ollama는 로컬 전용이라는 초기 정체성을 어느 정도 벗어나, 같은 인터페이스 안에서 로컬과 원격을 섞어 쓸 수 있는 도구로 확장되는 중이다.</p>

<h1 id="ollama로-잘-되는-일">Ollama로 잘 되는 일</h1>

<p>가장 잘 되는 일은 단일 사용자 데스크톱 추론이다. 노트북이나 워크스테이션 한 대에서 모델 한두 개를 띄워 놓고 본인이 직접 묻고 답을 받는 시나리오에서, Ollama는 설치, 모델 관리, 종료까지를 가장 적은 마찰로 처리한다. M-시리즈 맥북은 통합 메모리 덕에 7~14B 모델을 무리 없이 굴리고, 24GB VRAM의 RTX 4090 같은 데스크톱 GPU라면 30B급 모델까지 4비트 양자화로 올릴 수 있다.</p>

<p>두 번째는 프라이버시 또는 오프라인 제약이 강한 작업이다. 사내 코드, 환자 기록, 미공개 문서처럼 외부 API에 보내기 곤란한 입력이 있는 작업이라면, 로컬 추론 자체가 요구사항이 된다. 이 경우 Ollama의 OpenAI 호환 API는 기존에 OpenAI/Anthropic API로 짠 코드를 거의 그대로 재사용하게 해 준다. RAG 인덱싱 단계에서 쓰는 임베딩도 <a href="https://ollama.com/library/nomic-embed-text">nomic-embed-text</a> 또는 <a href="https://ollama.com/library/mxbai-embed-large">mxbai-embed-large</a>를 같은 서버에서 같이 띄워두면 한 프로세스로 검색과 답변을 모두 처리할 수 있다.</p>

<p>세 번째는 프로토타이핑이다. 새로운 아이디어를 검증할 때, 매번 토큰 비용을 신경 쓰며 호출하는 것보다는 로컬에서 8B급 모델로 워크플로의 골격을 먼저 잡고, 품질이 부족한 부분만 클라우드 API로 옮기는 진행이 효율이 좋다. 도구 호출 지원이 있는 모델(Llama 3.1 이후 계열, Qwen 3, gpt-oss 등)이라면 에이전트 루프의 형태도 그대로 검증해 볼 수 있다.</p>

<p>네 번째는 멀티모달의 가벼운 활용이다. Llama 3.2-Vision, Qwen2-VL, LLaVA 같은 비전 모델이 Ollama 카탈로그에 들어와 있어서, 스크린샷을 입력으로 넣어 화면 요약을 받거나 OCR 보조용으로 쓰는 작업은 데스크톱에서 충분히 돌아간다. 영상이 아닌 정지 이미지 단위라면 응답 지연도 받아들일 만한 수준이다.</p>

<p>마지막은 커스텀 모델의 배포 단순화다. 파인튜닝이나 LoRA로 만든 어댑터를 GGUF로 변환한 뒤 <code class="language-plaintext highlighter-rouge">Modelfile</code>로 묶어 두면, 팀 동료에게 모델 카드 한 줄과 <code class="language-plaintext highlighter-rouge">ollama pull</code> 명령으로 배포가 끝난다. 시스템 프롬프트, temperature, top_p 같은 운영 파라미터도 같이 박을 수 있어서 ‘같은 모델인데 사람마다 결과가 다르다’는 종류의 혼선을 줄여 준다.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> qwen3:8b</span>
PARAMETER temperature 0.2
PARAMETER num_ctx 8192
SYSTEM """
너는 사내 코드베이스 컨벤션을 따르는 코드 리뷰어다.
모든 응답은 한국어로, 변경 제안은 diff 형태로 작성한다.
"""
</code></pre></div></div>

<h1 id="ollama로-안-되거나-어려운-일">Ollama로 안 되거나 어려운 일</h1>

<p>먼저 품질의 절대치다. 2025년 12월 기준 데스크톱에서 굴릴 수 있는 30B급 이하 모델의 종합적인 추론 능력은 Claude Sonnet 4.5, GPT-5, Gemini 3 Pro 같은 프런티어 모델과 격차가 여전히 크다. gpt-oss-120B나 DeepSeek-R1-Distill-70B처럼 좀 더 큰 모델이 일부 벤치마크에서 클라우드 모델과 비슷한 점수를 내는 사례도 나왔지만, 이들은 단일 GPU 한 장으로는 실행이 어려워 데스크톱에서의 ‘Ollama 경험’과는 다른 이야기가 된다. 코드 작성, 도구 호출, 긴 추론 체인이 결합된 에이전트형 작업에서 격차는 더 두드러진다.</p>

<p>두 번째는 긴 컨텍스트다. 카드 상의 컨텍스트 길이가 128K로 적혀 있어도, 실제로 그만큼 띄우려면 KV 캐시가 VRAM을 빠르게 잡아먹는다. 7B 모델의 32K 컨텍스트를 4비트 양자화로 올리는 것까지는 24GB VRAM에서 무리가 없지만, 같은 모델로 128K를 켜는 순간 모델 가중치보다 KV 캐시가 더 큰 자리를 차지하게 된다. 길이가 늘어날수록 prefill 시간도 비례해서 늘어나고, 결과적으로 첫 토큰까지의 지연이 클라우드 API 대비 크게 벌어진다. 32K 이상 컨텍스트가 일상이 되는 워크로드라면 Ollama는 최선의 도구가 아니다.</p>

<p>세 번째는 동시성이다. Ollama의 서버는 기본적으로 한 모델당 한 번에 하나의 요청을 처리하도록 설계되어 있다. 큐가 있긴 하지만 <a href="https://github.com/vllm-project/vllm">vLLM</a>이나 <a href="https://github.com/sgl-project/sglang">SGLang</a>이 제공하는 continuous batching, paged attention, speculative decoding 같은 처리량 최적화가 들어가 있지 않다. 동시 사용자 수십 명에게 같은 모델을 서빙해야 하는 사내 챗봇 같은 환경이라면 처리량이 빠르게 한계에 부딪힌다. 이 자리는 Ollama 대신 vLLM 또는 SGLang의 영역이다.</p>

<p>네 번째는 멀티 GPU 활용이다. Ollama도 여러 GPU에 모델을 분산 적재할 수는 있지만, 텐서 병렬화의 효율이나 NVLink 최적화 같은 면에서는 vLLM 쪽이 한참 앞서 있다. A100 또는 H100 여러 장을 묶어서 70B 이상 모델을 풀 정밀도로 서빙하려는 시나리오에서 Ollama는 적합한 도구가 아니다.</p>

<p>다섯 번째는 학습이다. Ollama는 추론 전용 런타임이고, 파인튜닝이나 RLHF, GRPO 같은 학습 절차는 다루지 않는다. LoRA 어댑터를 import해서 추론에 합치는 것까지는 가능하지만, 학습 자체는 <a href="https://github.com/axolotl-ai-cloud/axolotl">Axolotl</a>, <a href="https://github.com/unslothai/unsloth">Unsloth</a>, HuggingFace <code class="language-plaintext highlighter-rouge">trl</code> 같은 별도 스택에서 진행한 뒤 결과물을 GGUF로 변환해 가져오는 흐름이 된다.</p>

<p>여섯 번째는 가장 큰 모델이다. Llama 4 Maverick의 400B 파라미터 클래스, DeepSeek-V3 계열의 600B+ MoE 모델 같은 것은 가정용 하드웨어에서 4비트 양자화로 적재하더라도 수백 GB의 VRAM 또는 통합 메모리가 필요하다. 이 영역은 Ollama Turbo 같은 클라우드 백엔드를 통해서 다루는 것이 사실상 유일한 선택이고, 그 시점에서 ‘Ollama로 로컬 추론’이라는 본래의 매력은 거의 사라진다. 다른 클라우드 추론 서비스 대비 가격 경쟁력이 있느냐의 문제로 넘어간다.</p>

<p>마지막으로, 신모델 지원의 시간차다. 새로운 오픈 모델이 공개되면 보통 며칠에서 길게는 몇 주에 걸쳐 GGUF 변환과 템플릿 정합 작업이 진행된다. 그 사이에는 Hugging Face Transformers나 vLLM 쪽이 먼저 동작하는 경우가 많다. 출시 직후 며칠 안에 모델을 검증해야 하는 입장이라면 Ollama만 보고 기다리는 것은 답이 아니다.</p>

<h1 id="모델-선택-기준">모델 선택 기준</h1>

<p>2025년 12월 시점에서 Ollama 카탈로그 안의 선택지는 작년보다 훨씬 두꺼워졌다. 일반적인 한국어, 영어 대화와 가벼운 코드 작업에는 Qwen3-8B 또는 Llama 3.3-8B가 무난하다. 추론을 명시적으로 요구하는 문제에는 DeepSeek-R1 distill 계열 또는 Qwen3-30B-A3B의 thinking 모드가 효과를 본다. 코드 보조 용도라면 Qwen2.5-Coder-7B/14B가 여전히 가성비가 좋다.</p>

<p>하드웨어 한도가 좀 더 여유 있는 워크스테이션에서는 gpt-oss-20B가 합리적인 후보다. OpenAI가 2025년 8월에 공개한 가중치로, 도구 호출과 함수 시그니처 준수가 비교적 안정적이라는 평가가 많다. 더 무거운 gpt-oss-120B는 단일 24GB GPU로는 빡빡하고, 통합 메모리가 큰 맥 스튜디오 또는 다중 GPU 환경에서야 데스크톱 추론으로 의미가 있다.</p>

<p>비전 입력이 필요하다면 Llama 3.2-Vision-11B 또는 Qwen2-VL-7B 정도가 출발점으로 무난하다. 임베딩은 한국어를 섞는 검색이라면 mxbai-embed-large가 nomic-embed-text보다 결과가 안정적이라는 후기가 많은 편이다.</p>

<p>선택의 기준은 결국 하드웨어와 작업의 결합이다. VRAM 용량이 모델 크기를 결정하고, 작업의 성격(대화/코드/추론/비전)이 모델 계열을 결정하며, 동시성과 컨텍스트 요구가 ‘Ollama로 끝낼 수 있는가, 아니면 vLLM 또는 클라우드 API로 가야 하는가’를 결정한다.</p>

<h1 id="정리">정리</h1>

<p>Ollama가 잘 어울리는 자리는 명확하다. 한 사람이 자기 머신에서 7~30B급 모델을 가지고 프라이빗하게 작업하거나, 팀 안에서 가벼운 사내 도구를 빠르게 시제품 형태로 띄우거나, 새로운 오픈 모델을 손쉽게 비교 평가하고 싶을 때다. 이 자리에서 Ollama는 압도적으로 편하다.</p>

<p>반대로 Ollama로 무리하게 끌고 가지 않는 편이 좋은 자리도 있다. 프런티어 품질의 답변이 필요한 사용자 대면 서비스, 32K 이상의 긴 컨텍스트를 일상적으로 다루는 워크로드, 동시 사용자 처리량이 중요한 서빙, 그리고 본격적인 학습 파이프라인이다. 이쪽은 클라우드 API와 vLLM/SGLang이 더 적합한 도구다.</p>

<p>요약하자면 ‘Ollama로 다 된다’와 ‘Ollama로는 진지한 일을 못 한다’는 양극단의 인식 모두 현실과 어긋나 있다. Ollama는 로컬 단일 사용자 추론이라는 한정된 자리에서 가장 좋은 도구이고, 그 경계 바깥에서는 망설이지 말고 다른 도구로 넘어가는 것이 비용과 결과 모두에서 합리적이다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="AI" /><category term="LLM" /><category term="Ollama" /><category term="로컬LLM" /><summary type="html"><![CDATA[회사에서 개발하던 한 프로젝트의 기능 중, OpenAI API를 호출해 결과를 받아 오는 작업이 서버장애로 먹통이 된 적이 있다. 이를 계기로 OpenAI 쪽 장애가 나면 사내 서버에 띄워둔 Ollama로 자동 전환되도록 fallback을 걸어 두려는 시도를 한 적이 있다. OpenAI 호환 엔드포인트 덕에 base_url만 바꾸면 코드가 그대로 동작하니, ‘API 모양이 같으면 결과도 어느 정도 비슷하지 않을까’ 정도의 막연한 기대로 짜둔 구성이었다. 하지만 테스트를 해보니 문제가 많았다. 답변 품질이 눈에 띄게 떨어졌고 응답 속도도 사용자가 체감할 만큼 느려져, ‘fallback이 있긴 하지만 fallback 상태로는 정상 운영이 어렵다’는 결론에 도달했다.]]></summary></entry><entry><title type="html">MCP(Model Context Protocol) 개념과 활용법</title><link href="https://ycseong07.github.io/2025/1215_mcp-model-context-protocol-%EA%B0%9C%EB%85%90%EA%B3%BC-%ED%99%9C%EC%9A%A9%EB%B2%95/" rel="alternate" type="text/html" title="MCP(Model Context Protocol) 개념과 활용법" /><published>2025-12-15T09:00:00+09:00</published><updated>2025-12-15T09:00:00+09:00</updated><id>https://ycseong07.github.io/2025/MCP-Model-Context-Protocol-%EA%B0%9C%EB%85%90%EA%B3%BC-%ED%99%9C%EC%9A%A9%EB%B2%95</id><content type="html" xml:base="https://ycseong07.github.io/2025/1215_mcp-model-context-protocol-%EA%B0%9C%EB%85%90%EA%B3%BC-%ED%99%9C%EC%9A%A9%EB%B2%95/"><![CDATA[<p>LLM에 도구를 붙이는 일은 처음에는 단순했다. OpenAI가 function calling을 발표한 2023년에는 모델이 호출할 함수의 JSON Schema를 프롬프트에 같이 넣어 주면 되었다. 다만 도구가 늘고, 모델이 늘고, 에이전트가 늘면서 도구를 어떻게 정의하고 어떻게 연결할지의 표준이 부재한 상태가 점점 비용으로 돌아왔다. Cursor, Claude Code, Cline에 같은 GitHub 도구를 붙이려면 호스트마다 어댑터를 따로 구현해야 했고, 그렇게 만들어 둔 어댑터는 호스트가 API를 한 번만 바꿔도 통째로 다시 짜야 했다.</p>

<p>MCP(Model Context Protocol)는 이 지점을 정리하기 위해 Anthropic이 <a href="https://www.anthropic.com/news/model-context-protocol">2024년 11월에 공개</a>한 오픈 표준이다. LLM 호스트와 외부 도구, 데이터 사이를 매개하는 통신 규격을 하나로 통일해 두면, 도구를 만든 쪽은 한 번만 구현하면 되고 호스트를 만든 쪽도 한 번만 통합하면 된다. 비유하자면 USB-C 같은 역할인데, 실제로 Anthropic이 발표문에서 같은 비유를 쓰기도 했다.</p>

<h1 id="왜-표준이-필요했나">왜 표준이 필요했나</h1>

<p>LLM 에이전트가 외부와 닿는 방법은 크게 셋이다. 모델이 가진 도구 호출 능력, 호스트가 코드로 미리 박아둔 통합, 그리고 RAG로 끌어오는 외부 문서다. 이중 첫 번째와 두 번째가 빠르게 늘면서 호스트마다 통합 표면적이 커졌다. Claude Desktop은 Claude Desktop 방식대로, Cursor는 Cursor 방식대로, IDE 플러그인은 또 다른 방식대로 도구를 정의했고, 같은 GitHub 연동이라도 코드를 따로 만들어야 했다.</p>

<p>도구를 만드는 쪽 입장에서는 통합 비용이 N×M으로 불어난다. 도구 N개와 호스트 M개를 곱한 만큼의 어댑터가 필요해진다는 뜻이다. MCP는 이 곱셈을 N+M으로 떨어뜨리는 게 목적이다. 도구 제공자는 MCP 서버 하나만 만들고, 호스트는 MCP 클라이언트 하나만 구현하면 양쪽이 알아서 만나게 된다.</p>

<p>또 다른 압력은 컨텍스트 엔지니어링 쪽에서 왔다. 컨텍스트에 모든 것을 넣지 않고 필요할 때만 도구로 끌어오는 패턴이 정착하면서, 도구 호출의 횟수와 다양성이 빠르게 늘었다. 호스트가 호출하는 도구가 30~40개를 넘어가면 호스트 내부에서 일일이 관리하기가 어려워지고, 도구 정의, 인증, 로깅을 외부 프로세스로 떼어내는 편이 운영상 자연스러워진다. MCP는 이 분리를 강제한다.</p>

<h1 id="mcp의-구조">MCP의 구조</h1>

<p>MCP는 호스트(host), 클라이언트(client), 서버(server)의 세 가지 역할로 구성된다.</p>

<p>호스트는 사용자가 직접 쓰는 LLM 애플리케이션을 말한다. Claude Desktop, Claude Code CLI, Cursor, Zed, Cline 같은 것들이 호스트다. 호스트는 모델과 사용자, 그리고 여러 MCP 서버 사이를 중재한다.</p>

<p>클라이언트는 호스트 안에서 MCP 서버 하나당 하나씩 만들어지는 연결 객체다. 호스트가 MCP 서버 5개를 붙였다면 내부적으로 클라이언트 5개가 떠 있는 셈이다. 클라이언트는 서버와 1:1로 JSON-RPC 메시지를 주고받으며, 자기가 담당하는 서버의 도구, 리소스, 프롬프트를 호스트에 노출한다.</p>

<p>서버는 외부 시스템에 연결되어 실제 작업을 수행하는 프로세스다. GitHub MCP 서버, Filesystem MCP 서버, Slack MCP 서버처럼 한 가지 도메인을 책임지는 단위로 만들어진다. 서버는 호스트와 같은 머신에서 stdio로 도는 경우도 있고, 원격에서 HTTP, SSE로 붙는 경우도 있다.</p>

<p>서버가 호스트에 노출하는 것은 세 종류다. Tools는 모델이 호출할 수 있는 함수, Resources는 모델이 읽을 수 있는 데이터(파일, DB row 등), Prompts는 사용자가 슬래시 커맨드처럼 꺼내 쓸 수 있는 프롬프트 템플릿이다. 이 셋의 분리가 중요한 이유는 권한과 트리거가 다르기 때문이다. Tools는 모델이 자율적으로 부르고, Resources는 호스트가 컨텍스트에 끼워 넣고, Prompts는 사람이 명시적으로 호출한다.</p>

<p>전송 계층은 두 가지다. 로컬 서버는 stdio로, 원격 서버는 HTTP+SSE(2025년 중반 이후로는 Streamable HTTP)로 동작한다. 메시지 포맷은 JSON-RPC 2.0을 그대로 따른다. 즉 새로운 와이어 프로토콜을 만든 게 아니라, 잘 알려진 규격 위에 LLM 도구 연동에 필요한 메서드(<code class="language-plaintext highlighter-rouge">tools/list</code>, <code class="language-plaintext highlighter-rouge">tools/call</code>, <code class="language-plaintext highlighter-rouge">resources/read</code> 등)를 정의한 형태다.</p>

<h1 id="claude-code-cli에서-mcp-붙이기">Claude Code CLI에서 MCP 붙이기</h1>

<p>Claude Code CLI는 두 가지 방식으로 MCP 서버를 등록할 수 있다. 설정 파일을 직접 편집하거나, <code class="language-plaintext highlighter-rouge">claude mcp</code> 서브커맨드로 등록하는 방법이다. 일회성으로 붙여 보고 버릴 거라면 CLI가 편하고, 팀에 공유할 거라면 파일에 박아 두는 편이 낫다.</p>

<p>CLI로 등록하는 예시는 다음과 같다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 로컬 stdio 서버 등록</span>
claude mcp add filesystem <span class="nt">--scope</span> project <span class="nt">--</span> npx <span class="nt">-y</span> @modelcontextprotocol/server-filesystem /Users/me/projects

<span class="c"># 원격 HTTP 서버 등록</span>
claude mcp add tavily <span class="nt">--scope</span> user <span class="nt">--transport</span> http https://mcp.tavily.com/mcp <span class="se">\</span>
  <span class="nt">--header</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TAVILY_API_KEY</span><span class="s2">"</span>

<span class="c"># 등록 상태 확인</span>
claude mcp list
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--scope</code>는 등록 범위를 정한다. <code class="language-plaintext highlighter-rouge">local</code>은 현재 디렉토리에서만, <code class="language-plaintext highlighter-rouge">project</code>는 <code class="language-plaintext highlighter-rouge">.mcp.json</code>에 저장되어 팀과 공유, <code class="language-plaintext highlighter-rouge">user</code>는 홈 디렉토리에 저장되어 모든 프로젝트에서 공통으로 쓰인다. 비밀 키가 들어가는 서버는 <code class="language-plaintext highlighter-rouge">user</code> 스코프, 팀이 함께 써야 하는 서버는 <code class="language-plaintext highlighter-rouge">project</code> 스코프로 두는 식이 무난하다.</p>

<p>파일로 직접 관리할 때는 프로젝트 루트의 <code class="language-plaintext highlighter-rouge">.mcp.json</code>이 기준이 된다. 형식은 Claude Desktop의 그것과 거의 같다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"filesystem"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@modelcontextprotocol/server-filesystem"</span><span class="p">,</span><span class="w"> </span><span class="s2">"."</span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"github"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"run"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-i"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--rm"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-e"</span><span class="p">,</span><span class="w"> </span><span class="s2">"GITHUB_PERSONAL_ACCESS_TOKEN"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ghcr.io/github/github-mcp-server"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"GITHUB_PERSONAL_ACCESS_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${GITHUB_TOKEN}"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>세션을 띄운 뒤에는 <code class="language-plaintext highlighter-rouge">/mcp</code>로 현재 연결 상태와 서버별로 노출된 도구, 리소스, 프롬프트를 확인할 수 있다. 도구가 잘 안 잡히거나 인증이 어긋나면 거의 항상 이 화면에서 단서가 나온다.</p>

<h1 id="자주-쓰는-서버들">자주 쓰는 서버들</h1>

<p>실제로 자주 붙이는 서버는 도메인별로 정형화되어 있다.</p>

<p>검색 쪽에서는 Tavily나 Exa의 MCP 서버를 가장 많이 본다. 호스트의 기본 웹 검색은 일반 검색엔진을 그대로 호출하는 경우가 많아 LLM 입력에 어울리지 않는 노이즈가 섞여 들어오는 반면, 위 서비스들은 LLM 입력용으로 정제된 결과를 돌려준다는 점이 차이가 크다.</p>

<p>코드, 문서 쪽에서는 GitHub MCP 서버, Filesystem MCP 서버, 그리고 <a href="https://context7.com">Context7</a>이 자주 보인다. Context7은 라이브러리 공식 문서를 버전별로 끌어올 수 있어서, 모델이 학습 시점에 못 본 최신 API 문서를 쓰게 만들 때 효과적이다. Filesystem 서버는 Claude Code 자체가 파일 도구를 갖고 있어 중복되지만, 프로젝트 디렉토리 바깥의 참조 자료를 별도 루트로 노출하고 싶을 때 따로 붙인다.</p>

<p>데이터베이스, 관측성 쪽에서는 PostgreSQL, BigQuery, Sentry, Linear의 공식, 비공식 서버가 흔하다. 이쪽은 권한 모델이 까다로워서, 가능하면 읽기 전용 자격증명만 노출하는 서버를 따로 두는 편이 안전하다.</p>

<p>브라우저 자동화로는 Playwright MCP 서버가 가장 많이 쓰인다. UI 변경을 에이전트가 직접 검증하게 만들 때, 스크린샷을 컨텍스트로 되돌려 받을 수 있다는 점에서 시각적 회귀 테스트와 잘 맞는다.</p>

<h1 id="mcp-도입으로-맞닥뜨리게-되는-문제들">MCP 도입으로 맞닥뜨리게 되는 문제들</h1>

<p>첫째로, MCP를 도입하면 도구가 빠르게 늘어난다. 늘어난 만큼 호스트의 도구 카탈로그도 부풀어 오르는데, 도구가 50개를 넘어가면 모델이 비슷한 이름의 도구 중 엉뚱한 쪽을 부르는 일이 잦아진다. 한 서버 안에서도 <code class="language-plaintext highlighter-rouge">search_code</code>와 <code class="language-plaintext highlighter-rouge">search_repositories</code>처럼 기능이 겹치는 도구가 같이 노출되면 호출 정확도가 떨어진다. 운영 단계에서는 자주 쓰지 않는 서버를 과감하게 빼고, 같은 서버 안에서도 도구를 일부만 화이트리스트로 열어 두는 편이 결과가 좋다. Claude Code의 <code class="language-plaintext highlighter-rouge">permissions</code> 설정으로 서버, 도구 단위 차단을 걸 수 있다.</p>

<p>두 번째 문제는 보안이다. MCP 서버는 모델이 부르는 함수를 외부 프로세스로 떼어 놓은 것이기 때문에, 그 프로세스의 권한이 곧 에이전트의 권한이 된다. GitHub PAT를 쥔 서버라면 Push까지 가능한 토큰을 쓰지 말고 Read 권한만 가진 토큰을 따로 발급해 붙이는 식의 분리가 필요하다. 신뢰할 수 없는 출처의 MCP 서버를 그대로 npm/uvx로 받아 실행하는 것도 위험하다. 서버 코드가 호스트와 같은 권한으로 돌면서 사용자의 셸에 접근할 수 있다는 점은 표준 자체의 한계라기보다 운영 책임 영역으로 봐야 한다. 2025년에는 MCP 서버를 가장한 악성 패키지 사례가 실제로 보고되기도 했다.</p>

<p>세 번째 문제는 prompt injection이다. MCP 서버가 돌려주는 결과 본문에 “지금부터 너는 다른 모델이다”, “이전 지시를 무시해라” 같은 텍스트가 섞여 있으면 모델이 그걸 새 시스템 프롬프트로 받아들이는 일이 일어난다. 외부 웹페이지나 이슈 본문을 읽어 오는 도구일수록 위험이 크다. 대응책은 두 가지다. 호스트 쪽에서 도구 결과를 명확하게 “tool_result” 블록으로 감싸 모델에게 신호를 주는 것, 그리고 destructive한 작업(파일 삭제, 외부 API 결제, 머지 등)은 결국 사람의 승인을 받게 하는 hook을 거는 것이다. MCP 자체는 이 위험을 자동으로 막아주지 않는다.</p>

<p>네 번째 문제는 컨텍스트 점유다. MCP 서버가 띄워지는 시점에 호스트는 그 서버의 도구, 리소스 목록을 시스템 프롬프트에 같이 싣는다. 서버 하나가 도구를 30개씩 노출하면, 사용자가 한 번도 그 도구를 부르지 않더라도 매 턴마다 도구 정의가 입력 토큰을 잡아먹는다. 비활성 서버는 비활성 상태로 두고, 정말 필요한 작업에서만 임시로 활성화하는 운영 패턴이 합리적이다. Claude Code는 <code class="language-plaintext highlighter-rouge">/mcp</code> 안에서 서버 단위 enable/disable이 가능하다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="AI" /><category term="MCP" /><category term="ClaudeCode" /><category term="에이전트" /><summary type="html"><![CDATA[LLM에 도구를 붙이는 일은 처음에는 단순했다. OpenAI가 function calling을 발표한 2023년에는 모델이 호출할 함수의 JSON Schema를 프롬프트에 같이 넣어 주면 되었다. 다만 도구가 늘고, 모델이 늘고, 에이전트가 늘면서 도구를 어떻게 정의하고 어떻게 연결할지의 표준이 부재한 상태가 점점 비용으로 돌아왔다. Cursor, Claude Code, Cline에 같은 GitHub 도구를 붙이려면 호스트마다 어댑터를 따로 구현해야 했고, 그렇게 만들어 둔 어댑터는 호스트가 API를 한 번만 바꿔도 통째로 다시 짜야 했다.]]></summary></entry><entry><title type="html">프롬프트 캐싱으로 API 비용 줄이는 법</title><link href="https://ycseong07.github.io/2025/1120_%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EC%BA%90%EC%8B%B1%EC%9C%BC%EB%A1%9C-api-%EB%B9%84%EC%9A%A9-%EC%A4%84%EC%9D%B4%EB%8A%94-%EB%B2%95/" rel="alternate" type="text/html" title="프롬프트 캐싱으로 API 비용 줄이는 법" /><published>2025-11-20T09:00:00+09:00</published><updated>2025-11-20T09:00:00+09:00</updated><id>https://ycseong07.github.io/2025/%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EC%BA%90%EC%8B%B1%EC%9C%BC%EB%A1%9C-API-%EB%B9%84%EC%9A%A9-%EC%A4%84%EC%9D%B4%EB%8A%94-%EB%B2%95</id><content type="html" xml:base="https://ycseong07.github.io/2025/1120_%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EC%BA%90%EC%8B%B1%EC%9C%BC%EB%A1%9C-api-%EB%B9%84%EC%9A%A9-%EC%A4%84%EC%9D%B4%EB%8A%94-%EB%B2%95/"><![CDATA[<p>LLM API를 어느 정도 진지하게 쓰기 시작하면 청구서가 가장 먼저 비명을 지르는 자리가 두 군데다. 하나는 길어지는 시스템 프롬프트, 또 하나는 멀티턴 대화의 누적된 히스토리다. 두 경우 모두 같은 텍스트를 매 요청마다 다시 보내고 있는데, 모델 입장에서도 매번 처음 보는 토큰처럼 attention을 다시 계산한다. 사용자도 비싸고 제공사도 비싼 구조가 한동안 그대로 굴러갔다.</p>

<p>프롬프트 캐싱은 이 비효율을 정공법으로 푸는 기능이다. Anthropic이 2024년 8월에 베타로 <a href="https://www.anthropic.com/news/prompt-caching">도입</a>했고, OpenAI는 같은 해 10월에 자동 캐싱 형태로 따라왔으며, Google Gemini는 명시적 캐시 객체를 만드는 방식으로 같은 시기에 합류했다. 2025년 들어서는 캐시 사용이 LLM 애플리케이션 비용 구조의 기본 전제가 되었고, Claude Code CLI도 사용자가 별도 설정을 하지 않아도 내부적으로 캐싱을 적극적으로 활용한다. 이 글에서는 프롬프트 캐싱이 무엇이고, Claude Code에서 어떻게 적용되며, 캐시 효율을 끌어올리려면 어떤 점을 신경 써야 하는지를 정리한다.</p>

<h1 id="프롬프트-캐싱이-풀어주는-문제">프롬프트 캐싱이 풀어주는 문제</h1>

<p>LLM 추론에서 입력 토큰 처리는 보통 두 단계로 나뉜다. prefill 단계에서 입력 전체에 대해 attention을 한 번 계산하고, decode 단계에서 한 토큰씩 출력을 낸다. prefill 단계가 입력 길이에 비례해서 비용과 지연시간을 차지한다. 입력 100K 토큰짜리 프롬프트는 출력이 짧아도 prefill 비용이 거의 그대로 들어간다.</p>

<p>문제는 같은 prefix를 반복해서 보내는 패턴이 LLM 워크로드에서 흔하다는 점이다. 코드 에이전트는 같은 시스템 프롬프트와 도구 정의를 모든 요청에 붙이고, RAG 시스템은 같은 매뉴얼을 매 질문마다 다시 보내며, 멀티턴 챗봇은 이전 턴 전체를 다음 요청에 포함시킨다. 1만 토큰짜리 시스템 프롬프트로 하루 1,000번 호출하면 그것만으로 1,000만 토큰을 처리한 비용이 청구된다.</p>

<p>프롬프트 캐싱은 이 prefix 부분의 KV 캐시를 제공사 서버 측에 잠시 보관해 두고, 같은 prefix가 다시 들어오면 prefill을 다시 돌리지 않고 재사용한다. 모델 입장에서는 동일한 attention 계산을 다시 할 이유가 없으니 비용도 떨어지고 첫 토큰까지의 지연시간도 줄어든다.</p>

<h1 id="anthropic-캐싱이-동작하는-방식">Anthropic 캐싱이 동작하는 방식</h1>

<p>Anthropic의 프롬프트 캐싱은 명시적이다. 요청 본문 안에 <code class="language-plaintext highlighter-rouge">cache_control</code> 마커를 박아 어디까지가 캐시 대상인지 직접 지정한다. 마커는 한 요청에 최대 4개까지 둘 수 있고, 각 마커 위치에 도달했을 때까지의 prefix가 하나의 캐시 블록으로 잡힌다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">messages</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
        <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">:</span> <span class="n">LARGE_SYSTEM_PROMPT</span><span class="p">,</span>
             <span class="sh">"</span><span class="s">cache_control</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">ephemeral</span><span class="sh">"</span><span class="p">}}</span>
        <span class="p">]</span>
    <span class="p">},</span>
    <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">이번 턴 질문</span><span class="sh">"</span><span class="p">}</span>
<span class="p">]</span>
</code></pre></div></div>

<p>가격 구조는 캐시 쓰기와 읽기가 다르다. 2025년 11월 기준 Claude Sonnet 4.5의 base 입력가가 100만 토큰당 $3인데, 캐시 쓰기는 5분 TTL일 때 $3.75, 1시간 TTL일 때 $6, 캐시 읽기는 $0.30이다. 즉 처음 한 번은 25%~100% 비싸게 쓰고, 두 번째 호출부터는 base의 10% 가격으로 읽는다. 캐시가 한 번이라도 재사용되면 손익분기를 넘고, 두 번째 호출부터는 거의 무료에 가깝다.</p>

<p>TTL은 기본 5분이다. 같은 prefix가 5분 안에 다시 들어오면 캐시 적중, 그렇지 않으면 캐시는 만료되고 다시 써야 한다. 1시간 TTL은 옵션이고, 가격이 두 배지만 사용 빈도가 낮은 prefix를 길게 살려둘 수 있다. 사내 챗봇처럼 사용자 트래픽이 띄엄띄엄 들어오는 워크로드에는 1시간 TTL이 어울리고, 코딩 에이전트처럼 같은 세션 안에서 연속으로 호출이 일어나는 경우에는 5분으로도 충분하다.</p>

<p>캐시 대상이 되려면 최소 길이를 넘어야 한다. Claude Sonnet과 Opus는 1,024 토큰 이상, Haiku는 2,048 토큰 이상부터 캐시 가능하다. 짧은 시스템 프롬프트는 굳이 마커를 박아도 캐싱이 적용되지 않는다는 뜻이다. 마커가 prefix의 어디에 위치하든 캐시는 메시지의 처음부터 그 마커까지의 누적 prefix를 기준으로 잡힌다는 점도 함께 기억해 둘 만하다.</p>

<h1 id="claude-code-cli에서의-캐싱">Claude Code CLI에서의 캐싱</h1>

<p>Claude Code CLI는 프롬프트 캐싱을 내부적으로 적극적으로 활용한다. 사용자가 직접 <code class="language-plaintext highlighter-rouge">cache_control</code>을 넣을 일은 없고, 대신 CLI가 어떤 토큰까지를 캐시 가능한 prefix로 묶을지를 자동으로 판단한다.</p>

<p>크게 세 영역이 캐시 대상이다. 첫째는 시스템 프롬프트와 도구 정의 묶음이다. Claude Code의 시스템 프롬프트는 작업 지침, 환경 정보, 등록된 도구 스키마를 합쳐 보통 1만~2만 토큰을 넘는다. 여기에 MCP 서버 한두 개가 붙으면 도구 정의가 더 늘어난다. 이 prefix는 세션 동안 거의 변하지 않으므로 매 요청마다 캐시 적중이 일어난다.</p>

<p>둘째는 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>와 그 하위 메모리 파일이다. 프로젝트 루트, 사용자 홈 디렉토리, 부모 디렉토리에서 자동 로드되는 이 파일들은 시스템 프롬프트 영역에 합쳐져 들어가므로 같은 캐시 라인 안에서 처리된다. 이 자리에 RAG처럼 자주 바뀌는 컨텍스트를 끼워 넣으면 매 호출마다 캐시가 깨진다는 점을 의식해 둘 필요가 있다.</p>

<p>셋째는 대화 히스토리다. Claude Code는 멀티턴이 진행될수록 이전 턴들을 prefix로 누적해 보내는데, 누적된 prefix의 끝부분 가까이에 캐시 마커를 갱신해 가며 진행한다. 이전 턴에서 읽은 큰 파일이나 grep 결과 같은 무거운 컨텍스트가 다음 턴에서도 입력 비용 없이 재활용되는 이유가 여기에 있다.</p>

<p>캐시 사용량은 <code class="language-plaintext highlighter-rouge">/cost</code> 명령으로 확인할 수 있다. cache creation, cache read 토큰이 각각 따로 표시되고, 정상적인 코딩 세션이라면 누적 토큰 중 80% 이상이 cache read 자리에서 잡힌다. 이 비율이 낮게 나온다면 prefix가 자주 바뀌는 패턴이 어딘가에 끼어 있다는 신호다. <code class="language-plaintext highlighter-rouge">/context</code> 명령은 입력 토큰의 구성 비율을 보여주므로 어느 영역이 컨텍스트를 차지하고 있는지 함께 점검할 수 있다.</p>

<h1 id="캐시-효율을-떨어뜨리는-패턴들">캐시 효율을 떨어뜨리는 패턴들</h1>

<p>캐시는 prefix 일치를 전제로 동작하기 때문에, 매 요청마다 prefix 앞쪽이 조금씩 달라지는 구성에서는 캐시 적중률이 빠르게 무너진다. 실전에서 자주 나오는 실수는 비슷한 형태를 띤다.</p>

<p>첫 번째는 시스템 프롬프트나 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> 안에 현재 시각, 랜덤 ID, 사용자 식별자처럼 매번 바뀌는 값을 넣어 두는 경우다. 변동값이 prefix의 위쪽에 있으면 그 아래의 모든 토큰이 캐시 대상에서 빠진다. 변동값은 가능한 한 prefix의 끝쪽, 사용자 메시지 직전에 두는 편이 안전하다. 정말 필요한 경우라면 시간 정보를 분 단위가 아니라 시간 단위 또는 일 단위로 둥글려 prefix 안정성을 회복시키는 식으로 절충할 수도 있다.</p>

<p>두 번째는 RAG 결과를 시스템 프롬프트 영역에 끼워 넣는 패턴이다. retrieval 결과는 매 질문마다 달라지므로 캐시 적중을 매번 깨뜨린다. retrieval로 받아온 문서는 사용자 메시지 쪽에 붙여 시스템 prefix를 안정시키는 편이 비용 면에서 유리하다. 또는 retrieval 자체를 도구 호출로 빼서, 도구 결과가 대화 히스토리의 뒤쪽에 자연스럽게 누적되도록 두는 구성도 자주 쓰인다.</p>

<p>세 번째는 <code class="language-plaintext highlighter-rouge">/clear</code>를 자주 누르는 습관이다. <code class="language-plaintext highlighter-rouge">/clear</code>는 현재 컨텍스트를 비우고 새 세션을 시작하는데, 이 시점에서 5분 TTL 캐시는 사실상 의미를 잃는다. 작업이 정말 끊어진 게 아니라면 같은 세션 안에서 계속 진행하고, 컨텍스트가 너무 커졌다고 느껴질 때만 <code class="language-plaintext highlighter-rouge">/compact</code>로 요약하는 편이 캐시를 유지하면서 비용을 관리하기에 낫다. <code class="language-plaintext highlighter-rouge">/compact</code> 자체는 캐시를 깨지만, 그 다음부터는 줄어든 prefix 위에서 새 캐시 라인이 형성된다.</p>

<p>네 번째는 멀티 에이전트 구성에서 sub-agent의 시스템 프롬프트를 매번 다르게 만드는 경우다. sub-agent는 매번 새로운 컨텍스트로 호출되기 때문에, 그쪽 시스템 프롬프트가 안정적이지 않으면 캐시 적중이 거의 일어나지 않는다. 자주 호출되는 sub-agent라면 같은 프롬프트 템플릿을 고정해 두고 변동값만 user 메시지로 넘기는 형태가 합리적이다.</p>

<h1 id="정리">정리</h1>

<p>프롬프트 캐싱은 LLM 비용 구조에서 가장 단순하면서 효과가 큰 최적화 수단이다. 같은 prefix를 반복해서 보내는 거의 모든 워크로드에서 호출당 비용이 한 자릿수 분의 1로 떨어진다. Claude Code CLI 사용자라면 별도의 코드 작업 없이도 캐싱의 혜택을 받고 있을 가능성이 높지만, prefix 안정성을 의식하지 않으면 그 혜택의 상당 부분이 빠져나갈 수 있다.</p>

<p>실제로 챙길 점은, 1. 시스템 프롬프트와 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>에는 자주 바뀌는 값을 넣지 말 것, 2.변동 정보는 prefix의 끝쪽 또는 사용자 메시지에 둘 것, 3. 같은 세션을 가능한 한 길게 유지할 것, 4. 비용 점검은 <code class="language-plaintext highlighter-rouge">/cost</code>와 <code class="language-plaintext highlighter-rouge">/context</code>로 정기적으로 들여다볼 것 정도로 정리할 수 있겠다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><category term="AI" /><category term="LLM" /><category term="API" /><category term="프롬프트캐싱" /><summary type="html"><![CDATA[LLM API를 어느 정도 진지하게 쓰기 시작하면 청구서가 가장 먼저 비명을 지르는 자리가 두 군데다. 하나는 길어지는 시스템 프롬프트, 또 하나는 멀티턴 대화의 누적된 히스토리다. 두 경우 모두 같은 텍스트를 매 요청마다 다시 보내고 있는데, 모델 입장에서도 매번 처음 보는 토큰처럼 attention을 다시 계산한다. 사용자도 비싸고 제공사도 비싼 구조가 한동안 그대로 굴러갔다.]]></summary></entry><entry><title type="html">AI 개발자가 희망하는 AI 기획자</title><link href="https://ycseong07.github.io/2025/0409_ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%ED%9D%AC%EB%A7%9D%ED%95%98%EB%8A%94-ai-%EA%B8%B0%ED%9A%8D%EC%9E%90/" rel="alternate" type="text/html" title="AI 개발자가 희망하는 AI 기획자" /><published>2025-04-09T09:00:00+09:00</published><updated>2025-04-09T09:00:00+09:00</updated><id>https://ycseong07.github.io/2025/AI-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%ED%9D%AC%EB%A7%9D%ED%95%98%EB%8A%94-AI-%EA%B8%B0%ED%9A%8D%EC%9E%90</id><content type="html" xml:base="https://ycseong07.github.io/2025/0409_ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%ED%9D%AC%EB%A7%9D%ED%95%98%EB%8A%94-ai-%EA%B8%B0%ED%9A%8D%EC%9E%90/"><![CDATA[<p>최근 카카오뱅크에서 올라온 <a href="https://recruit.kakaobank.com/jobs/210932">AI 기획자 채용 공고</a>를 우연히 접했다. ‘AI 기획자’라는 잡 타이틀이 등장한지는 꽤 됐지만, 지금까지는 (신생 포지션이 으레 그렇듯)기존의 기획자의 역할과 JD가 명확히 구분되지 않는다고 느끼고 있었다. 그런데 이번 카카오페이의 AI 기획자 공고와 인터뷰에서는 ‘AI 기획자’의 잡 포지션의 역할을 고민하며 찾아가고 있다는 생각이 들었다. 해당 팀의 인터뷰를 읽어보니, 기존 기획자와는 다른 역할을 해야한다고 인식하고 있지만, 그 역할이 정확히 무엇인지에 대한 고민은 아직 현재진행형이라는 느낌도 동시에 받았다. 그럼에도, “기존의 기획자와 구별되는 AI 기획자가 필요하다”는 인식이 자리 잡고 있는 건 분명해 보였다. 개인적으로는 우대사항에 ‘AI 에이전트에 대한 폭넓고 다양한 경험과 스터디를 하고 있는 분’이라는 조건이 붙어있는 점, 인터뷰 중 ‘단순히 툴이 바뀌는 것이 아니라 개발, 기획, 디자인, 운영 등 모든 사고방식을 완전히 바꿔야 한다’는 것을 언급했다는 점이 인상깊었다. 그리고 ‘기획자’와 구분되는 ‘AI 기획자’라는 잡 타이틀을 표방한다면, 그 차별점은 무엇이어야 할지 의문이 생겼다.</p>

<p>내친김에 AI 기획자의 역할이 무엇인지 생각해보고, AI 개발자 입장에서 ‘AI 기획자는 이랬으면 좋겠다’라는 바람을 정리해봤다(이래야 한다는 당위적인 주장은 아님을 밝힌다). 당연하게 들릴 수 있지만, AI가 포함된 서비스를 기획할 때는 데이터와 AI 기술에 대한 기본적인 이해가 필요하다. 그리고 AI 기획자와 기존의 소프트웨어 기획자의 차별점은 <strong>“데이터로 AI가 만들어지는 과정에 대한 이해도”</strong>여야 한다고 생각한다. 굳이 대입해 생각해보자면, PM(Product Manager)과 TPM(Technical Product Manager)이 구분되는 지점과 비슷하게 생각해볼 수 있겠다.</p>

<p><em>(최근 AI 기획/개발이라고 하면 생성형 AI, 특히 LLM에 관한 기획/개발을 주로 의미하지만, 이 글에서는 보다 폭 넓게 ML/DL/생성형 모델 등을 모두 포괄하는 의미로 ‘AI’를 사용했음을 밝힌다)</em></p>

<h1 id="ai-개발자와-ai-기획자">AI 개발자와 AI 기획자</h1>

<p>새로운 잡 포지션의 역할을 정할 때는, 기존의 가까운 잡 포지션의 역할과 구분되는 지점을 찾아보면 좋다. AI 개발자의 Job Description을 살펴보면 일반적으로 다음 업무가 포함된다.</p>

<ul>
  <li>데이터 파이프라인 및 인프라 설계: 데이터 수집, DB 설계, ETL 파이프라인 개발, 데이터 전처리 자동화 등</li>
  <li>모델 개발: 알고리즘 선정, 구현, 성능 개선, 모델 튜닝, 모델 모니터링 및 버전 관리 등</li>
  <li>서비스 연동: MLOps, API 설계, 모델 배포, 성능 최적화 등</li>
</ul>

<p>일반적으로 AI 개발자는 데이터, 알고리즘, 시스템을 기술적으로 다룬다면, 데이터 전략을 포함한 사업 기획, 서비스가 궁극적으로 달성하고자 하는 목표에 맞는 평가 지표 설정, 서비스 방향성 결정, 사업적 요구사항 정의, 협력 기관 관리, 마케팅 및 사업 운영 기획 등은 기획자 혹은 PM/PO의 역할로 볼 수 있다. AI 모델 개발 과정은 데이터로 시작해서 평가(지표/벤치마크)로 마무리된다. 이 두 지점에 대해서만이라도 이해가 있다면 프로젝트가 훨씬 매끄럽게 진행될 수 있다. 만약 이러한 기획자의 역할이 공백 상태이거나 이들에게 AI 관련 지식이 부족해 AI 개발자에게 의존하게 될 경우, 프로젝트가 제때 시작되지 못하거나 AI 개발자의 업무에 오버로드가 발생할 수 있다.</p>

<p>지금과 같은 과도기 상태에서는 기존의 기획자들에게 AI 제품을 새롭게 기획해야 하는 업무가 주어지기도 하는데, 만약 기획자에게 AI 지식이 없는 상태에서 AI 프로젝트를 기획한다면 발생할 수 있는 문제를 생각나는대로 나열해 보면 다음과 같다.</p>

<ul>
  <li>모델을 위한 데이터 수집 전략이 프로젝트 시작 이후에 논의되면서, 데이터가 제 때 수집되지 못하거나 품질이 낮아지고, 궁극적으로 모델의 성능을 하락시킨다.</li>
  <li>AI가 서비스 품질에 긍정적 영향을 주는지 판단하지 못하고, ‘일단 AI로 할 수 있는’ 기능을 넣는 것을 목표로 하게 된다.</li>
  <li>AI 개발자의 기술적인 피드백을 제대로 이해하지 못해 현실적이지 않은 요구사항을 제시하거나, 개발 일정을 잘못 예측하게 된다.</li>
  <li>1년 동안 AI 프로젝트를 진행한다고 할 때, 데이터 수집→개발→테스트에 이르는 과정을 1년 동안 진행한다고 생각한다. 실제로는 단기간 안에 MVP를 만들고, 데이터를 추가 수집하고, 모델을 개선하는 작업을 여러 번 반복해야 한다.</li>
  <li>만들어진 AI 모델을 어떻게 평가해야 할지에 대해 깊이 고민하지 않는다. 그러나 실제로는 서비스가 달성하고자 하는 바와 연결되는 평가 지표나 각종 벤치마크에 대한 이해가 필요하며, 이것에 따라 모델링 방향이 정해지기 마련이다. 생성형 AI의 경우 프로젝트에서 목표로 하는 기능을 specific하게 평가할 수 있는 벤치마크가 아직 없을 수도 있고, 이 경우 대안적인 지표 설계가 필요하다. 평가 지표가 정해지지 않으면 AI 개발자는 어떤 식으로 모델을 개선해야할지 헤메게 되며, 프로젝트 후반에 새로운 평가 기준을 급하게 만드는 문제를 겪기도 한다.</li>
</ul>

<h1 id="ai-제품-개발의-전제">AI 제품 개발의 전제</h1>

<p>첫째, AI가 포함되는 서비스의 경우, 데이터 자체가 서비스의 가치를 크게 좌우한다. 이는 기획 단계에서 데이터 자체가 하나의 주요한 포인트로서 논의되어야 함을 뜻한다.</p>

<p>둘째, AI 모델 성능은 많은 경우 “어떤 데이터를 얼마나 잘 모았는가”에 따라 좌우된다. 타겟 예측 모델이라면 무엇을 예측할 것인지 명확히 정의하고, 타겟에 해당하는(혹은 준하는) 데이터를 충분히 모을 수 있어야 한다. 또한 기획 단계에서 사용자 특성(연령대, 성별 등)을 고려한 데이터 확보 전략을 세울 수 있다면 좋고, 데이터 윤리나 규제(개인정보 보호 등)에 대한 이해도 필요하다.</p>

<p>셋째, AI는 반복적 실험과 검증을 반복하며 완성에 다가가는 것이지, 기간 안에 한 번 개발을 완료하는 방식으로 끝나지 않는다. 물론 소프트웨어나 AI 프로젝트마다 스프린트나 R&amp;D 범위가 다를 수는 있겠지만, 많은 경우 짧은 기간 안에 PoC를 진행하고, 필요한 데이터를 보강하고, 모델을 지속적으로 개선하며, 결과를 반복적으로 검증하는 프로세스가 필요하다. 기획 단계에서부터 피드백 사이클이 어느 정도 설계되어야 개발 프로세스가 매끄럽게 진행될 수 있다.</p>

<p>넷째, AI 기술이 빠르게 발전하고 있는 현 양상을 기획 단계에서도 감안해야 한다. 지금 풀고자 하는 문제가 몇 개월 뒤에는 이미 해결되어 있을 수도 있고, 혹은 문제를 풀고자 하는 방식이 다른 방식으로 변화될 수 있다. 그러한 때가 다가왔을 때는 적절한 기획 수정이 필요할 수 있다.</p>

<h1 id="ai-개발자가-희망하는-ai-기획자의-역할">AI 개발자가 희망하는 AI 기획자의 역할</h1>

<p>기획자가 데이터와 (평가지표를 포함한)AI 기술을 이해하면, 단순히 ‘어떤 데이터와 기술을 사용해 어떤 모델을 만들겠다’는 수준이 아니라, 어떤 데이터를 왜 수집해야 하는지, AI의 개발이 사용자 문제와 어떻게 연결되는지, 왜 필요한 것인지, 갖는 기술적인 한계는 무엇인지 설명할 수 있게 된다. 임원과의 커뮤니케이션 뿐만 아니라 AI 개발자, 디자이너, 사업팀 간의 원활한 커뮤니케이션을 돕는 것이 AI 기획자 정체성의 많은 지분을 차지하게 될 것이다.</p>

<p>또한 AI 기획자는 기술 그 자체보다는, 기술이 어떻게 사용자 경험을 개선하고 비즈니스로 이어질 수 있는지를 고민해야 한다. 가령, AI로 특정 질병을 예측하는 기능을 도입할 때, 단순히 예측 정확도를 높이는 것뿐만 아니라, 그 기능을 어떤 타겟 유저에게 제공하고, 어느 지점에서 유료화할지, 결과를 어떻게 시각화하여 납득 가능하게 만들지를 계획해야 한다.</p>

<p>마지막으로, 사소해 보일 수 있지만 가장 중요하다고 생각하는 점은, 데이터에 대한 이해를 AI 개발자와 공유하는 것이다. 한 가지 사례를 언급하면, 종종 “그 모델 개발할 때 데이터는 몇 건이나 필요할까요?” 라는 질문을 받는데, 가장 답하기 까다로운 질문 중 하나다. 특히 내가 속해있는 헬스케어 분야는 규제, 개인정보, 데이터 접근성 등의 이유로 원하는 만큼의 데이터 수집이 쉽지 않다. “많으면 많을수록 좋다”는 게 정답이기는 하지만 실제 업무에 도움이 되는 답변은 아니다. 헬스케어 도메인의 AI 개발자의 입장에서, 나는 대부분 약 50명(혹은 그 이하) 정도의 데이터를 모아 모델 PoC를 진행하고, 가능성을 확인한 이후 N수를 확장해 핵심 지표를 확인하는 식으로 확장해보자고 제안한다. 이 때, n회차 데이터 수집과 n+1회차 데이터 수집에서 수집되는 데이터의 형태가 크게 달라지거나, 타겟이 변경되는 경우 서로 다른 데이터를 어떻게 활용할지(이전 데이터를 버리거나, n+1회차 데이터 수집에서 하위 호환성을 고려하는 등)도 AI 개발자와 함께 논의할 수 있다면 더욱 좋다. 즉, 진행 단계에 따라 필요한 데이터가 달라질 수 있음을 인지하고, 실제 수집 속도나 비용에 따라 조정하는 역할을 해줄 수 있어야 한다. 물론, 온라인에서 데이터를 수집할 수 있는 상황이라면 개발자가 직접 수집, 전처리 할테니 해당사항이 적다.</p>

<p>계속해서 데이터와 평가지표에 대해 강조하는 이유는, 데이터는 서비스의 가치를 높이는 핵심이 되고, 평가지표는 임원(을 비롯한 다양한 이해관계자)을 설득하는 근거가 되기 때문이다.</p>

<h1 id="ai-기획자에게-바라는-역량">AI 기획자에게 바라는 역량</h1>

<p>그렇다면 구체적으로 AI 기획자는 어떤 것들을 알고있어야 커뮤니케이션의 가교 역할을 잘 할 수 있을까? 이 역시 AI 개발자의 입장에서 생각나는 대로 정리해본다.</p>

<ul>
  <li>이해관계자와의 소통 능력</li>
  <li>AI 서비스가 왜, 어디에, 어떻게 필요한지를 파악해 서비스를 기획하는 능력</li>
  <li>비즈니스 모델 기획 및 사업 추진 능력</li>
  <li>AI 기술과 일반 소프트웨어 개발 프로세스의 차이 이해</li>
  <li>데이터셋 구성이나 라벨링, 불균형이나 노이즈 등의 기본적인 데이터 관련 지식</li>
  <li>AI 프로젝트 진행 방식에 대한 이해 (지속적인 개선 구조)</li>
  <li>MVP로부터의 확장 가능성을 고려한 로드맵 설정</li>
  <li>모델 평가 지표 혹은 벤치마크에 대한 이해</li>
  <li>어떤 데이터를 누구에게서, 어떤 방식으로, 얼마나 수집해야 할지에 대한 데이터 전략</li>
</ul>

<h1 id="결론">결론</h1>

<p>AI 기획자가 AI 개발자보다 기술을 더 잘 알아야 한다는 것은 아니다. 오히려, 데이터 중심으로 돌아가는 AI 개발 프로세스에서 ‘어떤 데이터를 왜 모아야 하고, 그것이 사용자와 비즈니스에 어떻게 기여하는가’를 설계, 조정, 설명하는 역할이 핵심이라고 생각한다. 만약 기존 기획자가 모든 걸 다 맡기 어렵다면, PM/PO가 사업적 프레임을 잡고, AI 기획자가 데이터·모델 관련 이해를 더 깊게 챙겨주는 식으로 역할을 분담해도 좋을 듯 하다. AI 개발자 역시 업무 범위를 확장시켜 AI 기획자와 적극적으로 소통해서 <a href="https://www.stdy.blog/glue-who-silently-keep-the-organization-running/">Glue Work</a>를 양측에서 함께 해결하면서, 임원/디자이너/사업팀에게는 AI 모델의 기능과 한계를 알기 쉽게 설명하고, 사용자에게는 AI가 주는 가치가 와닿도록 구현할 수 있는 AI 기획자가 있다면, AI를 활용한 서비스는 훨씬 더 빠르고 정확하게 사용자와 시장의 요구에 부응할 수 있을 것이다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><summary type="html"><![CDATA[최근 카카오뱅크에서 올라온 AI 기획자 채용 공고를 우연히 접했다. ‘AI 기획자’라는 잡 타이틀이 등장한지는 꽤 됐지만, 지금까지는 (신생 포지션이 으레 그렇듯)기존의 기획자의 역할과 JD가 명확히 구분되지 않는다고 느끼고 있었다. 그런데 이번 카카오페이의 AI 기획자 공고와 인터뷰에서는 ‘AI 기획자’의 잡 포지션의 역할을 고민하며 찾아가고 있다는 생각이 들었다. 해당 팀의 인터뷰를 읽어보니, 기존 기획자와는 다른 역할을 해야한다고 인식하고 있지만, 그 역할이 정확히 무엇인지에 대한 고민은 아직 현재진행형이라는 느낌도 동시에 받았다. 그럼에도, “기존의 기획자와 구별되는 AI 기획자가 필요하다”는 인식이 자리 잡고 있는 건 분명해 보였다. 개인적으로는 우대사항에 ‘AI 에이전트에 대한 폭넓고 다양한 경험과 스터디를 하고 있는 분’이라는 조건이 붙어있는 점, 인터뷰 중 ‘단순히 툴이 바뀌는 것이 아니라 개발, 기획, 디자인, 운영 등 모든 사고방식을 완전히 바꿔야 한다’는 것을 언급했다는 점이 인상깊었다. 그리고 ‘기획자’와 구분되는 ‘AI 기획자’라는 잡 타이틀을 표방한다면, 그 차별점은 무엇이어야 할지 의문이 생겼다.]]></summary></entry><entry><title type="html">ML 모델 평가하기 (Classification)</title><link href="https://ycseong07.github.io/2025/0216_ml-%EB%AA%A8%EB%8D%B8-%ED%8F%89%EA%B0%80%ED%95%98%EA%B8%B0-classification/" rel="alternate" type="text/html" title="ML 모델 평가하기 (Classification)" /><published>2025-02-16T09:00:00+09:00</published><updated>2025-02-16T09:00:00+09:00</updated><id>https://ycseong07.github.io/2025/ML-%EB%AA%A8%EB%8D%B8-%ED%8F%89%EA%B0%80%ED%95%98%EA%B8%B0-Classification</id><content type="html" xml:base="https://ycseong07.github.io/2025/0216_ml-%EB%AA%A8%EB%8D%B8-%ED%8F%89%EA%B0%80%ED%95%98%EA%B8%B0-classification/"><![CDATA[<p>훈련이 완료된 머신러닝 모델의 성능을 평가할 때 단순히 validation set, test set 성능으로만 판단하기엔 부족한 부분이 있습니다. 이번 글에서는 분류 모델이 데이터의 의미 있는 패턴을 학습했는지 확인하거나, 우연한 상관관계에 의한 결과인건지, 특정 데이터 분포에만 특화되어 있는지 등을 검증하기 위한 방법을 소개합니다. 아래는 예제 코드를 위한 라이브러리 리스트입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">collections</span> <span class="kn">import</span> <span class="n">Counter</span>
<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="n">seaborn</span> <span class="k">as</span> <span class="n">sns</span>
<span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">joblib</span>
<span class="kn">import</span> <span class="n">warnings</span>

<span class="kn">from</span> <span class="n">sklearn.preprocessing</span> <span class="kn">import</span> <span class="n">StandardScaler</span><span class="p">,</span> <span class="n">MinMaxScaler</span>
<span class="kn">from</span> <span class="n">sklearn.metrics</span> <span class="kn">import</span> <span class="n">accuracy_score</span><span class="p">,</span> <span class="n">roc_auc_score</span><span class="p">,</span> <span class="n">f1_score</span><span class="p">,</span> <span class="n">recall_score</span><span class="p">,</span> <span class="n">confusion_matrix</span>
<span class="kn">from</span> <span class="n">sklearn.ensemble</span> <span class="kn">import</span> <span class="n">HistGradientBoostingClassifier</span>
<span class="kn">from</span> <span class="n">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">StratifiedKFold</span><span class="p">,</span> <span class="n">train_test_split</span><span class="p">,</span> <span class="n">cross_val_score</span>
<span class="kn">from</span> <span class="n">sklearn.base</span> <span class="kn">import</span> <span class="n">clone</span>

<span class="kn">from</span> <span class="n">joblib</span> <span class="kn">import</span> <span class="n">Parallel</span><span class="p">,</span> <span class="n">delayed</span>

<span class="n">warnings</span><span class="p">.</span><span class="nf">filterwarnings</span><span class="p">(</span><span class="sh">'</span><span class="s">ignore</span><span class="sh">'</span><span class="p">)</span>

<span class="c1"># model, X_train, y_train, X_test, y_test 변수는 이미 정의된 상태라고 가정
</span></code></pre></div></div>

<h1 id="1-y-randomization">1. Y Randomization</h1>

<p>Y Randomization은 모델이 학습한 패턴이 진짜 의미 있는 것인지, 아니면 우연히 얻어진 상관관계에 의한 것인지 판별하고자 할 때 사용합니다. 통계에 익숙하신 분들은 p-value를 통한 검증방법과 비슷하다고 생각하시면 감이 쉽게 잡힐 수도 있습니다(0.05와 같은 rule of thumb이 있는 것은 아니지만). 예측해야 할 target 변수를 무작위로 뒤섞은 뒤에도 비슷한 수준의 정확도나 AUC가 나온다면, 이는 모델이 진짜 패턴이 아니라 우연적 상관관계를 학습했을 가능성이 크다는 뜻입니다. 반대로 뒤섞은 데이터에서는 성능이 크게 떨어지고, 원본 데이터에서만 높은 성능을 보인다면 모델이 의미 있는 정보를 찾아냈다고 볼 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">y_randomization</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">n_randomizations</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">random_seed</span><span class="o">=</span><span class="mi">42</span><span class="p">):</span>
    <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="n">random_seed</span><span class="p">)</span>
    
    <span class="n">original_model</span> <span class="o">=</span> <span class="nf">clone</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
    <span class="n">original_model</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
    
    <span class="n">y_pred_proba</span> <span class="o">=</span> <span class="n">original_model</span><span class="p">.</span><span class="nf">predict_proba</span><span class="p">(</span><span class="n">X_test</span><span class="p">)[:,</span> <span class="mi">1</span><span class="p">]</span>
    
    <span class="n">original_auc</span> <span class="o">=</span> <span class="nf">roc_auc_score</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">y_pred_proba</span><span class="p">)</span>
    
    <span class="n">random_aucs</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">n_randomizations</span><span class="p">):</span>
        <span class="n">y_train_random</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">permutation</span><span class="p">(</span><span class="n">y_train</span><span class="p">)</span>
        <span class="n">model_random</span> <span class="o">=</span> <span class="nf">clone</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
        <span class="n">model_random</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train_random</span><span class="p">)</span>

        <span class="n">y_pred_random_proba</span> <span class="o">=</span> <span class="n">model_random</span><span class="p">.</span><span class="nf">predict_proba</span><span class="p">(</span><span class="n">X_test</span><span class="p">)[:,</span> <span class="mi">1</span><span class="p">]</span>
        
        <span class="n">score</span> <span class="o">=</span> <span class="nf">roc_auc_score</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">y_pred_random_proba</span><span class="p">)</span>
        <span class="n">random_aucs</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">score</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
    <span class="n">sns</span><span class="p">.</span><span class="nf">histplot</span><span class="p">(</span><span class="n">random_aucs</span><span class="p">,</span> <span class="n">kde</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">skyblue</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">axvline</span><span class="p">(</span><span class="n">original_auc</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">red</span><span class="sh">"</span><span class="p">,</span> <span class="n">linestyle</span><span class="o">=</span><span class="sh">"</span><span class="s">--</span><span class="sh">"</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sh">"</span><span class="s">Original AUC</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">AUC Distribution</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">AUC</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Frequency</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">legend</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="k">return</span> <span class="n">original_auc</span><span class="p">,</span> <span class="n">random_aucs</span>

<span class="n">orig_auc</span><span class="p">,</span> <span class="n">rand_aucs</span> <span class="o">=</span> <span class="nf">y_randomization</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">n_randomizations</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">random_seed</span><span class="o">=</span><span class="mi">1234</span><span class="p">)</span>
</code></pre></div></div>

<h1 id="2-adversarial-validation">2. Adversarial Validation</h1>

<p>Adversarial Validation은 훈련 데이터와 테스트 데이터가 비슷한 분포를 가지고 있는지 확인하기 위해, 가벼운 이진 분류 모델을 사용해보는 방법입니다. 만약 분류기의 AUC가 0.5에 가깝게 나온다면, 이 둘을 구별하기가 어렵다는 뜻이므로 학습/테스트 데이터 간 분포 차이가 작고, 검증셋의 평가와 테스트셋의 평가 간 차이가 크지 않을 것이라고 예상해볼 수 있습니다. 반면 학습 데이터와 테스트 데이터가 눈에 띄게 다른 경우, 모델이 테스트 환경에서 기대만큼 좋은 성능을 내지 못할 수 있음을 의미합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">adversarial_validation</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">n_splits</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span> <span class="n">seed</span> <span class="o">=</span> <span class="mi">42</span><span class="p">):</span>
    <span class="n">X_train_np</span> <span class="o">=</span> <span class="n">X_train</span><span class="p">.</span><span class="n">values</span>
    <span class="n">X_test_np</span> <span class="o">=</span> <span class="n">X_test</span><span class="p">.</span><span class="n">values</span>

    <span class="n">X_adv</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">concatenate</span><span class="p">([</span><span class="n">X_train_np</span><span class="p">,</span> <span class="n">X_test_np</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">y_adv</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">concatenate</span><span class="p">([</span><span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">X_train_np</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span><span class="n">np</span><span class="p">.</span><span class="nf">ones</span><span class="p">(</span><span class="n">X_test_np</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">])])</span>

    <span class="n">adv_model</span> <span class="o">=</span> <span class="nc">HistGradientBoostingClassifier</span><span class="p">(</span><span class="n">max_iter</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">seed</span><span class="p">)</span> <span class="c1"># 결측치 허용하는 모델
</span>    <span class="n">skf</span> <span class="o">=</span> <span class="nc">StratifiedKFold</span><span class="p">(</span><span class="n">n_splits</span><span class="o">=</span><span class="n">n_splits</span><span class="p">,</span> <span class="n">shuffle</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">aucs</span> <span class="o">=</span> <span class="nf">cross_val_score</span><span class="p">(</span><span class="n">adv_model</span><span class="p">,</span> <span class="n">X_adv</span><span class="p">,</span> <span class="n">y_adv</span><span class="p">,</span> <span class="n">cv</span><span class="o">=</span><span class="n">skf</span><span class="p">,</span> <span class="n">scoring</span><span class="o">=</span><span class="sh">"</span><span class="s">roc_auc</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">fold_aucs</span><span class="sh">"</span><span class="p">:</span> <span class="n">aucs</span><span class="p">,</span> <span class="sh">"</span><span class="s">adversarial_auc</span><span class="sh">"</span><span class="p">:</span> <span class="nf">float</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">aucs</span><span class="p">))}</span>

<span class="n">adv_val_result</span> <span class="o">=</span> <span class="nf">adversarial_validation</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">n_splits</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">1234</span><span class="p">)</span>
<span class="n">fold_aucs</span> <span class="o">=</span> <span class="n">adv_val_result</span><span class="p">[</span><span class="sh">"</span><span class="s">fold_aucs</span><span class="sh">"</span><span class="p">]</span>
<span class="n">adversarial_auc_mean</span> <span class="o">=</span> <span class="n">adv_val_result</span><span class="p">[</span><span class="sh">"</span><span class="s">adversarial_auc</span><span class="sh">"</span><span class="p">]</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
<span class="n">sns</span><span class="p">.</span><span class="nf">boxplot</span><span class="p">(</span><span class="n">y</span><span class="o">=</span><span class="n">fold_aucs</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">lightblue</span><span class="sh">"</span><span class="p">)</span>
<span class="n">sns</span><span class="p">.</span><span class="nf">stripplot</span><span class="p">(</span><span class="n">y</span><span class="o">=</span><span class="n">fold_aucs</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">red</span><span class="sh">"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.7</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Mean AUC: </span><span class="si">{</span><span class="n">adversarial_auc_mean</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">AUC Score</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>

</code></pre></div></div>

<h1 id="3-perturbation-test">3. Perturbation Test</h1>

<p>Perturbation Test는 모델의 강건성(Robustness)을 확인하는 데 초점을 둡니다. 모델을 훈련시킬 때는 데이터를 많이 정제하지만, 테스트 환경에서는 데이터에 노이즈가 섞여 들어오는 경우가 훨씬 많습니다. 이때 모델이 입력 특성에 약간의 변동만 있어도 성능이 크게 떨어진다면, 특정 데이터나 패턴에 과도하게 의존하고 있을 수 있다는 뜻일 수 있습니다. 따라서 각 컬럼에 일정 비율(예시 코드에서는 10%)로 무작위 노이즈를 섞어 넣어 보고, 그 전후로 모델 성능이 얼마나 변하는지를 살펴볼 수 있습니다. 성능이 상대적으로 안정적으로 유지된다면, 모델이 다양한 상황에서 견고하게 동작할 가능성이 높다고 판단할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">raw_perturbation</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">perturb_size</span><span class="o">=</span><span class="mf">0.1</span><span class="p">):</span>
    <span class="n">X_perturbed</span> <span class="o">=</span> <span class="n">X</span><span class="p">.</span><span class="nf">copy</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="n">X</span><span class="p">.</span><span class="n">columns</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">np</span><span class="p">.</span><span class="nf">issubdtype</span><span class="p">(</span><span class="n">X</span><span class="p">[</span><span class="n">col</span><span class="p">].</span><span class="n">dtype</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">number</span><span class="p">):</span>
            <span class="n">std</span> <span class="o">=</span> <span class="n">X</span><span class="p">[</span><span class="n">col</span><span class="p">].</span><span class="nf">std</span><span class="p">()</span>
            <span class="n">noise</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">normal</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">perturb_size</span> <span class="o">*</span> <span class="n">std</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="n">X</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
            <span class="n">X_perturbed</span><span class="p">[</span><span class="n">col</span><span class="p">]</span> <span class="o">+=</span> <span class="n">noise</span>
    <span class="k">return</span> <span class="n">X_perturbed</span>

<span class="k">def</span> <span class="nf">evaluate_model</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
    <span class="n">y_pred</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">predict</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>
    <span class="n">y_prob</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">predict_proba</span><span class="p">(</span><span class="n">X</span><span class="p">)[:,</span> <span class="mi">1</span><span class="p">]</span>
    <span class="n">accuracy</span> <span class="o">=</span> <span class="nf">accuracy_score</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">)</span>
    <span class="n">f1</span> <span class="o">=</span> <span class="nf">f1_score</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">)</span>
    <span class="n">auc</span> <span class="o">=</span> <span class="nf">roc_auc_score</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_prob</span><span class="p">)</span>
    <span class="n">recall</span> <span class="o">=</span> <span class="nf">recall_score</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">)</span>
    <span class="n">tn</span><span class="p">,</span> <span class="n">fp</span><span class="p">,</span> <span class="n">fn</span><span class="p">,</span> <span class="n">tp</span> <span class="o">=</span> <span class="nf">confusion_matrix</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">).</span><span class="nf">ravel</span><span class="p">()</span>
    <span class="n">specificity</span> <span class="o">=</span> <span class="n">tn</span> <span class="o">/</span> <span class="p">(</span><span class="n">tn</span> <span class="o">+</span> <span class="n">fp</span><span class="p">)</span> <span class="nf">if </span><span class="p">(</span><span class="n">tn</span> <span class="o">+</span> <span class="n">fp</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="mi">0</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">'</span><span class="s">accuracy</span><span class="sh">'</span><span class="p">:</span> <span class="n">accuracy</span><span class="p">,</span> <span class="sh">'</span><span class="s">f1</span><span class="sh">'</span><span class="p">:</span> <span class="n">f1</span><span class="p">,</span> <span class="sh">'</span><span class="s">auc</span><span class="sh">'</span><span class="p">:</span> <span class="n">auc</span><span class="p">,</span> <span class="sh">'</span><span class="s">recall</span><span class="sh">'</span><span class="p">:</span> <span class="n">recall</span><span class="p">,</span> <span class="sh">'</span><span class="s">specificity</span><span class="sh">'</span><span class="p">:</span> <span class="n">specificity</span><span class="p">}</span>

<span class="k">def</span> <span class="nf">evaluate_models</span><span class="p">(</span><span class="n">models</span><span class="p">,</span> <span class="n">model_names</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">perturb_func</span><span class="p">,</span> <span class="n">perturb_size</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span> <span class="n">n_iterations</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">random_seed</span><span class="o">=</span><span class="mi">42</span><span class="p">):</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="p">{</span><span class="sh">'</span><span class="s">accuracy</span><span class="sh">'</span><span class="p">:</span> <span class="p">[],</span> <span class="sh">'</span><span class="s">f1</span><span class="sh">'</span><span class="p">:</span> <span class="p">[],</span> <span class="sh">'</span><span class="s">auc</span><span class="sh">'</span><span class="p">:</span> <span class="p">[],</span> <span class="sh">'</span><span class="s">recall</span><span class="sh">'</span><span class="p">:</span> <span class="p">[],</span> <span class="sh">'</span><span class="s">specificity</span><span class="sh">'</span><span class="p">:</span> <span class="p">[]}</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">model_names</span><span class="p">}</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">n_iterations</span><span class="p">):</span>
        <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="n">random_seed</span> <span class="o">+</span> <span class="n">i</span><span class="p">)</span>
        <span class="n">X_test_perturbed</span> <span class="o">=</span> <span class="nf">perturb_func</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">perturb_size</span><span class="o">=</span><span class="n">perturb_size</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">model</span><span class="p">,</span> <span class="n">name</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">models</span><span class="p">,</span> <span class="n">model_names</span><span class="p">):</span>
            <span class="n">metrics</span> <span class="o">=</span> <span class="nf">evaluate_model</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X_test_perturbed</span><span class="p">,</span> <span class="n">y_test</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">metric_name</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">metrics</span><span class="p">.</span><span class="nf">items</span><span class="p">():</span>
                <span class="n">results</span><span class="p">[</span><span class="n">name</span><span class="p">][</span><span class="n">metric_name</span><span class="p">].</span><span class="nf">append</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">results</span>
    
    
<span class="n">results_raw</span> <span class="o">=</span> <span class="nf">evaluate_models</span><span class="p">([</span><span class="n">model</span><span class="p">],</span> <span class="p">[</span><span class="sh">'</span><span class="s">Model</span><span class="sh">'</span><span class="p">],</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">raw_perturbation</span><span class="p">,</span> <span class="n">perturb_size</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span> <span class="n">n_iterations</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">random_seed</span><span class="o">=</span><span class="mi">1234</span><span class="p">)</span>

<span class="k">for</span> <span class="n">metric</span><span class="p">,</span> <span class="n">values</span> <span class="ow">in</span> <span class="n">results_raw</span><span class="p">[</span><span class="sh">'</span><span class="s">Model</span><span class="sh">'</span><span class="p">].</span><span class="nf">items</span><span class="p">():</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">metric</span><span class="si">}</span><span class="s">: mean=</span><span class="si">{</span><span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">values</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">, std=</span><span class="si">{</span><span class="n">np</span><span class="p">.</span><span class="nf">std</span><span class="p">(</span><span class="n">values</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
<span class="n">sns</span><span class="p">.</span><span class="nf">histplot</span><span class="p">(</span><span class="n">results_raw</span><span class="p">[</span><span class="sh">'</span><span class="s">Model</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">accuracy</span><span class="sh">'</span><span class="p">],</span> <span class="n">binwidth</span><span class="o">=</span><span class="mf">0.05</span><span class="p">,</span> <span class="n">kde</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">lightblue</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Accuracy Distribution</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Accuracy</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Count</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<h1 id="4-permutation-test">4. Permutation Test</h1>

<p>Permutation Test는 모델에서 각 특성이 예측에 얼마나 기여하고 있는지를 직관적으로 살펴보는 방법입니다. 일반적으로 트리 기반 모델에서만 feature importance를 체크할 수 있다고 아시는 분들도 있지만, 이 방법을 사용하면 트리기반 모델이 아니더라도 비슷하게 feature importance를 추출해볼 수 있습니다. 한 번에 하나의 열을 무작위로 섞은 다음, 해당 열이 섞이기 전후로 모델의 예측 성능이 얼마나 떨어지는지를 측정합니다. 이렇게 모든 열에 대해 성능 변화를 측정해 보면, 성능 저하 폭이 큰 열일수록 모델이 해당 특성에 크게 의존하고 있음을 추측할 수 있습니다. 반대로 어떤 열을 섞어도 성능 차이가 거의 없다면, 해당 열은 모델이 예측하는 데 그다지 중요한 정보가 아니라고 볼 수 있습니다. 모든 열에 대해 이 작업을 수행해야하기 때문에 일반적으로 리소스가 많이 들기 때문에, 아래의 예제 코드는 병렬처리를 가능하게 한 코드로 작성했습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">compute_feature_importance</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">baseline_score</span><span class="p">,</span> <span class="n">n_repeats</span><span class="p">,</span> <span class="n">random_seed</span><span class="p">):</span>
    <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="n">random_seed</span><span class="p">)</span>
    
    <span class="n">score_drops</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">n_repeats</span><span class="p">):</span>
        <span class="n">X_permuted</span> <span class="o">=</span> <span class="n">X</span><span class="p">.</span><span class="nf">copy</span><span class="p">()</span>
        <span class="n">X_permuted</span><span class="p">[</span><span class="n">col</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">permutation</span><span class="p">(</span><span class="n">X_permuted</span><span class="p">[</span><span class="n">col</span><span class="p">].</span><span class="n">values</span><span class="p">)</span>
        
        <span class="n">y_pred_proba</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">predict_proba</span><span class="p">(</span><span class="n">X_permuted</span><span class="p">)[:,</span> <span class="mi">1</span><span class="p">]</span>
        <span class="n">score</span> <span class="o">=</span> <span class="nf">roc_auc_score</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_pred_proba</span><span class="p">)</span>
        <span class="n">score_drops</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">baseline_score</span> <span class="o">-</span> <span class="n">score</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">col</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">score_drops</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">permutation_test_feature_importance</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">n_repeats</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">random_seed</span><span class="o">=</span><span class="mi">42</span><span class="p">,</span> <span class="n">n_jobs</span><span class="o">=-</span><span class="mi">1</span><span class="p">):</span>
    <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="n">random_seed</span><span class="p">)</span>
    
    <span class="n">y_pred_proba</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">predict_proba</span><span class="p">(</span><span class="n">X</span><span class="p">)[:,</span> <span class="mi">1</span><span class="p">]</span>
    <span class="n">baseline_score</span> <span class="o">=</span> <span class="nf">roc_auc_score</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y_pred_proba</span><span class="p">)</span>
    
    <span class="n">results</span> <span class="o">=</span> <span class="nc">Parallel</span><span class="p">(</span><span class="n">n_jobs</span><span class="o">=</span><span class="n">n_jobs</span><span class="p">)(</span>
        <span class="nf">delayed</span><span class="p">(</span><span class="n">compute_feature_importance</span><span class="p">)(</span>
            <span class="n">model</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">baseline_score</span><span class="p">,</span> <span class="n">n_repeats</span><span class="p">,</span> <span class="n">random_seed</span>
        <span class="p">)</span> 
        <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="n">X</span><span class="p">.</span><span class="n">columns</span>
    <span class="p">)</span>
    
    <span class="n">feature_importances</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">col</span><span class="p">,</span> <span class="n">drop</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
        <span class="n">feature_importances</span><span class="p">[</span><span class="n">col</span><span class="p">]</span> <span class="o">=</span> <span class="n">drop</span>
    
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">baseline_score</span><span class="sh">"</span><span class="p">:</span> <span class="n">baseline_score</span><span class="p">,</span> <span class="sh">"</span><span class="s">feature_importances</span><span class="sh">"</span><span class="p">:</span> <span class="n">feature_importances</span><span class="p">}</span>

<span class="n">perm_test_result</span> <span class="o">=</span> <span class="nf">permutation_test_feature_importance</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">,</span>
    <span class="n">X</span><span class="o">=</span><span class="n">X_test</span><span class="p">,</span>
    <span class="n">y</span><span class="o">=</span><span class="n">y_test</span><span class="p">,</span>
    <span class="n">n_repeats</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
    <span class="n">random_seed</span><span class="o">=</span><span class="n">RAND</span><span class="p">,</span>
    <span class="n">n_jobs</span><span class="o">=-</span><span class="mi">1</span>
<span class="p">)</span>

<span class="n">baseline_score_pt</span> <span class="o">=</span> <span class="n">perm_test_result</span><span class="p">[</span><span class="sh">"</span><span class="s">baseline_score</span><span class="sh">"</span><span class="p">]</span>
<span class="n">feature_importances</span> <span class="o">=</span> <span class="n">perm_test_result</span><span class="p">[</span><span class="sh">"</span><span class="s">feature_importances</span><span class="sh">"</span><span class="p">]</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Baseline AUC: </span><span class="si">{</span><span class="n">baseline_score_pt</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="n">fi_df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="nc">DataFrame</span><span class="p">(</span><span class="nf">list</span><span class="p">(</span><span class="n">feature_importances</span><span class="p">.</span><span class="nf">items</span><span class="p">()),</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">feature</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">importance_drop</span><span class="sh">"</span><span class="p">])</span>
<span class="n">fi_df</span><span class="p">.</span><span class="nf">sort_values</span><span class="p">(</span><span class="sh">"</span><span class="s">importance_drop</span><span class="sh">"</span><span class="p">,</span> <span class="n">ascending</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">inplace</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">top_n</span> <span class="o">=</span> <span class="mi">20</span>
<span class="n">fi_top</span> <span class="o">=</span> <span class="n">fi_df</span><span class="p">.</span><span class="nf">head</span><span class="p">(</span><span class="n">top_n</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
<span class="n">sns</span><span class="p">.</span><span class="nf">barplot</span><span class="p">(</span><span class="n">x</span><span class="o">=</span><span class="sh">"</span><span class="s">importance_drop</span><span class="sh">"</span><span class="p">,</span> <span class="n">y</span><span class="o">=</span><span class="sh">"</span><span class="s">feature</span><span class="sh">"</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">fi_top</span><span class="p">,</span> <span class="n">palette</span><span class="o">=</span><span class="sh">"</span><span class="s">Blues_r</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Permutation Feature Importance (Top </span><span class="si">{</span><span class="n">top_n</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Mean AUC Drop</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Feature</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<p>실제로 사용할 때는 데이터 형태나 모델 구조에 따라 수정이 필요하겠지만, 전반적인 접근 방식을 파악하는 데는 도움이 되길 바랍니다.</p>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><summary type="html"><![CDATA[훈련이 완료된 머신러닝 모델의 성능을 평가할 때 단순히 validation set, test set 성능으로만 판단하기엔 부족한 부분이 있습니다. 이번 글에서는 분류 모델이 데이터의 의미 있는 패턴을 학습했는지 확인하거나, 우연한 상관관계에 의한 결과인건지, 특정 데이터 분포에만 특화되어 있는지 등을 검증하기 위한 방법을 소개합니다. 아래는 예제 코드를 위한 라이브러리 리스트입니다.]]></summary></entry><entry><title type="html">30살의 내가 미래의 나에게</title><link href="https://ycseong07.github.io/2025/0202_30%EC%82%B4%EC%9D%98-%EB%82%B4%EA%B0%80-%EB%AF%B8%EB%9E%98%EC%9D%98-%EB%82%98%EC%97%90%EA%B2%8C/" rel="alternate" type="text/html" title="30살의 내가 미래의 나에게" /><published>2025-02-02T09:00:00+09:00</published><updated>2025-02-02T09:00:00+09:00</updated><id>https://ycseong07.github.io/2025/30%EC%82%B4%EC%9D%98-%EB%82%B4%EA%B0%80-%EB%AF%B8%EB%9E%98%EC%9D%98-%EB%82%98%EC%97%90%EA%B2%8C</id><content type="html" xml:base="https://ycseong07.github.io/2025/0202_30%EC%82%B4%EC%9D%98-%EB%82%B4%EA%B0%80-%EB%AF%B8%EB%9E%98%EC%9D%98-%EB%82%98%EC%97%90%EA%B2%8C/"><![CDATA[<p>*</p>

<ol>
  <li>매일 나의 상태를 평온하게 유지하는 것은 불가능하다. 다만 평온한 상태를 지향하면서 균형을 잡는 것이 중요하다. 공부에 치우쳤을 때는 의식적으로 운동을 하고, 생각이 너무 많다면 그것들을 하나씩 행동으로 옮겨보고, 성과에 치우쳤을 때는 결과의 질과 과정을 조금 더 의식하자. 전체적인 방향만 틀리지 않으면 흔들리는 것은 아무 문제가 되지 않는다. 한 치의 흔들림도 없이 외줄을 타는 어름사니는 없다.</li>
  <li>평온은 나를 제대로 아는 것으로부터 시작된다. 현재의 나를 인정하고 받아들이고, 내가 잘 못하는 것과 잘하는 것을 구분해서 잘 알아야 한다. 잘하는 것은 내세우고, 못하는 것은 보완하면 된다. 더불어, 내 세상을 의식적으로 넓히자. 내가 모르는 것이 무엇인지 아는 것도 중요하다.</li>
  <li>결과가 나를 정의한다. 내가 어떤 생각을 하든, 행동을 하든 그것을 밖으로 표출하고, 세상과 사람들에게 내가 어떻게 받아들여지는지가 나의 모습니다. 스스로 “나는 ~한 사람이야”라고 주장하는 것은 대부분 별로 의미가 없다.</li>
  <li>삶에서 효율과 가성비를 따지지 말아야 하는 경우를 구분해야 한다. 일례로, 내게 익숙하지 않았던 것들을 받아들여야 할 때는 당연히 효율이 떨어진다. 오랜 기간 쉬었다가 다시 제 궤도로 돌아와야 할 때나 삶의 균형을 바로잡아야 할 때는 느긋하게 시간을 들이고 내가 변화하는 과정을 느끼는 것이 중요하다. ‘비효율’이라는 딱지를 붙여버리고, 행동하지 않으면 나아감은 없다.</li>
  <li>불행한 일이 다가오면 일일이 반응하지 말고, 한 발짝 떨어져서 관망하면서 이성적으로 바라보자. 다른 사람이 이런 일을 당했을 때 어떤 조언을 할지 생각해 보고, 그대로 내가 수용하면 된다. 그 상황에 대한 부정적인 감정은 아무 도움이 되지 않는다. 오히려 기회라는 생각, 얼마나 더 잘되려고 이럴까 하는 생각이 훨씬 도움이 된다.</li>
  <li>불행한 일 뿐만 아니라, 당면한 모든 일들은 가끔씩 멀찍이서 부감해보는 것이 도움이 된다. 사건을 멀리서 바라보면 방향이 보이고, 다른 사건들과의 관계가 보이고, 사실 그 사건이 그다지 무겁지 않다는 것도 알게 된다. 정체되지 않으려면 의식적으로 사건을 Zoom In/Out 할 수 있는 능력이 필요하다.</li>
  <li>지금의 삶이 만족스럽지 않다면 무언가 바꿔야 한다는 신호다. 만나는 사람, 시간을 쓰는 패턴, 주변 환경 중 무언가를 바꿔보자. 의지로 무언가를 해내는 것은 그 다음이다.</li>
  <li>내가 하는 일을 쓸모없다고 생각하지 말자. 내 삶에 책임을 지고 기대를 하자. 내가 충분히 고민하고 시작한 일이므로 누군가에게는, 최소한 내게는 의미 있는 일이다. 이렇게 생각하다 보면 자연스럽게 내 가치를 높게 평가하게 된다.</li>
  <li>‘중요한 건 꺾이지 않는 마음’이라는 문구에 마음이 동한다. 여기에는 ‘성공’이나 ‘결과’에 대한 표현도 없고, 과정이 아름답게 묘사되지도 않는다. 어떤 사건을 직선으로 표현한다면, 결과는 직선의 꼭짓점이고, 나머지는 전부 과정에 해당할 것이다. 과정이 전부라고 해도 무방하다. 여느 SNS에 묘사된 아름다운 과정과 결과는 모든 일이 끝난 후 각색된 것들이다. 과정은 진흙탕이고, 그걸 견뎌내는 것은 당연히 고통을 수반하며, 결과는 항상 찬란하지만은 않다.</li>
  <li>도저히 중심이 잡히지 않고, 아무것도 하기 싫은 날은 누구에게나 온다. 무기력한 하루를 보내고 나서 죄책감에 시달리는 것은 시간 낭비다. 딱 한 가지만 하고, 푹 쉬자.</li>
  <li>열심히 살기 위해서는 그 만큼보다 조금 덜 열심히 살면 된다. 1만큼 열심히 살면 2만큼 열심히 살고 싶어지고, 2만큼 열심히 살면 3만큼 열심히 살고 싶어진다.</li>
  <li>시작을 올바르게 하는 것은 중요하다. 첫 단추를 잘못 꿰면 그것을 끝까지 지속한다 하더라도 결과가 틀어져 있기 마련이다. 다만, 절대로 미루지 말고 시작하자.</li>
  <li>시작은 잘하는데 지속을 못 한다면, 혹은 너무 오래 걸린다면 스텝이 너무 크다는 뜻이다. 내가 뱁새라는 걸 알았다면, 가랑이 찢어지지 말고 뱁새의 걸음 만큼씩만 나아가자.</li>
  <li>고민할 시간에 행동하자. 4시간 더 고민하면 더 나아질 것인가? 그렇다면 고민하고, 아니라면 행동하자. 완벽주의는 의도적으로 버리려고 노력하자. 혼자하는 일 뿐만 아니라, 함께하는 일도 마찬가지다. 함께 고민해도 답이 안나온다면 그 자리를 박차고 일어나야 한다. 외부에서 답을 찾든, 일단 시작하든 하자.</li>
  <li>작은 사이클을 여러 번 돌리자. 좋은 평가를 받기 위해 필요한 것은 다수의 결과다.</li>
  <li>일을 진행할 때, 일을 분석하는 것 뿐만 아니라, 함께 일하는 사람이 어떤 사람인지에 대해서도 함께 생각하자. 문제가 기술이 아닌 사람에서 해결되는 경우도 있다.</li>
  <li>시작하고, 행동하고 있다면, 행동을 꾸준히 할 수 있는 환경과 시스템을 만들자. 하기 싫은 느낌은 의도적으로 잊어버리고, 생각과 고민없이 그냥 한다는 마음가짐이 좋다. 그것을 즐겁게 할 수 있는 방법이 있다면 더 좋다. 결국 끝까지 해야 결과가 나오고, 그것이 내 것이 된다.</li>
  <li>어차피 해야 할 일이라면 빨리 끝내자. 하루 미룰수록 제곱으로 괴로워진다. 막상 시작하면 얼마 안걸릴 수도 있다. 행동으로 옮기기 어렵다면, 난이도, 실력을 고려해 내가 몰입할 수 있는 환경을 의도적으로 만들자.</li>
  <li>‘그만큼 돈을 주면 나도 저렇게 하지’라는 생각이 든다면, 인생에 후불은 없다는 것을 상기하자. 그 사람은 그만큼의 돈을 받을 정도로 먼저 실천해 온 사람이다. 돈이 정의롭게 분배되지 않는다는 것도 함께 상기하자.</li>
  <li>‘과거에 A가 아닌 B를 했더라면..’이라는 생각이 든다면, 인생에 A/B 테스트는 없다는 것을 상기하자. 당시의 나도 그렇게 멍청하지는 않았고, 충분히 현명했을 것이다. 어느 정도 근거를 가지고 A를 시작했을 것이고, 시간이 조금 더 지나면 A를 선택하길 잘했다고 생각하게 되는 순간이 올 수도 있다. 정 후회가 된다면, A가 옳은 선택이 되도록 최선을 다해 미래를 비틀어보자.</li>
  <li>수신제가치국평천하(修身齊家治國平天下) 중 수신부터 제대로 하자. 환경 파괴, 성별 갈등, 공동체 붕괴 등과 같은 거대 담론에 관한 얘기들은 흥미롭지만, 나는 그것들에 대한 구체적인 지식을 모두 공부하지도 않았으며, SNS를 통해 듣게 된 말들을 사실인 것처럼 이해하고 있는 경우가 다분하다. 또한 이런 류의 정보는 사람을 한 명 거칠 때마다 조금씩 왜곡된다. 이런 것들을 걱정하는 데에 시간을 들이는 행위는 현재 내가 직면한 어떤 문제도 해결해 주지 못한다. 만약 지구의 환경이 걱정된다면, 내가 가진 능력을 발전시켜 환경에 어떤 식으로 기여할 수 있을지 고민하고, 행동하자.</li>
  <li>비슷한 맥락에서, 남들에게 진심으로 마음을 충분히 쓰려면 내 마음부터 다스려야 한다. 마음은 소모된다. 밑 빠진 독에서 물을 퍼 올리면 두 배로 빨리 마른다. 미래의 나 역시 타인이다. 나와 가장 가까운 타인부터 챙기자. 먼 타인에게 도움을 주면서 오는 만족감은 나를 충분히 회복시켜 주지 못한다.</li>
  <li>항상 배우면서 살자. 앞으로는 새로운 것이 더 빠르게 등장할 것이다. 꼭 필요하다면, 스스로에게 투자하는 것도 아끼지 말자.</li>
  <li>행운은 하늘이 점지해 주지만, 기회는 다른 사람들에게 마음을 쓰는 만큼 다가온다. 나를 충분히 챙겼다면, 다른 사람에게도 마음을 쓰자.</li>
  <li>나는 뇌 가소성 범위 안에서만 바뀔 수 있다. 청소년기를 한참 지난 내가 변화하기에는 어느 정도 한계가 있음을 인정하고, 내 그릇에 맞는 것을 담자. 시간을 쓰는 방법, 주변 환경, 네트워킹하는 사람들을 바꿔보고, 그 안에서 내가 바뀔 수 있는 범위를 찾으면 된다. 만약, 여전히 너무너무 불만족스럽고, 전혀 다른 나로 바뀌고자 한다면 그릇을 깨고 다시 빚어내는 과정을 감내해야 할 것이다.</li>
  <li>현재의 나를 정의하지 말고, 변화하는 연속성 안에서 나를 정의하자. 지금의 나는 1초 전의 나와 다른 사람이다. 과거에 정의한 내 모습은 지금과 다르다. 내가 어디로 나아가고 있는지, 어떤 가치를 추구하는지 보여주는 표현으로 나를 정의하자. 표현이 바뀌면 인식도 바뀐다.</li>
  <li>내가 실제로 무엇을 할 수 있는지가 중요하다. 권력은 능력에서 비롯되고, 카리스마는 할 수 있는 것을 하지 않는 것에서 비롯된다.</li>
  <li>A vs B의 이분법은 세상을 심플하게 바라볼 수 있게 하지만, 둘 중 하나만이 답인 경우는 거의(절대에 가깝게) 없다. 상황과 시간에 따라 A가 답일 수도, B가 답일 수도 있으며, A, B를 조화시키는 것이 더 나은 답일 수도, 제 3의 선택지인 C가 답일 수도 있다.</li>
  <li>내 삶에 대해 고찰할 때, ‘성공 vs 실패’의 이분법은 쓸모가 없다. ‘성공 vs 성공을 위한 과정’이라는 프레임이 훨씬 낫다. 그러니 방향을 정하고 나아가는 것을 두려워하지 말자.</li>
  <li>성공을 수치화할 수 있고, 성공에 필요한 요소가 A, B, C라면, 성공의 수치는 A + B + C가 아니라 A x B x C 에 가까울 것이다. A를 포기하고 B, C에 더 투자해 성공하고자 하는 전략은 비효율적이라는 뜻, 내세우지 않을 부분이라도 1 이상 만큼은 해야 한다는 뜻이다.</li>
  <li>감사하며 살자. 나는 알게모르게 많은 사람들의 지지 위에서 살고 있고, 나 혼자 이룬 것은 아무 것도 없다. 그러니 오만하지 말자. 나를 둘러싼 모든 사람들과 환경이 꽤 괜찮음에 항상 의도적으로 감사하자.</li>
  <li>그럭저럭 알고 지내는 지인에게 남의 험담은 절대 하지 말자. 그렇다고 현실적으로 아예 안 할 수도 없다. 만약 나를 믿어주는, 아주 친한 사람에게 푸념 삼아 다른 사람의 험담을 하게 된다면 1) 이 사람은 앞으로 나를 인식할 때 이 순간을 떠올릴 것이라는 것을 기억하고, 2) 대화의 마지막에 “그래도 그 사람의 이런 점은 장점이긴 해..”를 살포시 덧붙이자.</li>
  <li>과거에 좋았던 사람이 지금도 여전히 좋을 것이라고 기대하지 말자. 반대로, 과거에 안 좋았던 사람이 지금까지도 안 좋을 거라고 생각하지도 말자. 나 또한 마찬가지다. 내 과거 중 안 좋았던 것들을 덮어 숨기려 하지 말고, 앞으로 어떻게 좋은 사람이 될 것인가에 집중하자.</li>
  <li>짜증나거나 이해가 되지 않을 때 써먹을 수 있는 만능 주문, “그럴 수 있지”. 해결할 수 없는 일이라면 넘겨버리는 것이 가장 좋다.</li>
  <li>따로 요청받지 않았다면, 타인이 다짐한 일에 관여하지 말자. 영향력을 미치고 싶은 것은 당연한 욕구일 수 있지만, 그 전에 상대를 존중해야 한다. “그거 별로인 것 같은데?”는 최악이고, “A보다 B는 어때?” 정도의 관여도 10번은 고민하자. 상대가 그러한 다짐을 하게 된 계기와 이유가 있을 것이고, 다짐을 하게 되기까지 고민한 시간이 있을 것이다. 그것을 단 몇 초 만에 부정하는 것이 얼마나 폭력적인지 생각해 보자. 남들이 하겠다는 일에 초치지 말자는 뜻이다. 정말 영향을 미치고 싶다면, 내가 생각하는 바 대로 직접 행동하고, 이 방법이 더 좋다는 것을 결과로 보여주자. 그게 정말 좋은 방법이라면, 그 사람이 내 방법을 따라 할 것이다.</li>
  <li>현재의 만족스럽지 않은 상황이 주변 사람들 때문인 것 같다면, 좋은 네트워크에 소속되기 위한 내 노력이 부족했던 것은 아닌지 생각해 보자. 과거에는 한 사람이 접근하고 소속될 수 있는 사회연결망이 제한적이었고, 개인이 만족할 수 있는 일보다 주변이 인정하는 일을 해야 했고, 그 네트워크에 적응하며 부정적인 낙인이 찍히지 않는 것이 중요했을지 모른다. 그러나 지금은 내가 속할 수 있는 네트워크가 훨씬 다양해졌고, 내가 추구하는 가치를 공유하는 네트워크를 선택할 수 있는 환경이 마련되어있다. 속하길 원하는 네트워크가 있다면, 자격을 갖추고 직접 문을 두드려보자.</li>
  <li>내 생각과 의도를 가지고 선택하자. 다른 사람들이 하는 보편적인 선택이기 때문에 그렇게 하는 것이 아니라, 그것을 참고는 하되, 내 생각과 의도를 가지고 선택하면 된다. 그것이 남들이 하지 않는 것이라면 오히려 좋다. 그 결정들이 쌓여서 나를 희소하게 만들어 줄 것이다.</li>
</ol>

<ul>
  <li><strong>References</strong>
    <ul>
      <li>당신은 결국 무엇이든 해내는 사람,  김상현</li>
      <li>Beyond Order, Peterson, Jordan B.</li>
      <li><a href="https://www.instagram.com/p/C5Qom1fvB3Z/?igsh=cmhlMXlrMmh2NGln">https://www.instagram.com/p/C5Qom1fvB3Z/?igsh=cmhlMXlrMmh2NGln</a> (변성윤님 인스타그램)</li>
    </ul>
  </li>
</ul>]]></content><author><name>moondb</name><email>ycseong07@gmail.com</email></author><category term="blog" /><summary type="html"><![CDATA[*]]></summary></entry></feed>