Profile img

자손킴

@jasonkim@hackers.pub · 12 following · 31 followers

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #5-4 SSL 오프로드

자손킴 @jasonkim@hackers.pub

다음에 6장이 있지만 총정리를 하는 챕터이기 때문에 실질적인 내용은 여기까지가 끝이다. 책에서는 SSL 오프로드만 다루고 끝나지만 내부망 안에서의 보안도 짧게 정리했다.

TLS(SSL) 오프로드

TLS를 사용하면 보안이 강화되지만 TLS 핸드셰이크와 암복호화 작업에 CPU 자원을 많이 소모하게 된다. 이것을 전용 장비에 맡기면 웹서버는 효율적으로 애플리케이션 로직 처리에만 신경 쓰면 된다. 특히 로드밸런서에서 중앙집중식으로 TLS를 처리하면 웹서버의 부하가 크게 줄어들고 요청을 빠르게 처리할 수 있게 되어 시스템 전반적으로 부하 분산의 효과가 커진다. 특히 관리가 까다로운 공인 인증서를 로드밸런서에서 집중 관리할 수 있어 운영 부담이 크게 줄어든다.

TLS 오프로드 동작방식

  1. 클라이언트의 HTTPS 요청: 클라이언트가 웹 서비스에 접속하기 위해 HTTPS 요청을 보낸다. 이 시점에서 클라이언트는 TLS 핸드셰이크를 시작한다.

  2. 방화벽(fw1) 통과: 요청이 방화벽 fw1에 도달한다. 방화벽은 허용된 포트(443)로 들어오는 트래픽인지 확인하고, 패킷 필터링 규칙에 따라 트래픽을 통과시킨다. 이 단계에서 트래픽은 여전히 암호화된 상태이므로 일반적인 방화벽은 패킷의 내용을 검사할 수 없고 IP/포트 기반 필터링만 수행한다. (다만 SSL 인스펙션 기능이 있는 차세대 방화벽(NGFW)은 여기서 복호화 후 검사를 수행하기도 한다.)

  3. 로드밸런서(lb1)에서 TLS 종료: 로드밸런서 lb1이 클라이언트와 TLS 핸드셰이크를 완료한다. 서버 인증서를 클라이언트에게 제시하고 세션 키를 협상한 뒤 암호화된 트래픽을 복호화한다. 이 과정을 SSL/TLS Termination이라고 부른다.

  4. 평문 HTTP로 백엔드 전달: 로드밸런서는 복호화된 요청을 분석하여 라우팅 규칙에 따라 백엔드 서버 sv1 또는 sv2에 평문 HTTP로 전달한다. 이때 로드밸런서는 X-Forwarded-For, X-Forwarded-Proto 같은 헤더를 추가하여 원본 클라이언트 정보와 프로토콜 정보를 백엔드에 전달할 수 있다.

  5. 백엔드 서버 처리: 웹서버 sv1 또는 sv2는 평문 HTTP 요청을 받아 애플리케이션 로직을 처리하고 응답을 생성한다. 암복호화 작업이 없으므로 CPU 자원을 온전히 비즈니스 로직에 사용할 수 있다.

  6. 응답 암호화 및 전송: 백엔드 서버의 HTTP 응답이 로드밸런서 lb1로 돌아오면, 로드밸런서는 이를 TLS로 암호화하여 클라이언트에게 전송한다.

망분리는 만병통치약이 아니다.

TLS 오프로드를 하게 되면 로드밸런서를 통과한 패킷은 평문으로 내부망을 돌아다니게 된다. 책을 읽으며 처음에는 "어차피 방화벽 뒤에 있는 내부망이고 외부에서 접근이 차단되어 있으니 평문이어도 괜찮은 거겠군"이라고 생각을 했었다.

그러다 문득 망분리는 만병통치약이 아니라는 트윗이 기억이 났다.

https://x.com/simnalamburt/status/1823610803846517196?s=20

내부망이 안전하다는 가정은 내부자 위협, 자격 증명 탈취를 통한 침해, 그리고 한 시스템이 뚫린 후 내부망을 통해 다른 시스템으로 확산되는 횡적 이동 공격과 같은 위험이 도사리고 있다. 이른바 '침해 가정' 원칙에 따라, 공격자가 이미 망 내부에 들어와 있다는 전제로 보안 체계를 설계해야 한다.

Zero Trust 아키텍처

"결코 신뢰하지 말고 항상 검증하라(Never trust, always verify)"는 원칙에 따라 내부망과 외부망을 구분하지 않고 모든 접근에 대해 인증과 권한 검증을 수행한다. 마이크로 세그멘테이션을 통해 네트워크를 세분화하고 최소 권한 원칙을 적용한다.

mTLS

mTLS는 클라이언트와 서버가 서로의 인증서를 검증하여 양방향으로 신원을 확인하는 상호 인증 방식이다. 일반 TLS는 서버만 인증서를 제시하지만 mTLS에서는 클라이언트도 인증서를 제시해야 한다. Zero Trust 아키텍처에서 mTLS는 핵심 구성요소로 내부 서비스 간 통신에서도 모든 요청의 신원을 검증하여 "항상 검증하라"는 원칙을 기술적으로 구현한다.

그럼 굳이 TLS 오프로드 하지 말고 백엔드까지 암호화된 패킷을 전달하면 되는거 아닌가?

TLS 오프로드는 여전히 유효한 선택이다. L7 로드밸런서가 HTTP 헤더나 URL 경로, 쿠키를 분석해서 트래픽을 라우팅하려면 패킷 내용을 들여다볼 수 있어야 한다. WAF도 SQL 인젝션이나 XSS 같은 공격 패턴을 탐지하려면 평문 상태의 요청을 검사해야 한다. 암호화된 상태로는 이런 기능들이 불가능하다.

결국 L7 기능을 활용하려면 어디선가는 TLS를 종료해야 한다. 이때 공인 인증서 처리를 로드밸런서로 집중시키면 보안 정책 적용과 인증서 갱신이 훨씬 수월해진다.

만약 내부망 보안을 위해 재암호화가 필요한 TLS 브릿징(TLS Bridging) 방식을 사용하더라도 오프로드의 이점은 여전하다. 외부 노출용 공인 인증서는 로드밸런서가 전담하고, 내부 구간은 사설 CA나 자동화된 인증서 발급 체계(mTLS 등)를 이용해 백엔드 서버의 관리 부담을 최소화하면서도 보안과 가시성을 모두 챙길 수 있기 때문이다.

Read more →
1

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #5-3 DHCP

자손킴 @jasonkim@hackers.pub

DHCP(Dynamic Host Configuration Protocol)

DHCP는 IP주소, 서브넷 마스크, 기본 게이트웨이와 DNS 서버의 IP 주소 등 네트워크에 접속하기 위해 필요한 설정을 배포하는 프로토콜이다. RFC2131 에서 표준화 되어 있으며, DHCP의 역할, 메시지 형식, 형식을 구성하는 필드의 의미와 처리 흐름등이 상세하게 정의 되어 있다.

책에서는 DHCP를 L7에서 다루고 있는데, 정리하다보니 이게 L7에 있는게 맞나 싶은 생각이 든다.

IP 할당 방식

IP 주소를 단말(NIC)에 할당하는 방법에는 크게 '정적 할당'과 '동적 할당' 두 가지가 있다.

정적 할당

단말에 대해 일일이 수동으로 IP 주소를 설정하는 방식이다. 시스템 관리자가 비어있는 IP 주소를 사용자에게 할당해 준다.

정적 할당은 단말과 IP 주소가 고유하게 매핑되기 때문에 IP 주소 관리가 용이하다. 특정 IP의 이상 징후가 발생하면 어떤 단말이 문제인지 즉시 파악 할 수 있다. 그러나 단말의 수가 많아 지거나 교체가 빈번하면 하나씩 관리하기 어렵다는 문제가 있다.

동적 할당

DHCP를 사용하여 단말에 자동으로 IP 주소를 설정하는 방법이다. 정적 할당은 사용자가 시스템 관리자에게 요청하여 빈 IP 주소를 지급받아 수동 설정 했다면, 동적 할당은 이 모든 과정을 DHCP가 자동으로 처리한다.

DHCP 메시지 형식

DHCP는 UDP/67로 캡슐화된 DHCP 메시지 부분에 설정 정보를 담는다. DHCP 메시지는 여러 가지 필드로 구성되는데, 다음 3가지가 특히 중요하다.

  • 할당 클라이언트 IP 주소
    • DHCP 서버에서 단말에 배포하는 IP 주소
  • 클라이언트 MAC 주소
    • 단말의 MAC 주소
  • 옵션
    • 네트워크 설정에 관한 다양한 정보

옵션은 옵션 코드에 의해 식별되는데, 대표적인 코드는 다음과 같다.

  • (1) 서브넷 마스크
  • (3) 기본 게이트웨이
  • (6) DNS 서버 IP 주소
  • (12) 호스트 이름
  • (42) NTP 서버의 IP 주소
  • (51) IP 주소 임대 시간
  • (53) DHCP 메시지 유형
  • (54) DHCP 서버 ID

DHCP 처리 흐름

DHCP는 서버와 클라이언트로 구성되어 있다. DHCP 클라이언트가 있는 단말은 초기에는 IP가 할당되지 않은 상태이기 때문에 브로트캐스트를 통해 정보를 주고 받는다.

  1. DHCP Discover: 클라이언트가 네트워크에 접속하면 DHCP 서버를 찾기 위해 브로드캐스트로 Discover 메시지를 전송한다. 이 시점에서 클라이언트는 아직 IP 주소가 없으므로 출발지 IP는 0.0.0.0으로 설정된다.
  2. DHCP Offer: DHCP 서버가 Discover 메시지를 수신하면 할당 가능한 IP 주소와 서브넷 마스크, 임대 시간 등의 설정 정보를 담아 Offer 메시지를 유니캐스트로 응답한다. 네트워크에 여러 DHCP 서버가 존재하는 경우 클라이언트는 복수의 Offer를 수신할 수 있다.
  3. DHCP Request: 클라이언트가 수신한 Offer 중 하나를 선택하여 해당 DHCP 서버에 IP 주소 할당을 정식으로 요청한다. 이 메시지도 브로드캐스트로 전송되며, 선택되지 않은 다른 DHCP 서버들에게 해당 Offer가 거절되었음을 알리는 역할도 한다.
  4. DHCP ACK: DHCP 서버가 Request를 승인하면 ACK 메시지를 전송하여 IP 주소 할당을 확정한다. 클라이언트는 이 메시지를 수신한 후 비로소 할당받은 IP 주소를 사용할 수 있게 된다. 만약 요청한 IP 주소를 할당할 수 없는 경우에는 DHCP NAK 메시지가 전송된다.
  5. DHCP Release: 클라이언트가 더 이상 IP 주소를 사용하지 않을 때 서버에 반환을 알리는 메시지이다. 이 메시지를 수신한 서버는 해당 IP 주소를 풀(Pool)에 반환하여 다른 클라이언트가 사용할 수 있도록 한다.

IP 주소 임대와 갱신

DHCP로 할당받은 IP 주소는 영구적인 것이 아니라 임대 시간(Lease Time)이 정해져 있다. 클라이언트는 임대 시간이 만료되기 전에 갱신을 요청해야 하는데, 일반적으로 임대 시간의 절반(50%)이 경과하면 DHCP Request 메시지를 서버에 유니캐스트로 전송하여 갱신을 시도한다. 이 갱신이 실패하면 임대 시간의 7/8(87.5%)가 경과한 시점에 다시 브로드캐스트로 갱신을 시도하며, 그래도 실패하면 임대 만료 시 IP 주소 사용을 중단하고 처음부터 Discover 과정을 다시 수행한다.

네트워크 인프라 자동화의 기반이 되는 DHCP

DHCP는 IP 주소 할당 외에도 옵션 필드를 활용하여 다양한 부가 기능을 제공할 수 있다. 그 중 대표적인 것이 네트워크 부팅(PXE) 지원이다.

  • (66) TFTP 서버 이름
    • 부트 파일을 제공하는 TFTP 서버의 IP 주소 또는 호스트명
  • (67) 부트 파일 이름
    • 클라이언트가 다운로드해야 할 네트워크 부트 프로그램의 경로

PXE 부팅

PXE(Preboot Execution Environment)는 로컬 저장 장치 없이 네트워크를 통해 운영체제를 부팅하는 기술이다. PXE를 사용하면 서버의 NIC가 네트워크에서 부팅 이미지를 받아와 자동으로 OS 설치를 진행할 수 있다.

PXE 부팅의 동작 흐름은 다음과 같다.

  1. 클라이언트가 전원을 켜면 NIC의 PXE 펌웨어가 DHCP Discover 메시지를 브로드캐스트한다.
  2. DHCP 서버는 IP 주소와 함께 옵션 66(TFTP 서버 주소)과 옵션 67(부트 파일 경로)을 응답한다.
  3. 클라이언트는 TFTP 프로토콜을 사용하여 지정된 서버에서 부트 파일(예: pxelinux.0, bootx64.efi)을 다운로드한다.
  4. 다운로드한 부트 로더가 실행되어 OS 설치 또는 부팅이 진행된다.

이처럼 DHCP의 옵션 필드를 활용하면 단순한 IP 할당을 넘어 네트워크 인프라 자동화의 기반을 구축할 수 있다.

Read more →
1

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #5-2 DNS

자손킴 @jasonkim@hackers.pub

이번 섹션은 DNS에 대해서 알아본다. 책의 내용을 기반으로 패킷 분석은 생략하고 DNS의 발전과정, 존파일의 구체적인 예시와 보안 관련 내용을 짧게 추가하여 정리했다.

1장에서는 L2의 MAC 주소에 대해서 이야기했고, 2장에서는 L3의 IP주소에 대해서 이야기했다. IP 주소를 MAC 주소로 변환하기 위해서 ARP(Address Resolution Protocol)를 사용한다.

인터넷에서 다른 단말기에 접속하기 위해서는 IP 주소를 알아야 한다. 그러나 IP 주소는 숫자의 나열이기 때문에 서로 다른 단말기의 IP 주소를 외우고 있는 것은 힘들 뿐더러 IP 주소가 바뀔 경우 다시 외워야 한다는 번거로움이 크다.

DNS는 이러한 불편함을 해결하기 위해 만들어졌다.

DNS(Domain Name System)

인터넷의 전신인 아파넷(ARPANET)에서는 모든 컴퓨터의 이름과 지금의 IP 주소격인 Host Number를 HOSTS.TXT라는 파일에 기록하여 관리했다. 이 파일은 스탠포드 연구소(SRI)의 네트워크 정보 센터(NIC)에서 관리했고, 네트워크 관리자들이 주기적으로 FTP를 통해 이 파일을 다운로드하여 자신의 시스템에 복사했다.

1980년대 초에 네트워크가 급성장하면서 중앙집중식으로 관리되는 HOSTS.TXT는 확장성의 한계에 직면하게 되고 Paul Mockapetris가 RFC 882와 RFC 883을 통해 DNS를 제안한다. 이후 개정을 통해 RFC1034, RFC1035로 표준화 되었다.

DNS는 사람이 읽을 수 있는 도메인 이름을 컴퓨터가 통신에 사용하는 IP 주소로 변환해주는 분산 데이터베이스 시스템이다. 흔히 인터넷의 전화번호부에 비유되며 전 세계에 분산된 수많은 DNS 서버들이 협력하여 도메인 이름에 대한 질의에 응답한다.

도메인 이름

도메인은 계층 구조로 이루어져 있다. 가장 상위에는 루트 도메인이 있고 그 아래로 최상위 도메인(TLD, Top-Level Domain), 2레벨 도메인(2LD, 2nd Level Domain), 3레벨 도메인 순으로 내려간다. 예를 들어 www.example.com이라는 도메인이 있을 때 루트 도메인은 맨 끝의 점(.)으로 표현되며 보통 생략된다.

도메인 이름에서 점(.)으로 구분되는 각각의 부분을 라벨(label)이라고 부른다. www.example.com에서 www, example, com이 각각 하나의 라벨이다. 루트 도메인까지 포함하여 완전하게 표기된 도메인 이름을 FQDN(Fully Qualified Domain Name)이라고 한다.

도메인 이름은 일반적으로 호스트 부분과 도메인 부분으로 나눌 수 있다. www.example.com에서 www는 호스트 부분으로 특정 서버나 서비스를 가리키고, example.com은 도메인 부분으로 해당 조직이나 서비스의 영역을 나타낸다.

최상위 도메인(TLD)은 크게 일반 최상위 도메인(gTLD, Generic Top-Level Domain)과 국가 코드 최상위 도메인(ccTLD, Country Code Top-Level Domain)으로 구분된다. 일반 최상위 도메인은 특정 국가에 속하지 않고 용도나 분야에 따라 구분되는 도메인이다. 국가 코드 최상위 도메인은 ISO 3166-1 alpha-2 표준에 기반한 두 글자 코드로 각 국가나 지역을 나타내며 각 국가의 도메인 등록 기관이 해당 ccTLD의 정책을 독자적으로 관리한다.

DNS의 도메인 체계는 트리 형태의 계층 구조를 이룬다. 맨 꼭대기에는 루트 도메인이 위치하며 이 루트로부터 각 TLD가 가지처럼 뻗어나가고 각 TLD 아래에는 다시 수많은 2단계 도메인이, 그 아래에는 3단계 도메인이 연결되는 식이다. 이 구조에서 각 노드는 자신의 하위 도메인에 대한 정보를 관리하는 권한을 갖는다.

이름 풀이

IP 주소와 도메인 이름을 서로 변환하는 과정이다. 이름 풀이에는 hosts파일이나 DNS를 사용하며 /etc/nsswitch.conf 등에 의해 우선순위가 정해진다.

hosts 파일을 이용한 이름 풀이

hosts 파일은 IP 주소와 호스트 이름의 매핑을 한 줄에 하나씩 기록하는 단순한 텍스트 파일로, 127.0.0.1 localhost처럼 IP 주소 뒤에 공백을 두고 호스트 이름을 적는 형식이다. hosts 파일은 네트워크 연결 없이도 동작하고 설정에 따라서는 DNS보다 우선 적용된다.

운영체제는 이름 풀이 요청이 들어오면 먼저 이 파일을 확인하고 일치하는 항목이 있으면 DNS 질의 없이 바로 해당 IP 주소를 반환한다.

DNS를 이용한 이름 풀이

DNS를 이용한 이름 풀이는 여러 구성요소의 협력으로 이루어진다. 먼저 DNS 클라이언트(일명: 스터브 리졸버(Stub Resolver))는 사용자 컴퓨터의 운영체제에 내장된 DNS 클라이언트로 애플리케이션의 이름 풀이 요청을 받아 DNS 서버에 질의를 보내는 역할을 한다. 스터브 리졸버는 단순히 질의를 전달하고 응답을 받아오는 역할만 한다.

재귀 리졸버(Recursive Resolver) 또는 캐싱 네임서버는 DNS 클라이언트의 재귀 쿼리를 받아 인터넷에 있는 권한이 있는 네임서버에 이름 풀이 요청(반복 쿼리)를 보낸다.

권한이 있는 네임서버(Authoritative Name Server)는 특정 도메인에 대한 공식적인 DNS 레코드를 보유한 서버로 해당 도메인에 대한 질의에 최종적인 답변을 제공한다. 권한이 있는 네임서버는 존 파일(Zone File)이라는 텍스트 파일에 도메인 정보를 저장하며, 이 존 파일에는 리소스 레코드(Resource Record)라고 불리는 개별 항목들이 포함된다. A 레코드는 도메인 이름을 IPv4 주소로 매핑하고 AAAA 레코드는 IPv6 주소로 매핑한다. CNAME 레코드는 별칭을 정의하며 MX 레코드는 메일 서버, NS 레코드는 해당 도메인의 네임서버를 지정한다.

루트 네임서버는 DNS 계층의 최상위에서 TLD 네임서버의 위치를 알려주고 TLD 네임서버는 각 최상위 도메인에 등록된 도메인들의 권한이 있는 네임서버 위치를 안내한다.

DNS의 동작 방식

사용자가 웹 브라우저에 www.example.com을 입력하면 DNS를 통한 이름 풀이 과정이 시작된다. 먼저 운영체제의 스터브 리졸버가 설정된 재귀 리졸버에게 해당 도메인의 IP 주소를 요청한다. 재귀 리졸버는 자신의 캐시를 확인하여 이전에 조회한 적이 있는 도메인이라면 캐시된 결과를 즉시 반환한다.

캐시에 없는 경우 재귀 리졸버는 루트 네임서버에 질의를 보낸다. 루트 네임서버는 www.example.com의 IP 주소를 직접 알지 못하지만 com TLD를 담당하는 네임서버의 주소를 알려준다. 재귀 리졸버는 이어서 com TLD 네임서버에 질의하고 TLD 네임서버는 example.com 도메인의 권한이 있는 네임서버 주소를 응답한다. 마지막으로 재귀 리졸버가 해당 권한이 있는 네임서버에 질의하면 이 서버가 www.example.com의 실제 IP 주소를 담은 최종 답변을 반환한다.

재귀 리졸버는 이 결과를 캐시에 저장한 뒤 클라이언트에게 전달하여 전체 이름 풀이 과정이 완료된다. 캐시에 저장된 레코드는 TTL(Time To Live)이라는 유효 시간 동안 유지되며 TTL이 만료되면 다시 질의를 수행해야 한다.

재귀 쿼리와 반복 쿼리

DNS 질의는 재귀 쿼리(Recursive Query)와 반복 쿼리(Iterative Query)로 구분되며 이름 풀이 과정의 각 단계에서 서로 다른 방식이 사용된다.

재귀 쿼리는 클라이언트가 리졸버에게 최종 답변을 요구하는 질의 방식이다. 스터브 리졸버가 재귀 리졸버에게 질의를 보낼 때 이 방식을 사용한다. 재귀 쿼리를 받은 리졸버는 어떤 방법을 쓰든 완전한 답변을 돌려주거나 찾을 수 없다는 오류를 반환해야 하는 의무를 지게 된다. 클라이언트 입장에서는 한 번의 질의로 최종 결과를 받을 수 있으므로 구현이 단순해지고 복잡한 이름 풀이 로직은 전적으로 재귀 리졸버가 처리하게 된다.

반복 쿼리는 재귀 쿼리와 달리 상대 서버에게 최선의 답변만을 요청하는 방식이다. 재귀 리졸버가 루트 네임서버, TLD 네임서버, 권한이 있는 네임서버에 질의할 때 이 방식을 사용한다. 반복 쿼리를 받은 서버는 자신이 알고 있는 범위 내에서 답을 주거나 더 잘 알 것 같은 다른 서버의 주소를 알려주는 참조 응답(Referral)을 반환한다. 재귀 리졸버는 이 참조 응답을 따라가며 여러 서버에 순차적으로 질의하여 최종 답변을 찾아낸다.

정리하면, 일반적인 DNS 이름 풀이 과정에서 스터브 리졸버와 재귀 리졸버 사이에는 재귀 쿼리가 사용되고 재귀 리졸버와 각종 네임서버들 사이에는 반복 쿼리가 사용된다. 루트 네임서버, TLD 네임서버, 권한이 있는 네임서버와 같은 서버들은 모두 반복 쿼리만을 처리하며 다른 서버를 대신해서 질의를 수행하지 않는다.

A 레코드와 CNAME

리소스 레코드에는 여러 레코드 타입이 있지만 가장 기본이 되는 것은 A 레코드와 CNAME 레코드다.

A 레코드(Address Record)는 도메인 이름을 IPv4 주소에 직접 매핑한다. 사용자가 도메인에 접속할 때 최종적으로 필요한 것이 바로 이 A 레코드에 담긴 IP 주소다. 하나의 도메인에 여러 개의 A 레코드를 설정하여 트래픽을 분산시킬 수도 있다.

CNAME 레코드(Canonical Name Record)는 도메인 이름을 다른 도메인 이름으로 매핑하는 별칭 레코드다. CNAME은 IP 주소를 직접 가리키지 않고 다른 도메인을 가리킨다. 리졸버가 CNAME 레코드를 만나면 해당 별칭이 가리키는 도메인에 대해 다시 질의를 수행하여 최종적으로 A 레코드를 찾아낸다. CNAME은 여러 서브도메인이 같은 서버를 가리켜야 할 때 유용하다. 서버의 IP 주소가 변경되더라도 A 레코드 하나만 수정하면 해당 도메인을 가리키는 모든 CNAME이 자동으로 새 IP를 따라가게 된다.

다음은 example.com 도메인의 존 파일 예시다.

$TTL 3600
@       IN  SOA   ns1.example.com. admin.example.com. (
                  2024010101  ; Serial
                  7200        ; Refresh
                  3600        ; Retry
                  1209600     ; Expire
                  3600 )      ; Minimum TTL

@       IN  NS    ns1.example.com.
@       IN  NS    ns2.example.com.

@       IN  A     93.184.216.34
www     IN  CNAME example.com.
blog    IN  CNAME example.com.
api     IN  A     93.184.216.50

; 외부 서비스 연동
cdn     IN  CNAME d1234abcd.cloudfront.net.
app     IN  CNAME my-app.vercel.app.
storage IN  CNAME my-bucket.s3.amazonaws.com.

ns1     IN  A     93.184.216.10
ns2     IN  A     93.184.216.11

이 존 파일에서 @는 현재 도메인인 example.com을 의미한다. example.com은 A 레코드를 통해 93.184.216.34라는 IP 주소에 직접 매핑되어 있다. www.example.comblog.example.com은 CNAME 레코드로 example.com을 가리키므로, 결과적으로 같은 IP 주소로 연결된다. 반면 api.example.com은 별도의 A 레코드를 가지고 있어 다른 IP 주소를 사용한다.

CNAME은 같은 도메인 내의 다른 이름뿐 아니라 완전히 다른 도메인을 가리킬 수도 있다. 위 예시에서 cdn, app, storage 서브도메인은 각각 외부 서비스 제공자의 도메인을 가리킨다. 이 방식의 장점은 외부 서비스의 실제 IP 주소를 알 필요가 없다는 것이다. 서비스 제공자가 내부적으로 서버를 이전하거나 IP를 변경하더라도 사용자의 DNS 설정은 그대로 유지된다. 서비스 제공자의 네임서버가 항상 현재 유효한 IP 주소를 응답하기 때문이다.

DNS와 보안

DoT와 DoH

전통적인 DNS 질의는 암호화되지 않은 평문으로 전송된다. 이는 네트워크 경로 상의 누군가가 사용자가 어떤 도메인에 접속하려는지 엿볼 수 있고 응답을 조작할 수도 있다는 의미다.

DoT(DNS over TLS)와 DoH(DNS over HTTPS)는 이 문제를 해결하기 위해 DNS 질의를 암호화한다. DoT는 853번 포트를 사용하여 TLS로 DNS 트래픽을 감싸고 DoH는 일반 HTTPS 트래픽과 동일한 443번 포트를 사용한다. DoH는 일반 웹 트래픽과 구분이 어려워 차단하기 힘들다는 특징이 있다.

SNI와 ECH

DNS 질의를 암호화하더라도 여전히 접속하려는 도메인이 노출되는 지점이 있다. HTTPS 연결을 시작할 때 클라이언트는 SNI(Server Name Indication)라는 필드에 접속하려는 도메인 이름을 평문으로 보낸다. 하나의 IP 주소에서 여러 도메인을 호스팅하는 서버가 어떤 인증서를 제시할지 알아야 하기 때문이다.

ECH(Encrypted Client Hello)는 SNI를 포함한 ClientHello 메시지 전체를 암호화하여 이 문제를 해결한다. ECH를 사용하면 네트워크 관찰자는 사용자가 어떤 도메인에 접속하는지 알 수 없게 된다.

Read more →
2

Agent Skill도 Tool Use로 시작합니다.

자손킴 @jasonkim@hackers.pub

Agent Skill은 Anthropic이 2025년 10월에 발표한 기능이다. 발표 직후부터 폭발적인 반응을 얻어 커뮤니티에서 다양한 종류의 Skill이 만들어졌다. 2025년 12월 18일에 Anthropic은 Agent Skills를 독립적인 오픈 스탠다드로 발표했고 여러 서비스들이 Skill을 지원하고 있다.

이번 글에서는 Agent Skill이 Tool Use 위에서 어떻게 동작하는지 알아본다.

Agent Skill이란?

Agent Skill은 에이전트가 특정 작업을 더 정확하고 효율적으로 수행할 수 있도록 지시문(instructions), 스크립트(scripts), 리소스(resources) 등을 동적으로 불러올 수 있게 구성된 폴더다.

에이전트는 점점 더 많은 것을 할 수 있지만 실제 업무를 안정적으로 수행하려면 절차적 지식과 조직별 맥락이 필요하다. PDF 양식을 채우는 방법, 데이터베이스 마이그레이션을 안전하게 수행하는 순서, 브라우저 자동화의 베스트 프랙티스 같은 것들이다. 이런 지식을 매번 프롬프트에 모두 작성하면 컨텍스트를 낭비하게 되고 일관성도 떨어진다.

Agent Skill은 이러한 문제들을 해결하기 위해 작업에 필요한 지식을 재사용 가능한 단위로 패키징하고 필요할 때만 동적으로 로드한다.

효율적인 컨텍스트 관리 방법

Agent Skill은 점진적 공개(Progressive Disclosure) 패턴으로 컨텍스트를 효율적으로 관리한다. 점진적 공개는 다음과 같은 단계로 구성된다.

첫 번째 단계: 메타데이터 로드

에이전트가 시작할 때 모든 Skill의 name과 description만 로드한다. 이 메타데이터는 Claude가 각 Skill을 언제 사용해야 하는지 판단할 수 있을 만큼의 정보만 제공한다. 예를 들어 PDF Skill은 "PDF 파일에서 텍스트 추출, 폼 채우기, 문서 병합을 수행한다"는 설명만 시스템 프롬프트에 포함된다.

두 번째 단계: SKILL.md 전체 로드

Claude가 현재 작업에 해당 Skill이 관련 있다고 판단하면 전체 SKILL.md를 컨텍스트에 로드한다. 이 단계에서 상세한 지시문이 추가된다. 권장 크기는 5000 토큰 미만이다.

세 번째 단계 이상: 추가 파일 온디맨드 로드

Skill이 복잡해지면 모든 내용을 SKILL.md 하나에 담기 어려워진다. 이런 경우 references/, scripts/, assets/ 폴더에 추가 파일을 번들하고 SKILL.md에서 참조한다. Claude는 필요할 때만 이 파일들을 탐색하고 로드한다.

이 패턴의 장점은 "필요할 때만 필요한 만큼"이다. 모든 Skill의 전체 지시문을 처음부터 로드하면 컨텍스트가 금방 소진된다. 점진적 공개는 이 문제를 해결하면서도 에이전트가 적절한 시점에 적절한 Skill을 활성화할 수 있게 한다.

Agent Skill의 구조

일반적인 Skill의 구조는 다음과 같다.

skill-name/
├── SKILL.md           # 필수: 메타데이터 + 지시문
├── scripts/           # 선택: 실행 가능한 코드
├── references/        # 선택: 추가 문서
└── assets/            # 선택: 템플릿, 리소스

SKILL.md만 필수이고 나머지는 모두 선택이다. 단순한 Skill은 SKILL.md 하나만으로 구성될 수 있고 복잡한 Skill은 여러 개의 스크립트와 참조 문서를 포함할 수 있다.

필수요소인 SKILL.md는 다음과 같은 포맷으로 구성된다.

---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
license: Apache-2.0
compatibility: Designed for Claude Code
metadata:
  author: example-org
  version: "1.0"
allowed-tools: Bash(git:*) Read
---

# PDF Processing

## When to use this skill
Use this skill when the user needs to work with PDF files...

## How to extract text
1. Use pdfplumber for text extraction...

SKILL.md는 YAML frontmatter와 마크다운 본문으로 구성된다. YAML frontmatter에는 name과 description이 필수로 포함되어야 한다. name은 최대 64자의 소문자와 숫자 그리고 하이픈으로만 구성되며 하이픈으로 시작하거나 끝날 수 없다. description은 최대 1024자로 이 Skill이 무엇을 하는지 언제 사용해야 하는지 설명한다.

license, compatibility, metadata, allowed-tools는 선택 필드다. 각 선택 필드의 역할은 다음과 같다.

  • license: 라이선스 이름 또는 번들된 라이선스 파일에 대한 참조를 명시한다.
  • compatibility: 최대 500자. 환경 요구사항을 명시한다. 의도한 제품, 필요한 시스템 패키지, 네트워크 접근 필요 여부 등을 기술한다. 예를 들어 "Designed for Claude Code" 또는 "Requires git, docker, jq, and access to the internet" 같은 형태로 작성한다. 대부분의 Skill은 이 필드가 필요하지 않다.
  • metadata: 임의의 키-값 쌍을 저장하는 맵이다. author, version 같은 추가 속성을 담는다.
  • allowed-tools: 공백으로 구분된 사전 승인 도구 목록이다. 실험적 기능으로 에이전트 구현에 따라 지원 여부가 다를 수 있다.

YAML frontmatter 아래의 마크다운 본문이 실제 지시문이 된다. 이 지시문은 Skill이 활성화될 때 컨텍스트에 주입되어 에이전트의 행동을 안내한다.

첫번째 단계에서는 frontmatter의 name과 description만 사용된다. 이 정보로 에이전트는 언제 이 Skill을 활성화해야 하는지 판단한다. 두번째 단계에서 SKILL.md 전체가 로드되고 세번째 단계에서 마크다운 본문의 지시문에 따라 scripts/ 폴더의 코드를 실행하거나 references/ 폴더의 추가 문서를 참조한다.

dev-browser로 살펴보는 실제 Skill 동작

이제 Skill이 어떻게 동작하는지 실제 예시를 통해 살펴보자. 이 예시는 Use Claude Code with Chrome에 있는 사용 예시를 dev-browser Skill을 사용해 테스트하고 분석한 것이다.

사용한 프롬프트는 다음과 같다.

Go to code.claude.com/docs, click on the search box,
type "hooks", and tell me what results appear

이 요청은 tools 배열과 함께 전송되며 Skill도 tools 배열에 포함되어 있다. Claude API 요청 구조에 대해서는 이전 글을 참고한다.

{
  "name": "Skill",
  "description": "Execute a skill within the main conversation\n\n<skills_instructions>...",
  "input_schema": {
    "type": "object",
    "properties": {
      "skill": {
        "type": "string",
        "description": "The skill name. E.g., \"commit\", \"review-pr\", or \"pdf\""
      },
      "args": {
        "type": "string",
        "description": "Optional arguments for the skill"
      }
    },
    "required": ["skill"]
  }
}

Skill 도구의 description에는 <available_skills> 섹션이 포함되어 있어 사용 가능한 모든 Skill의 목록과 설명이 들어있다.

<available_skills>
<skill>
<n>dev-browser:dev-browser</n>
<description>
Browser automation with persistent page state. Use when users ask to 
navigate websites, fill forms, take screenshots, extract web data, 
test web apps, or automate browser workflows. Trigger phrases include 
"go to [url]", "click on", "fill out the form", "take a screenshot"...
</description>
<location>plugin</location>
</skill>
</available_skills>

사용자의 요청 "Go to code.claude.com/docs, click on the search box..."가 dev-browser의 description에 있는 트리거 프레이즈 "go to [url]", "click on"과 매칭된다. 에이전트는 이 매칭을 발견하고 Skill 도구를 호출한다.

{
  "type": "tool_use",
  "id": "toolu_017StpNdwovc4Lm8tGfK9XnA",
  "name": "Skill",
  "input": {
    "skill": "dev-browser:dev-browser",
    "args": "Go to code.claude.com/docs, click on the search box, type \"hooks\", and tell me what results appear"
  }
}

skill 필드에 plugin name을 포함한 qualified name(plugin-name:skill-name)이 사용되고 args에는 사용자의 원본 요청이 그대로 전달되었다.

Skill 도구의 tool_result로 SKILL.md 전체 내용이 반환된다.

Launching skill: dev-browser:dev-browser

Base directory for this skill: /Users/dev-test/.claude/plugins/cache/
dev-browser-marketplace/dev-browser/58c332a7c61a/skills/dev-browser

# Dev Browser Skill

Browser automation that maintains page state across script executions. 
Write small, focused scripts to accomplish tasks incrementally...

## Setup

First, start the dev-browser server using the startup script:

```bash
./skills/dev-browser/server.sh &
```

## Writing Scripts

Execute scripts inline using heredocs:

```bash
cd skills/dev-browser && npx tsx <<'EOF'
import { connect } from "@/client.js";
const client = await connect();
const page = await client.page("homepage");
// Your automation code here
await client.disconnect();
EOF
```

ARGUMENTS: Go to code.claude.com/docs, click on the search box...

tool_result에는 SKILL.md에 있는 모든 지시문이 포함되어 있다. ARGUMENTS에는 원본 사용자 요청이 첨부되어 있어 에이전트가 참조할 수 있다.

이제 에이전트는 SKILL.md의 지시문을 읽고 순서대로 작업을 수행한다. 먼저 Setup 섹션에 따라 서버를 시작한다.

{
  "type": "tool_use",
  "name": "Bash",
  "input": {
    "command": "cd /Users/dev-test/.claude/plugins/cache/dev-browser-marketplace/dev-browser/58c332a7c61a/skills/dev-browser && ./server.sh &",
    "description": "Start dev-browser server"
  }
}

서버가 준비되면 SKILL.md의 인라인 코드 템플릿을 참고하여 브라우저 자동화 스크립트를 작성하고 실행한다.

{
  "type": "tool_use",
  "name": "Bash",
  "input": {
    "command": "cd /Users/dev-test/.claude/plugins/cache/dev-browser-marketplace/dev-browser/58c332a7c61a/skills/dev-browser && npx tsx <<'EOF'\nimport { connect, waitForPageLoad } from \"@/client.js\";\n\nconst client = await connect();\nconst page = await client.page(\"claude-docs\");\nawait page.setViewportSize({ width: 1280, height: 800 });\n\nawait page.goto(\"https://docs.anthropic.com/en/docs/claude-code\");\nawait waitForPageLoad(page);\n\nconsole.log(\"Current URL:\", page.url());\nconst snapshot = await client.getAISnapshot(\"claude-docs\");\nconsole.log(snapshot);\n\nawait client.disconnect();\nEOF",
    "description": "Navigate to Claude Code docs"
  }
}

에이전트는 SKILL.md의 코드 템플릿을 그대로 복사하지 않는다. 템플릿 구조(import, connect, heredoc 패턴)를 따르되 작업에 맞게 즉석에서 코드를 작성한다. page.goto() URL이나 selectSnapshotRef() ref ID 등을 동적으로 결정한다.

SKILL.md가 명시한 Workflow Loop 패턴에 따라 Write a script → Run it → Evaluate → Decide → Repeat 과정이 반복된다. 페이지 탐색 스크립트 실행 → ARIA 스냅샷 확인 → 검색 버튼 클릭 → 검색어 입력 → 결과 확인 순서로 진행된다.

전체 흐름을 정리하면 다음과 같다.

User: "Go to code.claude.com/docs... Use dev-browser"


LLM: available_skills에서 매칭 발견
     description에 "go to", "click on" 트리거 포함


tool_use: Skill
  skill: "dev-browser:dev-browser"
  args: "Go to code.claude.com/docs..."


tool_result: SKILL.md 전체 + Base directory + ARGUMENTS


LLM: SKILL.md 지시문 해석
     "First, start the dev-browser server"


tool_use: Bash (./server.sh &)  ──► tool_result: "Server ready"


LLM: heredoc 템플릿 참고하여 스크립트 작성
     page.goto(), getAISnapshot() 활용


tool_use: Bash (npx tsx <<'EOF'...)


tool_result: snapshot 출력 (ARIA 트리)


(반복: 클릭, 입력, 스크린샷 등)

Skill 도구의 역할은 SKILL.md 파일 경로를 해석하고 전체 내용을 tool_result로 반환하는 것뿐이다. 실제 능력은 에이전트가 SKILL.md를 읽고 지시문에 따라 다른 도구들을 사용하면서 발현된다.

Agent Skill과 Subagent

Subagent와 Agent Skill은 서로 다른 문제를 해결한다.

Subagent는 컨텍스트 분리가 필요할 때 사용한다. 탐색이나 분석 과정이 메인 대화를 오염시키면 안 될 때 적합하다. 예를 들어 코드베이스 전체를 탐색해야 하는데 그 과정의 모든 파일 내용이 메인 컨텍스트에 쌓이면 금방 컨텍스트가 소진된다. Subagent는 독립적인 컨텍스트 윈도우에서 작업하고 결과만 반환한다. 또한 가벼운 모델(Haiku)로 빠르게 처리하거나 무거운 모델(Opus)로 깊이 분석하는 선택이 가능하다.

Agent Skill은 절차적 지식이 필요할 때 사용한다. PDF 폼 채우기나 브라우저 자동화처럼 "어떻게 해야 하는지"에 대한 베스트 프랙티스가 있는 작업에 적합하다. Skill은 현재 컨텍스트를 공유하면서 지시문만 추가로 주입한다. 별도의 메시지 루프를 만들지 않는다.

Agent Skill과 MCP

MCP와 Agent Skill도 역할이 다르다.

MCP는 외부 시스템과의 연동이 필요할 때 사용한다. 브라우저, 데이터베이스, 외부 API처럼 에이전트 내부에서 직접 실행하기 어려운 도구가 필요할 때 적합하다. MCP 서버는 외부 프로세스에서 실행되고 프로토콜을 통해 통신한다. 같은 도구를 여러 에이전트에서 공유할 수도 있다.

Agent Skill은 도구 사용 방법을 가르칠 때 사용한다. MCP가 "어떤 도구가 사용 가능한지"를 알려준다면 Skill은 "그 도구를 어떻게 효과적으로 사용하는지"를 가르친다. 실제로 mcp-builder라는 Skill은 MCP 서버를 더 잘 만들기 위한 지식을 제공한다. Skill이 MCP를 대체하는 것이 아니라 보완하는 관계다.

마무리

지금까지 Agent Skill이 Tool Use 위에서 어떻게 동작하는지 알아보았다.

Skill 도구가 tools 배열에 정의되어 있고 tool_use → tool_result 사이클을 거친다. 이는 Subagent(Task 도구)나 MCP(mcp__xxx 도구)와 동일한 패턴이다.

tools 배열
├── 내장 도구 (Bash, Read, Glob...)
│   └── Host 내부에서 직접 실행

├── Task 도구 (Subagent)
│   └── 새 메시지 루프에서 LLM 응답 반환

├── mcp__xxx 도구 (MCP)
│   └── 외부 서버의 실행 결과 반환

└── Skill 도구 (Skills)
    └── SKILL.md 로드 후 후속 도구 사용 안내

Agent Skill은 다른 도구 사용을 안내하는 메타 도구다. tool_result로 지시문을 컨텍스트에 주입하고 이후 Bash, Read 같은 다른 도구들의 사용을 안내한다. 결국 Skill → Bash → Read... 형태의 도구 체이닝이 발생한다.

Subagent와 MCP가 "무엇을 할 수 있는가"를 확장한다면 Skills는 "어떻게 잘 할 것인가"를 확장한다.

Read more →
7

MCP도 Tool Use를 사용합니다.

자손킴 @jasonkim@hackers.pub

지난 글에서는 Subagent가 Tool Use 위에서 어떻게 동작하는지 알아보았다. 이번 글에서는 MCP(Model Context Protocol)가 Tool Use와 어떻게 연결되는지 내장 도구인 Subagent를 예시로 비교하며 설명할 것이다. 또한 내장 도구가 있음에도 불구하고 MCP가 필요한 이유에 대해서도 알아본다.

내장 도구 vs MCP 도구

Subagent 글에서 살펴본 Task 도구는 에이전트에 내장된 도구였다. MCP 도구는 어떻게 다를까? 결론부터 말하면 LLM 입장에서는 둘 다 그냥 도구다. 차이는 실행이 어디서 일어나는가뿐이다.

내장 도구든 MCP 도구든 API 요청의 tools 배열에 동일한 형태로 들어간다:

{
  "tools": [
    {
      "name": "Read",
      "description": "Reads a file from the local filesystem...",
      "input_schema": { ... }
    },
    {
      "name": "Task",
      "description": "Launch a new agent to handle complex tasks...",
      "input_schema": { ... }
    },
    {
      "name": "mcp__claude-in-chrome__navigate",
      "description": "Navigate to a URL in the browser...",
      "input_schema": { ... }
    }
  ]
}

LLM은 도구 이름과 description, input_schema만 보고 어떤 도구를 호출할지 결정한다. 이 도구가 내장인지 MCP인지는 알 수 없고 알 필요도 없다.

핵심 차이는 도구가 어디서 실행되는가다.

내장 도구 (예: Task) MCP 도구
실행 위치 Host 내부 Host 외부 (별도 프로세스)
통신 방식 함수 호출 프로토콜 (STDIO/HTTP)
실행 주체 Host (또는 LLM) 외부 시스템
결과 Host가 생성한 데이터 외부 시스템이 반환한 데이터

다이어그램으로 보면 더 명확하다:

                        Host 프로세스
    ─────────────────────────────────────────────────
    
                         Agent

              ┌────────────┴────────────┐
              ▼                         ▼
            Task                   mcp__xxx 도구
          (내장 도구)                     │
              │                         │
              ▼                         │ STDIO / HTTP
         새 메시지 루프                    │
           (LLM)                       │

    ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─

                        외부 프로세스     ▼
                                   MCP Server
                               (Chrome, DB...)
  • 내장 도구 흐름: Agent → Task → 새 메시지 루프 (모두 Host 프로세스 내부)
  • MCP 도구 흐름: Agent → mcp__xxx 도구 → [프로세스 경계] → MCP Server (Host 외부)

실제 예시로 보는 차이

지난 글에서 본 Explorer subagent 호출과 MCP 도구 호출을 비교해보자.

내장 도구 (Task) 호출:

{
  "type": "tool_use",
  "id": "toolu_01ABC123",
  "name": "Task",
  "input": {
    "subagent_type": "Explore",
    "prompt": "entity 구조를 탐색해주세요",
    "description": "Entity 구조 탐색"
  }
}

Task 도구가 호출되면 Host 내부에서 새로운 메시지 루프가 생성되고, Haiku 모델이 Glob, Read 등 다른 내장 도구로 탐색을 수행한다. 결과는 LLM이 생성한 분석 텍스트다.

MCP 도구 호출:

{
  "type": "tool_use",
  "id": "toolu_01DEF456",
  "name": "mcp__claude-in-chrome__navigate",
  "input": {
    "tabId": 12345,
    "url": "http://localhost:3000"
  }
}

MCP 도구가 호출되면 Host는 외부의 MCP Server(Chrome 브라우저 프로세스)에 명령을 전달한다. 결과는 브라우저가 반환한 데이터(스크린샷, 콘솔 로그 등)다.

Agent 입장에서는 둘 다 tool_use 요청을 받아 실행하고 tool_result를 반환하는 동일한 패턴이다.

MCP 도구가 tools 배열에 포함되는 방식

MCP 서버가 제공하는 도구들은 어떻게 tools 배열에 들어갈까? MCP 도구는 mcp__server-name__tool-name 형태의 이름을 가진다.

{
  "name": "mcp__claude-in-chrome__navigate",
  "description": "Navigate to a URL, or go forward/back in browser history...",
  "input_schema": {
    "type": "object",
    "properties": {
      "tabId": {
        "description": "Tab ID to navigate",
        "type": "number"
      },
      "url": {
        "description": "The URL to navigate to",
        "type": "string"
      }
    },
    "required": ["tabId", "url"]
  }
}

이 네이밍 규칙이 필요한 이유는 여러 MCP 서버가 동시에 연결될 수 있기 때문이다. 예를 들어 filesystem 서버와 github 서버가 둘 다 read라는 도구를 제공한다면 충돌이 발생한다. mcp__filesystem__readmcp__github__read로 구분하면 이 문제가 해결된다.

Claude Code에서 /mcp를 입력하면 연결된 MCP 서버 목록을 볼 수 있다. Claude in Chrome이 제공하는 도구들을 살펴보자:

도구 이름 설명
mcp__claude-in-chrome__navigate URL로 이동하거나 브라우저 히스토리 앞/뒤로 이동
mcp__claude-in-chrome__computer 마우스/키보드로 브라우저와 상호작용, 스크린샷 촬영
mcp__claude-in-chrome__read_page 페이지의 접근성 트리 표현을 가져옴
mcp__claude-in-chrome__find 자연어로 페이지 요소 찾기
mcp__claude-in-chrome__form_input 폼 요소에 값 입력
mcp__claude-in-chrome__javascript_tool 페이지 컨텍스트에서 JavaScript 실행

이 도구들은 에이전트가 MCP 서버에 연결할 때 서버로부터 목록을 받아와 tools 배열에 추가된다. 에이전트가 시작될 때 대략 다음과 같은 과정이 일어난다:

  1. 에이전트가 설정된 MCP 서버들에 연결
  2. 각 서버에 tools/list 요청을 보내 제공하는 도구 목록 수신
  3. 받은 도구들에 mcp__server-name__ prefix를 붙여 tools 배열에 추가
  4. API 요청 시 내장 도구와 함께 전송

MCP 도구 호출 흐름

Claude가 MCP 도구를 호출하면 에이전트는 다음 단계를 수행한다:

Claude                    Agent                    MCP Server
   │                        │                          │
   │  tool_use              │                          │
   │  (mcp__claude-in-      │                          │
   │   chrome__navigate)    │                          │
   │ ─────────────────────► │                          │
   │                        │                          │
   │                      prefix 파싱                   │
   │                      server: claude-in-chrome     │
   │                      tool: navigate               │
   │                        │                          │
   │                        │  tools/call              │
   │                        │ ───────────────────────► │
   │                        │                          │
   │                        │  실행 결과                 │
   │                        │ ◄─────────────────────── │
   │                        │                          │
   │  tool_result           │                          │
   │ ◄───────────────────── │                          │
   │                        │                          │

결국 MCP 도구 호출도 일반 Tool Use와 동일한 패턴을 따른다. 차이점은 에이전트가 도구를 직접 실행하는 대신 외부 MCP 서버에 위임한다는 것뿐이다.

MCP는 왜 도구를 외부로 분리하는가

지금까지 MCP 도구가 어떻게 동작하는지 살펴보았다. 그런데 왜 이런 구조가 필요할까?

모든 도구를 내장할 수 없다

에이전트에 도구를 추가하는 가장 단순한 방법은 에이전트 내부에 직접 구현하는 것이다. 하지만 이 방식에는 한계가 있다:

  • 모든 도구를 미리 구현할 수 없다: 파일 시스템, 데이터베이스, 브라우저, Slack, GitHub, Jira... 세상에는 수많은 시스템이 있고 에이전트 개발자가 이 모든 연동을 직접 구현하기는 불가능하다.
  • 사용자마다 필요한 도구가 다르다: 어떤 사용자는 PostgreSQL을, 다른 사용자는 MongoDB를 사용한다. 모든 조합을 에이전트에 내장할 수 없다.
  • 도구 업데이트가 어렵다: 외부 API가 변경되면 에이전트 전체를 다시 배포해야 한다.

MCP는 이 문제를 도구 제공자와 도구 사용자의 분리로 해결한다.

MCP의 핵심 구조

MCP는 클라이언트-서버 아키텍처를 따른다.

참여자 (Participants):

  • MCP Host: MCP 클라이언트를 관리하는 AI 애플리케이션 (예: Claude Desktop, VS Code, Claude Code)
  • MCP Client: MCP 서버와 연결을 유지하고 컨텍스트를 얻어오는 컴포넌트
  • MCP Server: MCP 클라이언트에게 컨텍스트를 제공하는 프로그램 (도구 제공자)

Host와 Client의 관계:

  • Host는 MCP 서버 연결마다 별도의 MCP Client를 생성한다
  • 예를 들어 Claude Code(Host)가 Chrome 서버와 filesystem 서버에 연결하면 두 개의 MCP Client 객체가 생성된다
  • 각 MCP Client는 하나의 MCP Server와 전용 연결을 유지한다

이 분리 덕분에:

  • 도구 제공자는 MCP 서버만 만들면 됨 (에이전트 코드 수정 불필요)
  • 에이전트 개발자는 MCP 클라이언트만 구현하면 모든 MCP 서버의 도구 사용 가능
  • 사용자는 필요한 MCP 서버만 설치하여 에이전트 기능 확장 가능

MCP와 인증

원격 MCP 서버를 사용할 때는 인증이 필요한 상황이 발생한다. MCP 서버가 사용자의 GitHub 저장소에 접근하거나 Slack 워크스페이스에 메시지를 보내야 할 때, "이 요청이 정말 이 사용자로부터 온 것인가?"를 확인해야 한다.

MCP는 프로토콜 수준에서 OAuth 2.1 인증 체계를 표준화했다. 덕분에 어떤 MCP 클라이언트든 동일한 방식으로 MCP 서버에 인증할 수 있고, MCP 서버 개발자는 인증 로직을 한 번만 구현하면 모든 클라이언트와 호환된다.

Tool Use를 넘어서

지금까지 MCP를 Tool Use의 확장으로 설명했다. 실제로 MCP 도구는 가장 많이 사용되는 기능이고, LLM이 외부 시스템과 상호작용하는 핵심 방식이다.

하지만 MCP가 제공하는 것이 도구만은 아니다. MCP 명세를 보면 Tool Use와 무관하게 동작하는 기능들이 있다. 이 기능들은 tool_use -> tool_result 사이클을 거치지 않고 다른 방식으로 LLM에게 컨텍스트를 제공하거나 LLM의 능력을 활용한다.

MCP의 확장 기능

MCP는 도구(Tools) 외에도 몇가지 핵심 기능들이 있다. Resources, Prompts, 그리고 Sampling이다.

Resources

Resources는 Tool Use를 거치지 않는 데이터 제공 기능이다. 도구는 LLM이 "행동"을 요청할 때 호출되지만 Resource는 LLM이 응답을 생성하기 전에 컨텍스트로 미리 주입된다.

예를 들어 PostgreSQL MCP 서버가 데이터베이스 스키마를 Resource로 노출한다고 하자. 사용자가 "users 테이블에 email 컬럼 추가해줘"라고 요청하면 LLM은 별도의 도구 호출 없이도 현재 스키마 구조를 이미 알고 있다. SELECT * FROM information_schema.columns를 먼저 실행할 필요가 없는 것이다. Resource가 컨텍스트에 미리 주입되어 있기 때문이다.

Prompts

Prompts도 Tool Use와 무관하다. MCP 서버가 미리 정의한 재사용 가능한 프롬프트 템플릿으로 클라이언트가 직접 요청해서 가져온다.

예를 들어 코드 리뷰 MCP 서버가 "보안 취약점 분석" 프롬프트 템플릿을 제공하면 클라이언트는 이 템플릿을 불러와 LLM에게 전달할 수 있다. LLM이 도구를 호출하는 것이 아니라 클라이언트가 MCP 서버로부터 프롬프트를 받아오는 것이다.

Sampling (역방향 LLM 호출)

Sampling은 가장 독특한 기능이다. 일반적인 MCP 흐름은 LLM → Agent → MCP Server지만 Sampling은 이 방향을 뒤집는다:

일반 흐름:     LLM → Agent → MCP Server
Sampling:     MCP Server → Agent → LLM

MCP 서버가 복잡한 판단이 필요할 때 역으로 LLM에게 질문할 수 있다. 예를 들어 코드 분석 MCP 서버가 특정 패턴을 발견했을 때 "이 코드가 보안 취약점인지 판단해달라"고 LLM에게 요청하는 식이다. MCP 서버는 LLM의 답변을 기반으로 최종 결과를 만들거나 다른 도구나 함수를 사용하는 등의 판단을 할 수 있게 된다.

Sampling은 Tool Use의 tool_use -> tool_result 패턴이 아니라 MCP 프로토콜 자체의 sampling/createMessage 요청을 통해 동작한다.

마무리

지금까지 MCP가 Tool Use 위에서 어떻게 동작하고 또 Tool Use를 넘어 어떤 기능들을 제공하는지 살펴보았다.

MCP 도구는 Tool Use다. 내장 도구와 동일하게 tools 배열에 포함되고 tool_use로 호출되며 tool_result로 결과가 반환된다. LLM 입장에서는 구분이 없다. 차이점은 실행 위치뿐이다. 내장 도구는 Host 프로세스 내부에서, MCP 도구는 외부 MCP Server에서 실행된다.

하지만 MCP는 도구만 제공하지 않는다. Resources와 Prompts는 Tool Use 없이 컨텍스트를 제공하고 Sampling은 MCP 서버가 역으로 LLM을 활용할 수 있게 한다. MCP는 Tool Use를 확장하면서도 Tool Use만으로는 해결할 수 없는 영역까지 커버하는 프로토콜이다.

모든 도구를 에이전트에 내장할 수 없기 때문에 에이전트와 도구를 분리할 필요가 생겼고 MCP는 도구 제공자와 사용자를 분리하여 생태계 확장을 가능하게 한다.

이 글에서는 Tool Use의 기본 구조를, 이어지는 글에서는 Subagent가 Tool Use 위에서 동작함을 살펴보았고 이번 글에서 MCP가 Tool Use를 확장하면서도 그 이상의 기능을 제공함을 살펴보았다. Claude Code의 핵심 확장 기능들은 Tool Use라는 메커니즘 위에서 동작하지만 MCP 생태계는 그보다 더 넓은 가능성을 열어두고 있다.

Read more →
4
1
0

스마일 PRO 라식 수술 후기

자손킴 @jasonkim@hackers.pub

시력교정술을 받은 주변사람들이 신세계라며 추천을 해도 그동안 관심이 없었다. 어렸을때부터 안경을 쓰고 평생을 살아온지라 안경을 쓰는 것이 불편하다고 느껴 본 적이 없었기 때문이다.

라식을 해볼까? 라고 생각이 든 것은 스쿠버 다이빙을 시작하게 되면서였다. 다이빙에 취미를 붙이고나니 가장 불편한게 눈이었다. 렌즈를 끼고 있는게 불편한건 물론이고 아침 바쁜 와중에 렌즈가 안들어가서 진을 다 빼고 하루를 시작하는것도 문제였다. 게다가 렌즈를 부족하게 들고가거나 숙소에 렌즈를 놓고 오는 등 나의 정신머리 때문에 반쪽짜리 다이빙을 하게 되는 일이 반복되면서 시력교정을 해야겠다고 결심했다.

공장식 병원이야 어차피 거기서 거기라는 생각에 병원비교 사이트를 보고 적당한 곳을 골라 검사 예약을 했다. 원하면 당일 수술도 가능하다고 하던데 나는 검사만 먼저 받고 일주일후에 수술을 하기로 했다.

검사는 일반적인 안과 검사와 크게 다른 것은 없었다. 눈에 바람을 쏘는 것을 시작으로 뭔가를 들여다보고 빛을 비추고 등등 예닐곱가지의 검사가 진행되었다. 이어서 시력 검사를 하고 교정 후 도수를 결정했는데 이것은 안경 맞출때와 동일했다.

검사가 끝나고 의사선생님과 상담하며 눈의 상태에 대해서 설명을 들었다. 다행히 안압이나 눈 모양등도 정상이고 각막 두께도 평균이라 라섹, 라식 모두 원하는대로 진행이 가능하다고 했다. 다만 이제 노안이 오고 있기 때문에 돋보기를 쓰는 시간을 조금이라도 더 늦추려면 시력을 약간 낮추고 양눈 중 주로 가까이 보는 눈은 시력을 조금 더 낮춰 교정하는게 좋겠다는 제안을 받았다.

이어서 코디네이터에게 수술 종류와 방법에 대한 설명을 들었다. 나는 회복이 빠른게 최우선이었기 때문에 스마일PRO를 하기로 결정했다.

수술 당일에도 2~3가지의 검사를 다시하고 최종적으로 교정 시력에 대해서도 다시 한 번 확인을 하고 코디네이터에게 수술 후 주의사항과 안약 투여에 대한 설명을 듣는다.

눈에 물이 닿는 것과 격렬한 운동은 일주일 정도 피해야한다. 2주간은 금주하고 한 달동안 과음도 피해야 한다. 목욕탕, 사우나처럼 뜨거운 증기와 물은 한달간 피한다.

의사선생님을 만나 눈 상태에 대해서 최종점검을 하고 수술 대기실로 이동을 하여 위생모와 가운을 입고 잠시 기다리다 수술실로 들어갔다. 간호사님의 안내에 따라 수술장비에 누우면 눈을 감고 있으라 한 후 세팅을 시작하는데, 이때부터는 절대 고개를 들지말라고 한다.

잠시후 눈을 뜨라고 한 뒤 의사가 눈에 마취안약을 넣어줬다. 집게 같은걸로 눈을 벌리고 "이것만 문제 없으면 다른건 잘 참으실 수 있을거에요"라는 소리와 함께 눈앞에서 뭔가가 왔다갔다 하는데 아마도 마취가 되었는지 안구를 건드려 보는 것 같았다. 눈에 아무런 느낌은 없었다.

수술은 오른쪽 눈부터 진행되었다. 왼쪽눈에는 거즈 같은 것을 덮고 오른쪽 눈으로 앞에 보이는 초록불빛을 바라보라고 한다. 초록불빛을 바라볼때 다른 눈을 감으면 눈이 움직일 수 있으니 양쪽눈을 다 뜨되 수술하는 눈으로만 보라고 한다.

처음에는 크고 흐릿하게 보였던 초록불빛이 점점 작고 선명해 지면서 초록점으로 보인다.초록점을 눈의 중앙에 오도록 보라고 하는데, 이때가 가장 어려웠다. 초록점을 눈의 가운데에 오도록 보고 있는데 의사가 거기가 아니니 제대로 보라고 하는것이다. 몇 번을 다시 시도해도 잘 안됐는지 "환자분이 하는 수술인데 협조가 안되면 어떻게 하냐"며 의사가 약간 역정을 냈다.

초록점을 보는데 실패하면 기계가 눈을 잡아줄 수 없고 의사가 직접 조작하여 수술을 해야 하는데, 그러면 아무래도 기계만큼의 정확도가 나오지는 않는 모양이다.

어떻게하면 방향을 맞출 수 있을지 궁리하다 눈알을 오른쪽에서 왼쪽으로 천천히 굴려보다 의사가 됐다고 하면 멈춰보기로 했다. 눈알을 굴리는데 의사가 거기가 맞다고 하였다. 초록점은 안구의 중앙이 아닌 미간 정도의 위치에 있었다. 잘은 몰라도 사람마다 안구의 각도 같은게 차이가 있나보다.

이제 레이저를 조사하니 가만히 있으라 하고 의사와 간호사가 시간을 세어준다. 초록점을 보고 6~7초 정도 있으면 어느새 점이 사라지고 눈앞이 하얗게 보인다. 2~3초가 더 지나자 끝났다며 잘 참았다고 한다. 레이저가 조사되는 동안은 별다른 느낌은 없었다.

다음으로 왼쪽눈을 진행하는 오른쪽보다 훨씬 수월했다. 왼쪽눈도 우선 처음에는 초록점을 눈의 중앙에 오게 바라봤는데 그게 맞았는지 한 번에 진행되었다. 마찬가지로 약 10초정도가 걸려 레이저 조사가 끝났다.

레이저 조사가 끝나면 무언가로 눈을 후비적거리고 주사를 몇 개 놓고 안약등을 넣고 불빛을 쬐어준다. 이것은 레이저 조사가 마지막으로 끝난 왼쪽을 먼저 하고 오른쪽을 하였다. 아마도 각막 조각을 제거하고 소독등의 처치를 하는 것 같았다. 이 과정은 눈 한쪽당 1~2분 정도 걸렸던 것 같다.

모든 과정이 끝나면 간호사의 안내에 따라 장비에서 일어나 이동을 한다. 궁금한 마음으로 주변을 둘러봤는데 세상이 온통 뿌연게 온통 손자국이 번짐 안경을 쓰고있는 기분이었다. 뿌옇긴해도 이전보다 더 선명해진것은 체감이 됐다.

다시 한 번 의사선생님을 만나 눈에 이상이 없나 검사를 받고 몇가지 주의 사항을 들은 후 퇴원을 한다. 초반에는 빛번짐이 있을 수 있고 시력이 한번에 교정시력 만큼까지 잘보이는 건 아니지만 시간이 지나며 계속 더 잘보일 것 이라고 한다. 그리고 이제 눈이 시리기 시작할건데 1~3시간 정도면 가라앉을 거라는 이야기도 들었다.

퇴원하는 길에 약국에 들려서 안약 2종류와 인공눈물을 받았다. 안약은 일주일간 하루 4번을 투여하고 인공눈물은 수시로 넣으라고 한다. 안약을 여러개 넣을때는 최소한 5분 간격을 두고 넣으라고 하는데 종류가 여러개이다보니 안약 넣다보면 하루가 다 간다.

집에 오니 눈시림이 더 심해지고 눈에 다래끼나 나거나 눈썹이 들어간듯한 이물감이 느껴져서 눈을 뜨고 있을 수가 없었다. 눈을 떠도 온통 뿌옇게 보여서 사물의 형체는 분간이 되지만 글씨 같은건 읽을 수가 없었다. 안약을 넣어야 하는데 주의사항이 적혀있는 종이를 읽을 수 없어서 제미나이 라이브를 켜고서 읽어달라고 했다.

안약을 넣고서 침대에 누워 눈을 감았다. 눈을 감아도 빛이 밝으면 눈이 시려서 빛을 차단하고 누워있다 두어시간 자고 일어났다. 이물감은 여전했지만 눈시림과 빛번짐이 덜해서 눈을 뜨고 무언가를 볼 수는 있었다. 눈을 오래 뜨고 있으면 피로감이 있는건 마찬가지라 저녁 먹고 다시 안약을 넣고 일찍 잠을 청했다. 덕분에 밀린 수면 부채를 많이 갚았다.

눈의 피로감 때문인지 깊게는 못자고 자다 두어번 깼다. 일어나보니 새벽 5시쯤 되었는데 더이상 잠이 안오길래 후기나 써야겠다고 생각했다. 이물감이나 눈시림은 많이 나아졌고 눈도 어제보다는 더 선명하게 보이기 시작했다. 그러나 빛번짐으로 인해 탁하고 뿌옇게 보이는건 여전했다.

모니터나 스마트폰의 화면을 볼 수는 있는데 밝으면 눈이 아프고 집중하면 눈이 시려서 글자 크기를 키우고 화면 밝기는 최대한 낮췄다. 모니터를 오래 보면 눈이 금방 피로해져서 드문드문 후기를 적다가 진료 시간이 다되어 다시 병원을 찾았다.

라식 수술 후에는 1일, 1주일, 1개월, 3개월에 진료를 받는 것을 권장하고 6개월 이후에는 6개월~1년 주기로 한 번씩 검사를 받는 것을 권장한다고 한다.

불편한게 있는지 물어봐서 눈시림과 이물감이 있었지만 지금은 많이 좋아졌다고 답하였다. 먼거리와 가까운 거리의 시력을 다시 한 번 검사하고 의사선생님을 만나 안구 상태에 대해서 진료를 받았다. 어제 수술 중 초록점을 바라보는 문제에 대해 이야기를 나눴다.

내가 걱정했던 부분은 혹여나 잘못된 위치를 바라봐서 각막이 엉뚱하게 절삭된 것은 아닐까 하는 것이었다. 하루가 지나면서 그렇지는 않을 것 같다는 생각을 했으나 혹시 모를 일이라 한 번 더 의사선생님한테 물어봤다.

다행히 어제 수술도 정확히 되었고 오늘 안구 상태도 이상 없으니 걱정 말라는 답을 들었다. 혹여나 초록점을 제대로 못보는 환자가 있다면 나처럼 안구의 중앙이 아닌 다른 곳을 봐야 하는 것일 수 있으니 다른 곳을 보도록 유도하며 안구를 맞추면 좋겠다는 말씀을 드리고 다음주에 뵙자하고 진료를 마무리했다.

시간이 지날수록 시야가 점점 선명해 지는게 체감이 되고 있다. 그러나 뿌옇게 보이는 느낌은 아직 남아 있어서 안경을 닦거나 고쳐써야 할 것 같은데 그럴 안경이 없어서 당혹스러움을 느끼고 있다.

나는 이제 지금까지와는 다른 눈으로 세상을 보게 되었다.

Read more →
2

Subagent는 Tool Use입니다.

자손킴 @jasonkim@hackers.pub

지난 글에서는 Tool Use가 무엇이고 어떻게 동작하는지 알아보았다. 이번 글에서는 subagent가 Tool Use 위에서 어떻게 동작하는지 알아볼 것이다.

Tool Use 정리

Subagent 설명에 앞서 Tool Use를 간단히 정리해보고 넘어가자.

┌─────────────────────────────────────────────────────────────┐
│                        메인 메시지 루프                         │
│                                                             │
│   ┌──────┐    request     ┌─────────┐    API call   ┌─────┐ │
│   │ User │ ─────────────► │  Agent  │ ────────────► │ LLM │ │
│   │      │ ◄───────────── │         │ ◄──────────── │     │ │
│   └──────┘    response    │         │   tool_use    └─────┘ │
│                           │    │    │                       │
│                           │    ▼    │                       │
│                        ┌──────────────┐                     │
│                        │    Tools     │                     │
│                        │ (Bash, Read, │                     │
│                        │  Glob, ...)  │                     │
│                        └──────────────┘                     │
└─────────────────────────────────────────────────────────────┘

Tool Use는 LLM이 외부 도구를 호출할 수 있게 해주는 메커니즘이다. Agent는 LLM에게 사용 가능한 도구 목록을 제공하고 LLM은 필요할 때 tool_use 응답을 반환한다. Agent는 해당 도구를 실행하고 결과를 다시 LLM에게 전달한다. 이 과정이 반복되면서 복잡한 작업을 수행한다.

Subagent란?

Subagent는 특정 작업에 특화된 AI 에이전트다. 각 subagent는 자신만의 컨텍스트 윈도우에서 독립적으로 작동하며 완료되면 결과를 메인 에이전트에게 반환한다.

Claude Code에서 subagent는 Task라는 도구로 구현되어 있다. 메인 에이전트의 시스템 프롬프트에는 Task 도구의 description으로 사용 가능한 모든 subagent 목록과 각각의 용도가 포함된다. 메인 에이전트는 이 description을 참고하여 적절한 subagent를 선택한다. 예를 들어 "코드베이스 구조 파악"이 필요하면 Explore를, "데이터베이스 스키마 설계"가 필요하면 database-schema-architect를 호출한다.

Subagent의 구조

Subagent는 마크다운 파일로 정의되고 YAML frontmatter에 다음과 같은 설정을 포함한다.

---
name: code-reviewer
description: Expert code review specialist. Use immediately after writing or modifying code.
tools: Read, Grep, Glob, Bash
model: sonnet
---

You are a senior code reviewer ensuring high standards of code quality and security.

When invoked:
1. Run git diff to see recent changes
2. Focus on modified files
3. Begin review immediately

Review checklist:
- Code is simple and readable
- Functions and variables are well-named
...

각 필드의 역할은 다음과 같다:

필드 설명
name subagent의 고유 식별자
description 언제 이 subagent를 사용해야 하는지 설명
tools 이 subagent가 사용할 수 있는 도구 목록
model 사용할 모델 (sonnet, opus, haiku 또는 inherit)

YAML frontmatter 아래의 마크다운 본문이 subagent의 시스템 프롬프트가 된다. 이 프롬프트는 subagent가 호출될 때 API 요청의 system 필드에 주입되어 subagent의 행동을 정의한다.

이 subagent 정의는 메인 에이전트의 시스템 프롬프트에 있는 tools 섹션의 Task 도구 설명에 포함된다:

{
  "name": "Task",
  "description": "Launch a new agent to handle complex, multi-step tasks autonomously.

Available agent types and the tools they have access to:
- Explore: Fast agent specialized for exploring codebases. (Tools: All tools)
- Plan: Software architect agent for designing implementation plans. (Tools: All tools)
- database-schema-architect: Use this agent when you need to design database schemas...
...
"
}

Task 도구의 input schema는 다음과 같다:

{
  "type": "object",
  "properties": {
    "subagent_type": {
      "type": "string",
      "description": "The type of subagent to launch"
    },
    "prompt": {
      "type": "string",
      "description": "The task to delegate to the subagent"
    },
    "description": {
      "type": "string",
      "description": "Short description of what the agent will do"
    }
  }
}
  • subagent_type: 호출할 subagent 종류 (Explore, Plan, general-purpose 등)
  • prompt: subagent에게 전달할 작업 지시. 이 내용이 subagent의 user 메시지가 된다
  • description: 3-5단어의 짧은 요약. 로그나 UI에 표시된다

Subagent 호출 과정

실제 메시지 흐름을 통해 subagent가 어떻게 동작하는지 살펴보자.

1단계: 메인 에이전트가 Task 도구 호출

사용자가 "선물을 주고받는 기능을 위한 entity 설계"를 요청했다고 하자. 메인 에이전트(Opus 4.5)는 먼저 코드베이스를 탐색해야 한다고 판단하고 Explorer subagent를 호출한다:

{
  "content": [
    {
      "type": "thinking",
      "thinking": "사용자가 선물을 주고받는 기능을 위한 entity 설계를 요청했습니다. Plan mode가 활성화되어 있으므로, 먼저 코드베이스를 탐색하여 기존 구조를 이해해야 합니다.\n\nPhase 1에 따라:\n1. 기존 entity 구조와 패턴을 이해하기 위해 Explore agent를 사용\n2. 선물 관련 기존 구현이 있는지 확인\n3. 사용자와 아이템 관련 entity가 어떻게 구성되어 있는지 파악\n\n먼저 Explore agent를 사용하여 기존 entity 구조를 파악하겠습니다."
    },
    {
      "type": "text",
      "text": "선물 주고받기 기능을 위한 entity 설계를 시작하겠습니다. 먼저 기존 코드베이스의 entity 구조와 관련 패턴을 탐색하겠습니다."
    },
    {
      "type": "tool_use",
      "id": "toolu_01QjShG7gnF36vLgGKCWBNjg",
      "name": "Task",
      "input": {
        "subagent_type": "Explore",
        "prompt": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요:\n\n1. 기존 entity 파일들의 위치와 구조 파악...",
        "description": "Entity 구조 탐색"
      }
    }
  ]
}

thinking 블록에서 메인 에이전트는 현재 상황(Plan mode 활성화)을 파악하고 Explore agent를 선택한 이유를 명시적으로 추론했다.

2단계: 새로운 메시지 루프에서 Subagent 실행

Agent는 Task 도구 호출을 받으면 완전히 새로운 메시지 루프를 시작한다. 이 때 위에서 보았던 YAML frontmatter 하단의 마크다운 본문이 subagent 전용 시스템 프롬프트로 주입된다.

{
  "model": "claude-haiku-4-5-20251001",
  "system": [
    {
      "type": "text",
      "text": "You are Claude Code, Anthropic's official CLI for Claude."
    },
    {
      "type": "text",
      "text": "You are a file search specialist for Claude Code...

=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files
- Modifying existing files
- Deleting files
...

Your strengths:
- Rapidly finding files using glob patterns
- Searching code and text with powerful regex patterns
- Reading and analyzing file contents
..."
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# claudeMd\nCodebase and user instructions are ... <system-reminder>"
    },
    {
      "role": "user",
      "content": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요..."
    }
  ],
  "tools": [
    {"name": "Bash", ...},
    {"name": "Glob", ...},
    {"name": "Grep", ...},
    {"name": "Read", ...}
  ]
}
  • 모델: Explorer는 빠른 탐색을 위해 Haiku 4.5를 사용한다 (메인은 Opus 4.5)
  • 시스템 프롬프트: 읽기 전용 모드로 제한된 전용 프롬프트가 주입된다
  • 도구 제한: 메인 에이전트가 가진 모든 도구가 아닌, 탐색에 필요한 도구만 제공된다
  • CLAUDE.md 전달: 프로젝트의 CLAUDE.md 내용이 user 메시지의 <system-reminder> 태그 안에 포함되어 전달된다. 따라서 subagent도 프로젝트 컨벤션을 따른다
  • 메시지: 메인 에이전트의 prompt가 user 메시지로 전달된다

3단계: Subagent의 도구 사용

Subagent도 자신만의 메시지 루프에서 도구를 사용한다:

{
  "content": [
    {
      "type": "text",
      "text": "이 NestJS 프로젝트의 entity 구조를 철저하게 탐색하겠습니다."
    },
    {
      "type": "tool_use",
      "id": "toolu_01VHysdohmuy9r58aVdcUQRh",
      "name": "Glob",
      "input": {
        "pattern": "**/*.entity.ts"
      }
    },
    {
      "type": "tool_use",
      "id": "toolu_01QYrMBhzbXpExYwgCFM7MLb",
      "name": "Bash",
      "input": {
        "command": "find /Volumes/project/src -type f -name \"*.ts\" | grep -E \"(entity|entities)\" | head -20",
        "description": "Find entity files in src directory"
      }
    }
  ]
}

Explorer subagent는 두 개의 도구를 병렬로 호출했다. Glob으로 entity 파일 패턴을 찾고 Bash로 디렉토리를 탐색한다. 이 과정이 반복되면서 subagent는 필요한 정보를 수집한다.

4단계: Subagent 완료 및 결과 반환

Subagent가 탐색을 완료하면 수집한 정보를 정리하여 최종 응답을 반환한다. 이 응답은 메인 에이전트의 Task 도구 호출에 대한 tool_result로 전달된다:

┌─────────────────────────────────────────────────────────────────────┐
│                           메인 메시지 루프                              │
│                                                                     │
│  User ──► Agent ──► LLM(Opus)                                       │
│                │                                                    │
│                ▼ tool_use: Task                                     │
│     ┌─────────────────────────────────────────────────────┐         │
│     │           Subagent 메시지 루프 (Explorer)              │         │
│     │                                                     │         │
│     │   prompt ──► Agent ──► LLM(Haiku)                   │         │
│     │                   │                                 │         │
│     │                   ▼ tool_use                        │         │
│     │              ┌─────────┐                            │         │
│     │              │  Glob   │                            │         │
│     │              │  Bash   │                            │         │
│     │              │  Read   │                            │         │
│     │              └─────────┘                            │         │
│     │                   │                                 │         │
│     │                   ▼                                 │         │
│     │              최종 응답                                │         │
│     └────────────────────┬────────────────────────────────┘         │
│                          │                                          │
│                          ▼ tool_result                              │
│                 Agent 계속 진행                                       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

메인 에이전트는 Explorer의 조사 결과를 바탕으로 다음 단계(예: database-schema-architect 호출)를 진행한다.

마무리

지금까지 subagent의 동작 방식에 대해서 알아보았다. 핵심을 정리하면:

Subagent는 Tool Use다. 메인 에이전트가 Task 도구를 호출하면, 새로운 메시지 루프가 생성되어 전용 시스템 프롬프트와 제한된 도구로 작업을 수행한다.

왜 사용하는가?

  • 컨텍스트 분리: 탐색/분석 과정이 메인 대화를 오염시키지 않는다
  • 전문화: 각 subagent가 특정 작업에 최적화된 프롬프트와 도구를 가진다
  • 효율성: 목적에 맞게 가벼운 모델(Haiku)이나 무거운 모델(Opus)을 취사선택

주의할 점:

  • Subagent는 메인 대화 히스토리를 모른다. 필요한 정보는 Task의 prompt에 명시해야 한다 (단, CLAUDE.md는 자동 전달됨)
  • Subagent는 subagent를 호출할 수 없다 (무한 중첩 방지를 위해 Task 도구를 사용하지 않는다.)
  • 각 호출은 새로운 컨텍스트에서 시작한다 (단, resume 파라미터로 이전 대화 이어가기 가능)

결국 subagent는 Tool Use 패턴의 확장이다. 단순한 Function Call이라면 코드 실행 결과를 텍스트로 반환하지만 subagent는 별도의 메시지 루프에서 LLM이 생성한 텍스트를 반환한다는 차이만 있다. 메인 에이전트 입장에서는 둘 다 tool_result로 받는 텍스트일 뿐이다.

Read more →
3

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #5-1 TLS

자손킴 @jasonkim@hackers.pub

L7 애플리케이션

전송 계층은 전송 제어를 하고 애플리케이션별로 패킷을 분류하는 것 까지만 담당한다. 애플리케이션 계층은 패킷을 애플리케이션으로 처리하고 애플리케이션과 사용자를 연결하는 계층이다.

OSI 7계층의 L5, L6을 다루지 않는 이유

OSI 7계층 모델에서는 세션 계층(L5)과 프레젠테이션 계층(L6)이 별도로 정의되어 있다. 세션 계층은 애플리케이션 간의 세션(연결) 설정, 관리, 종료를 담당하고, 프레젠테이션 계층은 데이터의 형식 변환, 암호화, 압축을 담당한다.

하지만 현대 인터넷의 근간인 TCP/IP 모델에서는 이 두 계층을 애플리케이션 계층과 분리하지 않는다. TCP/IP 모델은 OSI의 L5~L7을 하나의 애플리케이션 계층으로 통합하며 세션 관리나 데이터 표현 방식은 각 애플리케이션 프로토콜이 자체적으로 처리한다. 예를 들어 TCP는 이미 전송 계층에서 연결의 설정과 해제(3-way handshake, 4-way handshake)를 관리하고 TLS는 애플리케이션 프로토콜 수준에서 암호화와 데이터 무결성을 처리한다.

실제로 RFC 3439에는 "Layering considered harmful"이라는 섹션이 있을 정도로 엄격한 계층 분리보다는 실용적인 프로토콜 설계가 중시된다. 이러한 이유로 이 책에서도 L5, L6을 별도로 다루지 않고 애플리케이션 프로토콜로 통합하여 설명한다.

다양한 프로토콜

이 책에서는 HTTP, SSL/TLS, DNS, DHCP에 대해서 다룬다.

  • HTTP(Hypertext Transfer Protocol): 웹 브라우저와 웹 서버 간의 통신을 위한 프로토콜이다. 요청-응답 방식으로 동작하며, 웹 페이지, 이미지, API 데이터 등 다양한 리소스를 전송한다.

  • SSL/TLS(Secure Sockets Layer/Transport Layer Security): 네트워크 통신을 암호화하여 보안을 제공하는 프로토콜이다. HTTPS는 HTTP에 TLS를 결합한 것으로 웹에서 가장 널리 사용되는 보안 통신 방식이다.

  • DNS(Domain Name System): 도메인 이름(예: www.example.com)을 IP 주소로 변환하는 시스템이다. 사용자가 기억하기 쉬운 도메인 이름을 사용하여 웹사이트에 접속할 수 있게 해준다.

  • DHCP(Dynamic Host Configuration Protocol): 네트워크에 연결된 장치에게 IP 주소, 서브넷 마스크, 기본 게이트웨이, DNS 서버 등의 네트워크 설정을 자동으로 할당하는 프로토콜이다.

HTTP는 따로 정리하지 않을 것이고, TLS, DNS, DHCP에 대해서만 정리 할 것이다.

TLS

TLS(SSL)은 애플리케이션을 암호화하는 프로토콜이다.

SSL에서 TLS로의 전환

SSL은 1995년 Netscape가 웹 통신 보안을 위해 개발한 프로토콜이다. SSL 2.0이 최초로 공개되었지만 심각한 보안 취약점이 발견되어 1996년 SSL 3.0으로 대체되었다. 이후 IETF(Internet Engineering Task Force)가 SSL을 표준화하는 과정에서 프로토콜 이름이 TLS(Transport Layer Security)로 변경되었다. 1999년 TLS 1.0이 RFC 2246으로 발표되었는데 이는 SSL 3.0을 기반으로 하되 상호 운용성이 없을 정도로 충분한 차이가 있었다.

SSL 3.0은 2014년 POODLE(Padding Oracle On Downgraded Legacy Encryption) 공격 취약점이 발견된 후 2015년 공식적으로 폐기되었다. TLS 1.0과 1.1도 2020년 주요 브라우저들에 의해 지원이 중단되었고 2021년 RFC 8996을 통해 공식 폐기되었다.

현재는 TLS 1.2(2008년 출시)와 TLS 1.3(2018년 출시)이 사용되며 TLS 1.3이 권장된다.

책에서는 TLS 1.2와 RSA를 기반으로 설명하고 있지만 이 포스팅에서는 TLS 1.3과 Ed25519, X25519를 기반으로 정리 할 것이다.

TLS로 막을 수 있는 위협

TLS는 스푸핑, 변조, 도청이라는 세 가지 주요 보안 위협을 방지한다.

암호화로 도청 방지

도청은 통신 당사자가 아닌 제3자가 네트워크를 흐르는 데이터를 몰래 가로채 읽는 행위이다. 공공 와이파이에서 로그인 정보를 훔치거나 네트워크 패킷을 캡처하여 민감한 정보를 탈취하는 것이 대표적인 예다.

암호화는 정해진 규칙(암호화 알고리즘)에 따라 데이터를 변환하는 기술이다. TLS는 대칭키 암호화를 사용하여 통신 내용을 암호문으로 변환한다. 도청자가 암호화된 패킷을 가로채더라도 복호화 키 없이는 원본 데이터를 알 수 없다.

해싱으로 변조 방지

변조(Tampering)는 통신 중인 데이터를 제3자가 중간에서 가로채어 내용을 바꾸는 행위이다. 예를 들어 은행 송금 요청에서 수신자 계좌번호나 금액을 변경하는 중간자 공격(Man-in-the-Middle Attack)이 있다.

해싱은 불규칙한 길이의 데이터에서 정해진 계산(해싱 알고리즘)에 따라 고정된 길이의 데이터(해시값)를 생성하는 기술이다. TLS는 메시지 인증 코드(MAC)를 사용하여 각 메시지에 해시 기반 태그를 붙인다. 수신자는 받은 데이터로 동일한 해시를 계산하고 송신자가 보낸 MAC 태그와 비교한다. 만약 데이터가 조금이라도 변경되었다면 해시값이 완전히 달라지므로 변조를 즉시 탐지할 수 있다.

디지털 인증서로 스푸핑 방지

스푸핑(Spoofing)은 공격자가 다른 서버나 사용자로 위장하여 통신 상대방을 속이는 행위이다. 가짜 은행 웹사이트를 만들어 사용자의 로그인 정보를 탈취하는 피싱 공격이 대표적이다.

디지털 인증서는 인터넷에 있는 다른 단말에 "나는 진짜입니다!"라고 증명하는 파일이다. TLS는 신뢰할 수 있는 인증 기관(CA, Certificate Authority)이 발급한 디지털 인증서를 사용하여 서버의 신원을 검증한다. 클라이언트는 서버가 제시한 인증서가 신뢰할 수 있는 CA에 의해 서명되었는지 인증서의 도메인이 접속하려는 도메인과 일치하는지 확인한다. 이 검증을 통해 가짜 서버에 연결되는 것을 방지한다.

TLS를 지탱하는 기술

TLS는 암호화 알고리즘, 키 교환 알고리즘, 디지컬 서명 알고리즘, 메시지 인증 알고리즘 4가지 기술을 조합하여 사용한다.

암호화 알고리즘

암호화는 평문(원본 데이터)을 암호문(읽을 수 없는 형태)으로 변환하는 과정이며 복호화는 암호문을 다시 평문으로 되돌리는 과정이다. TLS에서 실제 데이터 암호화에는 대칭키(공통키) 암호화 방식을 사용한다.

대칭키 암호화는 암호화와 복호화에 동일한 키를 사용하는 방식이다. AES-GCM이나 ChaCha20-Poly1305 같은 알고리즘이 대표적이며 처리 속도가 빨라 대용량 데이터 암호화에 적합하다.

하지만 대칭키 암호화에는 근본적인 문제가 있다. 통신을 시작하기 전에 양측이 동일한 키를 가지고 있어야 하는데 이 키를 어떻게 안전하게 전달할 것인가? 키를 평문으로 네트워크에 전송하면 도청자에게 탈취당할 수 있다. 키가 탈취되면 해당 키로 암호화된 모든 통신 내용이 노출된다. 이것이 바로 '키 전달 문제'이며 이를 해결하기 위해 키 교환 알고리즘이 필요하다.

키 교환 알고리즘

공통키 암호 방식을 사용하면 키 전달시 보안 문제를 피할 수 없다. 키 교환 알고리즘은 도청자가 지켜보는 공개 채널을 통해서도 양측이 안전하게 공유 비밀(Shared Secret)을 생성할 수 있게 해주는 기술이다.

RSA의 문제점

이전에는 RSA 키 교환을 사용했다. 클라이언트가 무작위 비밀값을 생성하고 서버의 RSA 공개키로 암호화하여 전송하면 서버가 자신의 개인키로 복호화하는 방식이다. 하지만 이 방식에는 심각한 문제가 있다.

만약 공격자가 암호화된 통신을 모두 저장해두었다가, 나중에 서버의 RSA 개인키가 유출되면 과거의 모든 통신을 복호화할 수 있다. 이를 '전방 비밀성(Forward Secrecy)'이 없다고 한다.

X25519로의 전환

TLS 1.3에서는 RSA 키 교환이 완전히 제거되고, X25519(또는 ECDHE) 같은 임시(Ephemeral) Diffie-Hellman 키 교환만 사용한다. X25519는 Curve25519 타원 곡선을 기반으로 한 ECDH(Elliptic Curve Diffie-Hellman) 키 교환 함수로 Daniel J. Bernstein이 2006년에 설계했다.

X25519의 장점은 다음과 같다:

  • 완전 순방향 비밀성(Perfect Forward Secrecy): 매 세션마다 새로운 임시 키 쌍을 생성하므로 서버의 인증서 개인키가 유출되어도 과거 세션의 통신 내용을 복호화할 수 없다.
  • 높은 성능: 256비트 키로 128비트 보안 수준을 제공하면서도 기존 알고리즘보다 훨씬 빠르다.
  • 구현 안전성: 타이밍 공격 등 부채널 공격에 강하도록 설계되었다.

X25519 키 교환 동작 원리

  1. 키 쌍 생성: 영희와 철수는 각각 32바이트의 무작위 개인키(a, b)를 생성한다.
  2. 공개키 계산: 각자 자신의 개인키와 타원 곡선의 기준점(G)을 곱하여 공개키를 계산한다. 영희의 공개키 = a × G, 철수의 공개키 = b × G
  3. 공개키 교환: 영희와 철수는 자신의 공개키를 상대방에게 전송한다. 이 공개키는 도청자가 볼 수 있어도 안전하다.
  4. 공유 비밀 계산: 영희는 자신의 개인키(a)와 철수의 공개키(b × G)를 곱하여 공유 비밀을 계산한다. 철수는 자신의 개인키(b)와 영희의 공개키(a × G)를 곱한다. 타원 곡선의 수학적 특성에 의해 a × (b × G) = b × (a × G)가 성립하므로, 양측은 동일한 공유 비밀을 얻는다.
  5. 세션 키 유도: 공유 비밀은 HKDF(HMAC-based Key Derivation Function)를 통해 실제 암호화에 사용할 세션 키로 변환된다.

도청자는 공개키(a × G, b × G)만 볼 수 있는데 여기서 개인키(a, b)를 알아내는 것은 타원 곡선 이산 로그 문제(ECDLP)를 푸는 것으로 현재 기술로는 계산적으로 불가능하다.

디지털 서명 알고리즘

앞서 언급했듯이 TLS는 디지털 인증서에 포함된 디지털 서명을 통해 상대방이 제3자가 신뢰 할 수있는 상대인지 여부를 판단한다.

RSA에서 Ed25519로

예전에는 RSA가 디지털 서명에 널리 사용되었지만 몇 가지 한계가 있다. 동등한 보안 수준을 위해 훨씬 큰 키 크기가 필요하고(RSA 3072비트 ≈ Ed25519 256비트) 서명 생성 속도가 상대적으로 느리며 구현 시 패딩 오라클 공격 등에 취약할 수 있다.

Ed25519는 이러한 문제를 해결한 현대적인 디지털 서명 알고리즘이다. Edwards 곡선 기반의 EdDSA(Edwards-curve Digital Signature Algorithm) 구현체로, Daniel J. Bernstein 팀이 설계했다. 2023년 FIPS 186-5에 공식 포함되어 미국 연방 정부 시스템에서도 승인된 서명 알고리즘이 되었다.

Ed25519의 특징

  • 작은 키와 서명 크기: 공개키 32바이트, 서명 64바이트로 매우 컴팩트하다.
  • 빠른 성능: 서명 생성이 RSA보다 약 33배 빠르다.
  • 높은 보안성: 128비트 보안 수준을 제공하며, 부채널 공격에 강하도록 설계되었다.
  • 결정적 서명: 난수 생성기에 의존하지 않아 구현 오류로 인한 개인키 노출 위험이 없다. (Sony PlayStation 3 펌웨어 서명키 유출 사건은 ECDSA의 잘못된 난수 사용으로 발생했다.)

디지털 서명 생성과 검증 과정

서명 생성 (서버/발급자 측):

  1. 서명할 메시지(예: 인증서 내용)를 준비한다.
  2. 개인키와 메시지를 사용하여 해시를 계산한다.
  3. 이 해시와 개인키를 타원 곡선 연산에 사용하여 서명값(R, s)을 생성한다.
  4. 서명을 메시지(인증서)에 첨부한다.

서명 검증 (클라이언트 측):

  1. 서버로부터 인증서와 서명을 받는다.
  2. 인증서에 포함된 공개키를 추출한다.
  3. 공개키, 메시지, 서명을 사용하여 타원 곡선 방정식을 검증한다.
  4. 방정식이 성립하면 서명이 유효하고, 인증서가 해당 개인키 소유자에 의해 서명되었음이 증명된다.

통신 상대방 인증:

  1. 클라이언트가 서버에 연결하면 서버는 자신의 디지털 인증서를 제시한다.
  2. 인증서에는 서버의 공개키와 CA(인증 기관)의 디지털 서명이 포함되어 있다.
  3. 클라이언트는 이미 신뢰하고 있는 CA의 공개키로 인증서의 서명을 검증한다.
  4. 검증이 성공하면 인증서에 있는 서버 공개키가 진짜 해당 서버의 것임이 보장된다.

메시지 인증 알고리즘

TLS에서 앞서 언급한 디지털 서명 알고리즘은 통신 상대방을 인증하는 것일 뿐 이후 주고받는 애플리케이션 데이터(메시지)를 인증하는 것은 아니다.

디지털 서명은 비대칭키 암호화를 사용하므로 연산 비용이 높다. 매 메시지마다 서명을 생성하고 검증하는 것은 성능상 비효율적이다. 따라서 TLS는 핸드셰이크 과정에서 합의한 대칭키를 사용하는 MAC(Message Authentication Code)으로 각 메시지의 무결성과 인증을 보장한다.

MAC이란?

MAC은 메시지와 공유 비밀키를 입력으로 받아 고정 길이의 인증 태그를 생성하는 알고리즘이다. 단순한 해시와 달리, 비밀키가 없으면 올바른 MAC 태그를 생성할 수 없다. 따라서 MAC은 메시지 무결성(변조 여부)과 메시지 인증(발신자 확인)을 동시에 제공한다.

TLS에서는 주로 HMAC(Hash-based MAC)을 사용한다. HMAC은 SHA-256 같은 해시 함수와 비밀키를 결합하여 MAC 태그를 생성한다. TLS 1.3에서는 AEAD(Authenticated Encryption with Associated Data) 모드인 AES-GCM이나 ChaCha20-Poly1305를 사용하는데, 이들은 암호화와 메시지 인증을 동시에 수행한다.

MAC으로 변조 검증하는 과정

  1. 송신자: 암호화된 메시지와 공유 비밀키를 MAC 알고리즘에 입력하여 MAC 태그를 생성한다.
  2. 전송: 암호화된 메시지와 MAC 태그를 함께 전송한다.
  3. 수신자: 받은 메시지와 동일한 공유 비밀키로 MAC을 직접 계산한다.
  4. 비교: 계산한 MAC과 받은 MAC 태그를 비교한다.
  5. 판정: 두 값이 일치하면 메시지가 변조되지 않았음이 보장된다. 일치하지 않으면 메시지가 전송 중에 변조되었거나 올바른 키를 가진 발신자가 보낸 것이 아니므로 메시지를 폐기한다.

이 과정에서 공격자가 메시지 내용을 조금이라도 바꾸면 해시값이 완전히 달라지므로 올바른 MAC 태그를 생성할 수 없다. 또한 공유 비밀키 없이는 위조된 메시지에 대한 유효한 MAC을 만들 수 없어 메시지의 출처도 검증된다.

Read more →
6

Claude Code의 거의 모든 것은 Tool Use 입니다. MCP도 subagent도 Skills 역시요.

자손킴 @jasonkim@hackers.pub

이번 글에서는 지난글에 이어서 Claude가 도구를 사용하는 구체적인 방법을 알아본다. Claude가 사용할 수 있는 도구들의 목록은 Tools 섹션에 포함되어 있다. Tools 섹션에 대해서는 이전 글을 참고한다.

Tool Use 란?

Tool Use는 Claude가 외부 도구(함수)를 호출하여 실제 작업을 수행할 수 있게 하는 메커니즘이다. Claude는 텍스트 생성만으로는 수행할 수 없는 작업들, 예를 들어 파일 읽기, 명령어 실행, 웹 검색 등을 도구를 통해 수행한다.

Claude에게 사용 가능한 도구들의 스키마를 알려주면 Claude는 사용자의 요청을 분석하여 적절한 도구를 선택하고 필요한 파라미터와 함께 도구 사용을 요청한다. 에이전트(클라이언트)는 이 요청을 받아 실제로 도구를 실행하고 그 결과를 다시 Claude에게 전달한다.

Tools 섹션: 도구 정의하기

Claude가 도구를 사용하려면 먼저 어떤 도구가 있는지 알아야 한다. 에이전트는 API 요청의 tools 배열에 사용 가능한 도구들을 정의한다. 각 도구는 이름, 설명, 그리고 입력 스키마를 포함한다.

Bash 도구 정의 예시

{
  "name": "Bash",
  "description": "Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nIMPORTANT: This tool is for terminal operations like git, npm, docker, etc...",
  "input_schema": {
    "type": "object",
    "properties": {
      "command": {
        "type": "string",
        "description": "The command to execute"
      },
      "timeout": {
        "type": "number",
        "description": "Optional timeout in milliseconds (max 600000)"
      },
      "description": {
        "type": "string",
        "description": "Clear, concise description of what this command does in 5-10 words, in active voice."
      }
    },
    "required": ["command"],
    "additionalProperties": false,
    "$schema": "http://json-schema.org/draft-07/schema#"
  }
}

Glob 도구 정의 예시

{
  "name": "Glob",
  "description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns",
  "input_schema": {
    "type": "object",
    "properties": {
      "pattern": {
        "type": "string",
        "description": "The glob pattern to match files against"
      },
      "path": {
        "type": "string",
        "description": "The directory to search in. If not specified, the current working directory will be used."
      }
    },
    "required": ["pattern"],
    "additionalProperties": false,
    "$schema": "http://json-schema.org/draft-07/schema#"
  }
}

도구 정의에서 description이 중요하다. Claude는 이 설명을 읽고 어떤 상황에서 해당 도구를 사용해야 하는지 판단한다. input_schema는 JSON Schema 형식으로 Claude가 도구를 호출할 때 어떤 파라미터를 어떤 형식으로 전달해야 하는지 정의한다.

Claude가 도구를 선정하는 방법

Claude가 도구를 선택하는 과정은 Messages API의 대화 흐름 속에서 이루어진다. 실제 예시를 통해 살펴보자.

사용자의 요청

사용자가 "이 NestJS 프로젝트에서 entity 구조를 탐색해주세요"라고 요청하면 에이전트는 다음과 같은 메시지를 API에 전송한다:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요..."
    }
  ]
}

Claude의 도구 사용 요청

Claude는 사용자의 요청을 분석하고 작업 수행에 필요한 도구들을 선택하여 tool_use 블록으로 응답한다:

{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "이 NestJS 프로젝트의 entity 구조를 철저하게 탐색하겠습니다."
    },
    {
      "type": "tool_use",
      "id": "toolu_01ABC123XYZ",
      "name": "Glob",
      "input": {
        "pattern": "**/*.entity.ts"
      }
    },
    {
      "type": "tool_use",
      "id": "toolu_01DEF456UVW",
      "name": "Bash",
      "input": {
        "command": "find /workspace/my-nestjs-project/src -type f -name \"*.ts\" | grep -E \"(entity|entities)\" | head -20",
        "description": "Find entity files in src directory"
      }
    }
  ]
}

여기서 주목할 점이 있다. Claude는 한 번의 응답에서 여러 도구를 동시에 요청할 수 있다. 위 예시에서는 GlobBash 두 도구를 병렬로 요청했다. 각 도구 요청에는 고유한 id가 부여되어 나중에 결과를 매핑할 때 사용된다.

응답의 stop_reason

Claude가 도구 사용을 요청하면 API 응답의 stop_reason"tool_use"로 설정된다:

{
  "id": "msg_01XYZ789ABC",
  "type": "message",
  "role": "assistant",
  "model": "claude-haiku-4-5-20251001",
  "content": [...],
  "stop_reason": "tool_use",
  "usage": {
    "input_tokens": 714,
    "output_tokens": 314
  }
}

stop_reason은 에이전트에게 "응답이 끝난 것이 아니라 도구 실행이 필요하다"는 신호를 보낸다.

에이전트는 tool_use 요청을 받으면 무엇을 하는가?

에이전트(클라이언트)가 stop_reason: "tool_use" 응답을 받으면 다음 단계를 수행해야 한다:

  1. 도구 요청 파싱: 응답의 content 배열에서 type: "tool_use" 블록들을 추출한다.

  2. 도구 실행: 각 도구 요청에 대해 실제 도구를 실행한다. 예를 들어:

    • Bash 도구 → 시스템에서 실제 bash 명령어 실행
    • Glob 도구 → 파일 시스템에서 패턴 매칭 수행
    • Read 도구 → 파일 내용 읽기
  3. 결과 수집: 각 도구의 실행 결과를 수집하고 tool_use_id와 함께 결과를 구성한다.

  4. 모델에 결과 전달: 수집한 결과를 tool_result 형식으로 모델에 다시 전송한다.

이 과정에서 에이전트는 도구 실행의 성공/실패 여부, 타임아웃 처리, 보안 검증 등을 담당한다. Claude는 도구의 스키마와 용도만 알 뿐 실제 실행은 에이전트의 몫이다.

에이전트가 모델에 도구 실행 결과를 알리는 방법

에이전트가 도구를 실행한 후에는 그 결과를 tool_result 형식으로 모델에 전달한다. 이 결과는 user role의 메시지로 전송된다.

tool_result 구조

{
  "role": "user",
  "content": [
    {
      "tool_use_id": "toolu_01DEF456UVW",
      "type": "tool_result",
      "content": "/workspace/my-nestjs-project/src/modules/chat/entities/dm-unlock.entity.ts\n/workspace/my-nestjs-project/src/modules/agora/entities/call-session.entity.ts\n/workspace/my-nestjs-project/src/modules/user/entities/user.entity.ts\n/workspace/my-nestjs-project/src/modules/user/entities/user-profile.entity.ts\n/workspace/my-nestjs-project/src/modules/item/entities/item.entity.ts\n...",
      "is_error": false
    },
    {
      "tool_use_id": "toolu_01ABC123XYZ",
      "type": "tool_result",
      "content": "/workspace/my-nestjs-project/src/modules/agora/entities/agora-event-log.entity.ts\n/workspace/my-nestjs-project/src/modules/agora/entities/call-participant.entity.ts\n/workspace/my-nestjs-project/src/modules/item/entities/item.entity.ts\n...",
      "cache_control": {
        "type": "ephemeral"
      }
    }
  ]
}

tool_result의 핵심 필드는 다음과 같다:

필드 설명
tool_use_id Claude가 요청한 도구의 고유 ID. 어떤 요청에 대한 결과인지 매핑
type 항상 "tool_result"
content 도구 실행의 실제 결과 (문자열)
is_error 도구 실행 실패 시 true
cache_control (선택) 프롬프트 캐싱을 위한 제어 옵션

전체 대화 흐름

tool_result를 받은 Claude는 결과를 분석하고 추가 도구가 필요하면 다시 tool_use를 요청한다. 충분한 정보가 모이면 최종 응답을 생성한다. 이 과정이 반복되면서 복잡한 작업도 단계별로 수행할 수 있다:

User → Claude: "entity 구조를 탐색해주세요"
Claude → Agent: tool_use (Glob, Bash)
Agent → Claude: tool_result (파일 목록)
Claude → Agent: tool_use (Read - 여러 파일)
Agent → Claude: tool_result (파일 내용들)
Claude → User: 최종 분석 결과

실제 예시에서 Claude는 먼저 GlobBash로 entity 파일 목록을 찾고 그 결과를 받은 후 Read 도구로 개별 파일들을 읽어 분석했다:

{
  "type": "text",
  "text": "좋습니다. 이제 주요 entity 파일들을 읽겠습니다."
},
{
  "type": "tool_use",
  "id": "toolu_01GHI789RST",
  "name": "Read",
  "input": {
    "file_path": "/workspace/my-nestjs-project/src/modules/user/entities/user.entity.ts"
  }
},
{
  "type": "tool_use",
  "id": "toolu_01JKL012MNO",
  "name": "Read",
  "input": {
    "file_path": "/workspace/my-nestjs-project/src/modules/user/entities/user-profile.entity.ts"
  }
}

마무리

Claude Code와 같은 에이전트는 모델에 사용할 수 있는 도구를 알려주어 도구를 능동적으로 사용하게 만듦으로써 유저의 실행환경과 상호 협력하여 도구를 실행한다. 유저에게 질문을 하는 AskUserQuestion도 도구이고 심지어 계획 모드를 빠져나가는 ExitPlanMode도 도구다.

MCP(Model Context Protocol) 서버가 제공하는 기능들도 결국 도구로 노출되며 Subagent 호출도 도구를 통해 이루어진다. Skills도 마찬가지다. 결국 Claude Code의 거의 모든 확장 기능은 Tool Use라는 하나의 메커니즘 위에서 동작한다.

이 구조를 이해하면 Claude Code가 어떻게 파일을 읽고, 코드를 실행하고, 웹을 검색하는지 명확해진다. 그리고 새로운 도구를 추가하거나 MCP 서버를 연동할 때도 같은 패턴이 적용된다는 것을 알 수 있다.

Read more →
8

Claude API의 Request Body 분석

자손킴 @jasonkim@hackers.pub

Claude API의 Request는 크게 4가지 분류를 가지고 있다.

  • System Messages
  • Messages
  • Tools
  • Model & Config

각각은 다음과 같은 역할을 한다.

System Messages

System Messages는 Claude에게 역할, 성격, 제약사항 등을 지시하는 최상위 설정이다. 배열 형태로 여러 개의 시스템 메시지를 전달할 수 있다.

"system": [
  {
    "type": "text",
    "text": "You are Claude Code, Anthropic's official CLI for Claude.",
    "cache_control": {
      "type": "ephemeral"
    }
  },
  {
    "type": "text",
    "text": "You are an interactive CLI tool that helps users with software engineering tasks...",
    "cache_control": {
      "type": "ephemeral"
    }
  }
]

System Messages에는 다음과 같은 내용이 포함된다:

  • Claude의 페르소나 및 역할 정의
  • 보안 및 윤리 가이드라인
  • 응답 형식 및 톤 설정
  • 프로젝트 정보 등 컨텍스트
  • cache_control을 통한 캐싱 설정

Messages

Messages는 userassistant 역할이 번갈아가며 주고받은 대화를 누적하는 배열이다. assistant 메시지는 반드시 모델의 실제 응답일 필요가 없다. 이를 활요해 API 호출 시 assistant 메시지를 미리 작성해서 전달하면, Claude는 그 내용 이후부터 이어서 응답한다. 이를 Prefill 기법이라 한다.

이 대화 기록을 통해 Claude는 맥락을 유지하며 응답한다.

"messages": [
  {
    "role": "user",
    "content": [...]
  },
  {
    "role": "assistant",
    "content": [...]
  },
  {
    "role": "user",
    "content": [...]
  }
]

User Message

User의 content는 주로 두 가지 type으로 구성된다:

1. text - 사용자의 일반 메시지나 시스템 리마인더

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "선물을 주고받는 기능을 위한 entity를 설계하라."
    }
  ]
}

2. tool_result - Tool 실행 결과 반환

{
  "role": "user",
  "content": [
    {
      "tool_use_id": "toolu_01Qj7gnFLKWBNjg",
      "type": "tool_result",
      "content": [
        {
          "type": "text",
          "text": "## Entity 구조 탐색 보고서\n\n철저한 탐색을 통해..."
        }
      ]
    }
  ]
}

Assistant Message

Assistant의 content는 주로 세 가지 type으로 구성된다:

1. text - Claude의 응답 메시지

{
  "type": "text",
  "text": "선물 주고받기 기능을 위한 entity 설계를 시작하겠습니다."
}

2. thinking - Extended Thinking 기능 활성화 시 사고 과정 (signature로 검증)

{
  "type": "thinking",
  "thinking": "사용자가 선물을 주고받는 기능을 위한 entity 설계를 요청했습니다...",
  "signature": "EqskYIChgCKknyFYp5cu1zhVOp7kFTJb..."
}

3. tool_use - Tool 호출 요청

{
  "type": "tool_use",
  "id": "toolu_01Qj7gn6vLKCNjg",
  "name": "Task",
  "input": {
    "subagent_type": "Explore",
    "prompt": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요...",
    "description": "Entity 구조 탐색"
  }
}

User와 Assistant의 협력

Tool 사용 흐름은 다음과 같이 진행된다:

  1. Assistant: tool_use로 Tool 호출 요청
  2. User: tool_result로 실행 결과 반환
  3. Assistant: 결과를 바탕으로 text 응답 또는 추가 tool_use

이 과정에서 어떤 Tool을 사용할 수 있는지는 tools 배열이 정의한다.

Tools

Tools는 Claude가 사용할 수 있는 도구들을 정의하는 배열이다. 각 Tool은 name, description, input_schema 세 가지 필드로 구성된다.

Tool의 기본 구조

"tools": [
  {
    "name": "ToolName",
    "description": "Tool에 대한 설명...",
    "input_schema": {
      "type": "object",
      "properties": {...},
      "required": [...],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    }
  }
]
필드 설명
name Tool의 고유 식별자. Claude가 tool_use에서 이 이름으로 호출
description Tool의 용도, 사용법, 주의사항 등을 상세히 기술. Claude가 어떤 Tool을 선택할지 판단하는 근거
input_schema JSON Schema 형식으로 입력 파라미터 정의

input_schema 구조

input_schema는 JSON Schema draft-07 스펙을 따르며, Tool 호출 시 필요한 파라미터를 정의한다.

"input_schema": {
  "type": "object",
  "properties": {
    "pattern": {
      "type": "string",
      "description": "The regular expression pattern to search for"
    },
    "path": {
      "type": "string",
      "description": "File or directory to search in. Defaults to current working directory."
    },
    "output_mode": {
      "type": "string",
      "enum": ["content", "files_with_matches", "count"],
      "description": "Output mode: 'content' shows matching lines, 'files_with_matches' shows file paths..."
    },
    "-i": {
      "type": "boolean",
      "description": "Case insensitive search"
    },
    "head_limit": {
      "type": "number",
      "description": "Limit output to first N lines/entries"
    }
  },
  "required": ["pattern"],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

properties 내 각 파라미터 정의

각 파라미터는 다음 필드들로 정의된다:

필드 설명
type 데이터 타입 (string, number, boolean, array, object 등)
description 파라미터의 용도와 사용법 설명
enum (선택) 허용되는 값의 목록. 이 중 하나만 선택 가능
default (선택) 기본값

input_schema의 메타 필드

필드 설명
type 항상 "object"
properties 파라미터 정의 객체
required 필수 파라미터 이름 배열. 여기 포함되지 않은 파라미터는 선택적
additionalProperties false면 정의되지 않은 파라미터 전달 불가
$schema JSON Schema 버전 명시

실제 예시: Grep Tool

{
  "name": "Grep",
  "description": "A powerful search tool built on ripgrep\n\n  Usage:\n  - ALWAYS use Grep for search tasks...",
  "input_schema": {
    "type": "object",
    "properties": {
      "pattern": {
        "type": "string",
        "description": "The regular expression pattern to search for in file contents"
      },
      "path": {
        "type": "string",
        "description": "File or directory to search in (rg PATH). Defaults to current working directory."
      },
      "glob": {
        "type": "string",
        "description": "Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\")"
      },
      "output_mode": {
        "type": "string",
        "enum": ["content", "files_with_matches", "count"],
        "description": "Output mode. Defaults to 'files_with_matches'."
      },
      "-A": {
        "type": "number",
        "description": "Number of lines to show after each match"
      },
      "-B": {
        "type": "number",
        "description": "Number of lines to show before each match"
      },
      "-i": {
        "type": "boolean",
        "description": "Case insensitive search"
      },
      "multiline": {
        "type": "boolean",
        "description": "Enable multiline mode. Default: false."
      }
    },
    "required": ["pattern"],
    "additionalProperties": false,
    "$schema": "http://json-schema.org/draft-07/schema#"
  }
}

이 Tool을 Claude가 호출할 때의 tool_use:

{
  "type": "tool_use",
  "id": "toolu_01ABC123",
  "name": "Grep",
  "input": {
    "pattern": "class.*Entity",
    "path": "src/modules",
    "glob": "*.ts",
    "output_mode": "content",
    "-i": true
  }
}

requiredpattern만 있으므로 나머지는 선택적이다. Claude는 input_schemadescription을 참고하여 적절한 파라미터를 선택한다.

Model & Config

마지막으로 모델 선택과 각종 설정 옵션들이다:

{
  "model": "claude-opus-4-5-20251101",
  "max_tokens": 32000,
  "thinking": {
    "budget_tokens": 31999,
    "type": "enabled"
  },
  "stream": true,
  "metadata": {
    "user_id": "user_2f2ce5dbb94ac27c8da0d0b28dddf815fc82be54e0..."
  }
}
옵션 설명
model 사용할 Claude 모델 (claude-opus-4-5, claude-sonnet-4-5 등)
max_tokens 최대 출력 토큰 수
thinking Extended Thinking 설정 (budget_tokens로 사고 토큰 예산 설정)
stream 스트리밍 응답 여부
metadata 사용자 ID 등 메타데이터

마치며

지금까지 Claude API Request Body의 4가지 핵심 구성 요소를 살펴보았다:

  1. System Messages: Claude의 역할과 행동 방식을 정의
  2. Messages: user-assistant 간 대화 기록을 누적하며, tool_use/tool_result를 통해 Tool과 상호작용
  3. Tools: JSON Schema 기반으로 사용 가능한 도구의 이름, 설명, 입력 파라미터를 정의
  4. Model & Config: 모델 선택, 토큰 제한, 스트리밍 등 설정

이 구조를 알면 Claude가 주고받은 메시지를 어떻게 관리하는지, 도구를 어떻게 사용하는지 이해하고 API를 더 효과적으로 활용할 수 있다.

Read more →
3

언제까지 (a:number, b:number) => a + b, (a:string, b:string) => a + b, <T>(a: T, b: T) => a + b 를 해줘야 하나고
그냥 대충 눈치껏 (a, b) => a + b 하면 'ba 와 더할 수 있어야 하는 타입이고 a 는 무언가와 더할 수 있는 타입이구나' 하고 추론할 수 있는 분석기가 달린 언어가 필요함

3

Claude Code가 모델이 하지도 않은 말을 했다고 하는 이유.

자손킴 @jasonkim@hackers.pub

Claude Code에서 첫 번째 요청을 입력하면 가장 먼저 다음과 같은 JSON을 API로 보낸다. 이 요청은 실제 작업에 앞서 대화 주제를 파악하고 제목을 생성하기 위한 보조 요청이다.

{
  "model": "claude-haiku-4-5-20251001",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Request Body의 구조를 분석하고 분류별로 묶어서 표현한다. ultrathink"
        }
      ]
    },
    {
      "role": "assistant",
      "content": [
        {
          "type": "text",
          "text": "{"
        }
      ]
    }
  ],
  "system": [
    {
      "type": "text",
      "text": "You are Claude Code, Anthropic's official CLI for Claude."
    },
    {
      "type": "text",
      "text": "Analyze if this message indicates a new conversation topic. If it does, extract a 2-3 word title that captures the new topic. Format your response as a JSON object with two fields: 'isNewTopic' (boolean) and 'title' (string, or null if isNewTopic is false). Only include these fields, no other text. ONLY generate the JSON object, no other text (eg. no markdown)."
    }
  ],
  "tools": [],
  "metadata": {
    "user_id": "user-id"
  },
  "max_tokens": 32000,
  "stream": true
}

시스템 프롬프트를 보면 이 요청이 신규 대화인지 판단하고, 신규 대화라면 2-3 단어의 제목을 추출하여 isNewTopictitle 필드로 구성된 JSON만 반환하라고 지시하고 있다.

여기서 내 눈에 띈 것은 첫 번째 요청임에도 불구하고 마치 멀티턴 대화가 진행된 것처럼 messages의 마지막 roleassistant라는 점이었다. 게다가 Claude가 { 한 글자만 응답한 것처럼 구성되어 있다.

이 요청에 대한 응답은 다음과 같다.

{
  "id": "msg_id",
  "type": "message",
  "role": "assistant",
  "model": "claude-haiku-4-5-20251001",
  "content": [
    {
      "type": "text",
      "text": "\n  \"isNewTopic\": true,\n  \"title\": \"Request Body Formatting\"\n}"
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 187,
    "output_tokens": 26,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 0
  }
}

content.text를 보기좋게 정리해서 적으면 다음과 같다.

  "isNewTopic": true,
  "title": "Request Body Formatting"
}

완전한 JSON에서 맨 앞의 {가 빠진 형태다. 알고 보니 이것은 prefill 기법이라 불리는 것으로, 모델이 응답의 앞부분을 이미 출력한 것처럼 설정하여 이어지는 응답을 원하는 형식으로 유도하는 방법이다.

Claude Code는 이 기법을 활용해 모델이 JSON 형식으로 응답하도록 강제하고 있다. 단순히 "JSON으로 응답해줘"라고 요청하는 것보다 훨씬 확실한 방법이다. 모델 입장에서는 이미 {로 시작했으니 자연스럽게 JSON을 완성할 수밖에 없기 때문이다.

Prefill은 JSON 외에도 다양하게 활용할 수 있다. 예를 들어 ```python으로 시작하면 모델이 파이썬 코드 블록을 완성하게 되고, <analysis>로 시작하면 XML 형식의 응답을 유도할 수 있다.

Read more →
4

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #4

자손킴 @jasonkim@hackers.pub

이 글은 네트워크 계층과 애플리케이션을 연결하는 L4 전송 계층의 핵심 개념을 소개합니다. 포트 번호를 통해 애플리케이션을 식별하고, UDP와 TCP 프로토콜의 특징과 패킷 형식을 설명합니다. UDP는 실시간성을, TCP는 신뢰성을 중시하며, TCP는 3-way handshake로 연결을 설정하고, 흐름 제어, 혼잡 제어, 재전송 제어를 통해 데이터 전송을 관리합니다. 특히 TCP 커넥션의 상태 전이 과정과 4-way handshake를 통한 연결 종료 과정을 상세히 다룹니다. 이 글을 통해 독자는 L4 전송 계층의 작동 방식과 TCP의 신뢰성 있는 데이터 전송 메커니즘에 대한 깊이 있는 이해를 얻을 수 있습니다.

Read more →
3

youknowone replied to the below article:

abab, asc, 123, abc

@disjukr@hackers.pub

흔히 사용되는 JavaScript의 배열 정렬 방식인 `arr.sort((a, b) => a - b)`는 코드를 읽는 개발자에게 혼란을 줄 수 있습니다. 이 글에서는 이러한 정렬 코드를 볼 때마다 오름차순인지 내림차순인지 확인해야 하는 번거로움을 줄이기 위해, 더 명확한 함수 이름을 사용할 것을 제안합니다. `sortAsc(arr)`와 같이 오름차순을 의미하는 함수명도 좋지만, `sort123(arr)` 또는 `sortAbc(arr)`처럼 정렬 방향을 직관적으로 나타내는 이름을 사용하면 코드의 가독성을 더욱 향상시킬 수 있습니다. 이처럼 명확한 이름은 코드를 이해하는 데 필요한 인지적 노력을 줄여주어, 개발 생산성 향상에 기여할 수 있다는 점을 강조합니다.

Read more →
7
3

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #3-2

자손킴 @jasonkim@hackers.pub

이 글은 네트워크 라우팅의 기본 개념과 동작 방식, 그리고 NAT 기술에 대해 설명합니다. 라우팅은 IP 패킷이 목적지에 도달하기 위해 거쳐야 하는 경로를 결정하는 과정으로, 라우팅 테이블을 통해 넥스트 홉을 찾아 패킷을 전달합니다. 라우팅 방식에는 수동으로 설정하는 정적 라우팅과 라우터 간 정보 교환을 통해 자동으로 경로를 설정하는 동적 라우팅이 있습니다. 또한, NAT는 하나의 공인 IP 주소를 여러 장비가 공유할 수 있도록 IP 주소를 변환하는 기술로, 특히 NAPT는 IP 주소와 포트 번호를 함께 변환하여 효율적인 네트워크 사용을 가능하게 합니다. 이 글을 통해 독자는 네트워크 라우팅과 NAT의 핵심 원리를 이해하고, 실제 네트워크 환경에서 이들이 어떻게 활용되는지 파악할 수 있습니다.

Read more →
0

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #3-1

자손킴 @jasonkim@hackers.pub

이 글은 네트워크 계층(L3) 프로토콜의 핵심인 IP에 대해 심도 있게 다룬다. L3 라우터의 역할부터 시작하여, IPv4의 구조, IP 단편화 과정, 그리고 PMTUD(Path MTU Discovery)의 중요성을 설명한다. IP 헤더의 각 필드(버전, 헤더 길이, ToS, 패킷 길이 등)에 대한 자세한 분석을 제공하며, IP 주소와 서브넷 마스크를 통해 네트워크와 호스트를 구분하는 방법을 설명한다. 클래스풀 주소 지정 방식과 클래스리스 주소 지정 방식의 차이점을 비교하고, 공인 IP 주소와 사설 IP 주소의 개념을 명확히 한다. 마지막으로, ICMP(Internet Control Message Protocol)를 통해 IP 레벨의 통신 상태를 확인하고 오류를 알리는 방법을 소개한다. 이 글을 통해 독자는 IP 프로토콜의 기본 원리를 이해하고, 네트워크 문제 해결 능력을 향상시킬 수 있을 것이다.

Read more →
4

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #2

자손킴 @jasonkim@hackers.pub

이 글은 동일 네트워크 내 장비들이 L2 스위치를 통해 통신하는 방식에 대해 설명합니다. MAC 주소를 기반으로 통신하는 L2 계층은 통신 시작 시 IP 주소만 알고 MAC 주소를 모르는 상황을 해결하기 위해 ARP를 사용합니다. MAC 주소와 IP 주소의 역할, 이더넷 프레임 구조, 그리고 ARP의 동작 방식과 한계점을 상세히 다룹니다. 또한 L2 스위칭의 핵심인 MAC 주소 테이블과 flooding 과정을 설명하고, MAC 주소 중복 문제와 VLAN 기술을 소개합니다. 마지막으로, OVS 환경에서 VLAN 설정 시 vlan_mode와 tag의 의미를 명확히 구분하여 네트워크 구성에 대한 이해를 돕습니다.

Read more →
4

Ailrun (UTC-5/-4) replied to the below article:

공허한 참

박준규 @curry@hackers.pub

하스켈의 `all` 함수에 빈 리스트를 넣었을 때 왜 `True`가 반환되는지에 대한 의문을 "공허한 참(Vacuous truth)"이라는 개념을 통해 탐구합니다. 흔히 '구현이 그렇게 되어 있으니까'라고 생각할 수 있지만, 저자는 이 현상을 논리적으로 분석합니다. `all` 함수의 구현 방식과, 빈 리스트에 대한 연산 결과가 전체 결과에 미치는 영향을 설명하며, 공집합의 모든 원소가 짝수라는 명제가 참인 이유와 유사한 논리적 근거를 제시합니다. 이를 통해 코드와 수학 간의 연결고리를 발견하고, 마지막으로 ChatGPT가 생성한 유머러스한 이미지를 곁들여 독자에게 즐거움을 선사합니다.

Read more →
5

@curry박준규 all을 다음과 같이 정의하면 문제가 무엇일까요?

all p [] = False
all p [x] = p x
all p (x:xs) = p x && all xs

이 질문에 대한 대답 중 all의 의미에 관한 것이 있을 겁니다. 논리적으로 "모든 ...에 대해"를 어떻게 이해해야 하는냐에 대한 것 말이지요.

공집합을 직접 사용하는 것이 가장 간단한 예시겠지만, 좀 더 논리학에서 자주 사용되는 예시로는 "20세기의 모든 프랑스 왕은 대머리다"가 있겠습니다. 이는 무의미하게 (Vacuously) 참인데요, 왜냐면 19세기를 마지막으로 프랑스에는 더 이상 왕이 없기 때문이지요. 즉, 일반적으로 "모든 ...에 대해"에서 "..." 부분이 (결과적으로) 공집합일 경우 "모든 ..."에 의해 수식된 본문이 어떤 문장인지와는 상관 없이 참이라고 이해한다는 것이지요.

5

자손킴 shared the below article:

파서 콤비네이터: 하스켈 초보자를 위한 파싱

박준규 @curry@hackers.pub

이 글은 하스켈 초보자를 위한 파서 컴비네이터에 대한 입문 튜토리얼입니다. 파싱은 프로그래밍에서 흔히 발생하는 작업이지만, 정규 표현식이나 문자열 조작만으로는 복잡한 형식을 다루기 어렵습니다. 저자는 `Text.ParserCombinators.ReadP` 라이브러리를 사용하여 파서 컴비네이터를 소개하고, 이를 통해 더 읽기 쉽고 유지보수가 용이한 파서를 작성할 수 있음을 보여줍니다. METAR 보고서 파싱 예제를 통해 `satisfy`, `many1`, `<|>`, `option` 등의 기본적인 파서 콤비네이터 함수를 설명하고, 펑터와 모나드의 개념을 활용하여 파서를 구성하는 방법을 안내합니다. 또한, 파싱된 데이터의 유효성을 검사하고, 결과를 더 의미 있는 데이터 타입으로 변환하는 방법을 제시합니다. 이 튜토리얼을 통해 독자는 파서 컴비네이터의 기본 원리를 이해하고, 실제 데이터 파싱 작업에 적용할 수 있는 능력을 얻게 됩니다. 마지막으로, 저자는 독자들에게 배운 내용을 바탕으로 전체 METAR 보고서를 파싱하는 라이브러리를 만들어 Hackage에 제출해 볼 것을 권장하며, 파서가 없는 데이터를 만났을 때 `ReadP`를 자신 있게 사용할 수 있기를 바랍니다.

Read more →
11
4

stride 구현에 앞서 어떻게 동작하는건지 정리해봤다.

  • 모든 프로세스는 각자의 '보폭(stride)'을 가진다.
  • 스케쥴러가 프로세스를 실행시키면 해당 프로세스는 'stride'만큼의 '거리(pass)'를 이동한다.
  • 스케쥴러는 항상 가장 '짧은 거리를 이동한(pass가 가장 작은)' 프로세스를 실행시킨다.
  • 결과적으로 보폭이 작을수록 더 자주 실행된다.

몇가지 고려해야 할 것들이 떠올랐다.

  • stride로 바뀌면 lottery와는 달리 더 자주 실행되어야 할 프로세스에 작은 값을 부여해야한다. 사용자 입장에서 좋은 방식인가?
  • 신규 프로세스 실행 비율에 대한 공정성이 지켜지는지?
  • pass가 누적되면 overflow 될 가능성은 없나?
  • 가장 작은 pass를 찾는 효율적인 방법

그리고 다음과 같이 결정했다.

  • 우선순위를 정하는 API인 setticket은 그대로 유지하자.
  • tickets의 반비례되는 수인 stride를 만들기 위해 max_tickets(교재의 L)를 정한다.
    • stride = L / tickets
  • 신규 프로세스에는 현재 실행중인 프로세스중에서 가장 작은 pass를 부여하여 공정성을 유지한다.
  • pass를 uin64로 지정하고 max_tickets를 2^32로만 잡아도 최대 42.9억번 정도의 tick이 지나야 overflow가 발생한다. 지금은 고려하지 말자.
  • 초기 구현은 선형탐색으로 단순하게 구현하고 이후 최적화를 하자.
1

그동안 Conal이란걸 만들고 있었습니다. Classic FRP 라이브러리입니다.

또, 소개글 쓰려니까 머리 아파서, 클로드랑 즉석 팟캐스트를 열었습니다. 술술 읽혔으면 좋겠네요.

당장 프로덕션에 쓰려면 개선할 부분이 많습니다. 피드백과 기여 환영합니다.

8
8

스터디에 참여하고 계신 조교(?)님 께서 lottery scheduler 구현을 리뷰해주셨다. 혹시나 잘못 이해하고 구현한 부분이 있을까봐 걱정했는데, 리뷰해주신 덕분에 잘못된 부분은 없음을 확인했다. 더불어서 C로 코딩할때 유의해야 할 부분들을 배울 수 있었다.

0

lottery scheduler를 구현했다. 이어서 스케쥴러가 정해진 티켓 비율만큼 프로세스를 실행하는지 테스트하는 프로그램을 작성하기 위해 계획을 세웠다. 자식 프로세스의 pid를 알면 일정 주기마다 getpinfo를 호출하여 프로세스가 실행된 tick을 구해 계산 할 수 있을 것이다. 그런데 부모 프로세스가 자식의 pid를 알려면 어떻게 해야하지? 여기서 갑자기 혼란이 생겼다.

fork의 반환값은 0이면 자식이고 0보다 크면 부모이다. 0보다 작다면 실패이다. 이것을 사용하면 포크를 실행한 부모 프로세스인지 포크로 만들어진 자식 프로세스인지 알 수 있다.

내가 혼란스러웠던 부분은 코드를 작성하는 입장에서 fork의 호출은 한 번이지만 이후 실행은 2개로 갈라져서 반환 값이 2개가 된다는 사실을 정확히 인지하지 못해서였다. 부모인지 자식인지 판단하는 코드를 나눠서 생각하지 못하고 오로지 부모 입장에서만 생각하다보니 0도 부모가 리턴 받는다고 착각을 했던 것이다.

이제 부모가 자식을 더 잘 알 수 있게 되었으니 마저 코딩을 하도록 해야겠다.

0

settickets, getpinfo를 구현하고 ps를 구현했다. pstat을 전역 변수로 두고 관리하려 했으나 proc, pstat을 각각 관리하면 복잡해 진다는 것을 깨달았다. 다른 스터디원분이 먼저 작성하신 코드를 참고하여 proc에 ticks를 추가하고 getpinfo 호출시 pstat을 만드는 방향으로 계획을 바꿨다. 유저 공간에서 시스템콜에 포인터를 넘기고, 그 포인터에 값을 채워받는 것을 구현하는게 난관이었는데 Claude Code가 해결해줘서 넘어갔다.

0

스케쥴러가 어떤 프로세스를 실행중인지 확인해보기 위해 컨텍스트 스위칭 대상이 되는 프로세스의 pid를 출력해봤다. 기본 구현은 라운드 로빈이기 때문에 순서대로 순환되며 출력 될거라 예상했지만 순서가 뒤섞여서 출력이 되었다. 원인은 CPUS 옵션을 주지 않으면 기본값이 3으로 설정되기 때문에 여러 스케쥴러가 동시에 실행되어서 그런 것이었다. CPUS를 1로 설정하자 기대한대로 순서대로 순환되어 출력하는 것을 확인하였다.

0

scheduler 함수의 분석을 시작했다. scheduler는 컨텍스트 스위칭을 위해 swtch함수를 호출하는데, 이 함수는 어셈블리로 작성되어 있는지라 언제 리턴 되는지 더더욱 파악하기가 어려웠다. (사실 어셈블리가 아니었어도 파악하기는 쉽지 않았을거다.)

Claude Code에게 코드베이스 분석을 맡기고 몇가지 문답을 통해 타이머 인터럽트가 trap 핸들러를 호출하고, trap 핸들러가 yield->sched->swtch흐름으로 호출하여 scheduler함수로 돌아간다는 사실을 파악했다. 이 과정이 반복되며 프로세스는 순차 실행되고 그 사이사이에 scheduler가 교차 실행된다는 것을 알게 되었다.

책에 설명이 있긴 하지만 코드를 한땀한땀 분석했으면 오래 걸렸을텐데, Claude Code로 시간을 아낄 수 있었다. 예전엔 공부할 엄두가 안나던 것들이 LLM덕분에 조금은 가벼운 마음으로 시작해 볼 수 있게 되었다. 좋은 세상이다.

1

현재의 스케쥴러 구현인 라운드 로빈 방식을 이해해볼겸 루프안에서 fork를 사용해 n개의 프로세스를 생성하는 간단한 코드를 작성했다.

프로세스가 기대와 달리 n개가 생성되는게 아닌, (말 그대로) 기하급수적으로 생성되어서 원인을 생각해봤다. fork는 프로세스를 처음부터 실행하는게 아니라 현재까지 실행된 상태를 복제하여 새로운 프로세스를 만들고, 자기자신의 프로세스도 계속해서 실행하기 때문이다.

그렇기 때문에 n개의 프로세스를 만들려면 fork를 부모 프로세스만 실행할 수 있도록 자식 프로세스는 원하는 작업을 실행하고 루프를 탈출시켜야 한다는 일종의 fork 사용에 대한 관례를 배웠다.

사실 책 앞에 다 나왔던 내용인데 대충 읽었더니 이렇게 멀리 돌아가는 고생을 하고 있다. ㅋㅋㅋ

0

난수 생성기가 만만해보여 먼저 찾아봤다. 적당히 복붙해서 구현하려 했으나 균일성, 편향제거, 동시성, 병렬성등 고려해야 할게 많다는 사실을 새삼 깨달았다. 그동안 단일 스레드 환경에서 편하게 살았음을 실감했다.

1
0
4
1

여성향 커미션 중개 플랫폼 크레페를 운영하는 쿠키플레이스에서 시니어 백엔드 엔지니어 채용을 진행중입니다. 채용공고에 해당 직무 소개, 복지, 연봉, 회사문화의 내용이 포함돼 있습니다. 많은 관심 부탁드립니다. Node.js, TypeScript, GraphQL에 대한 높은 숙련도 및 지식으로 팀에 기여해주실 분을 쿠키플레이스에서 극진히 기대하고 있습니다.

크레페에서는 이런 기술스택을 사용합니다

  • Node.js, TypeScript, Vitest, Fastify
  • GraphQL  - Yoga, Relay, Pothos, Prisma
  • ElasticSearch, MongoDB, FCM
  • Docker, Github Actions
  • AWS  - ElasticBeanstalk, CloudWatch, Aurora PostgreSQL, Lambda, SES, S3, ElastiCache (Redis)
  • Grafana, Sentry

구성원의 성장과 덕질을 지원해요

  • 희망 도서 구매 (만화책 및 TRPG 룰북 포함)
  • 워크샵 및 교육 프로그램 지원
  • 전시, 공연 및 각종 행사 티켓 지원
  • 월 5만 크레페 포인트
  • 전동작탁 AMOS JP-EX COLOR
  • 6인용 TRPG/보드게임 테이블

지원자님이 예상하실 수 있는 처우는 이래요

  • 연봉: 최소 8000만원 ~ 최대 2억원 (주 40시간)
  • 스톡옵션 부여에 열려있는 포지션
크레페, 나만의 레시피, 나만의 창작물
10
1
0
0

자손킴 replied to the below article:

펑터Functor

lionhairdino @lionhairdino@hackers.pub

하스켈 펑터 입문자를 위한 이 글은 `Maybe Int` 타입의 값에서 `Int`를 직접 "꺼내올 수 없다"는 개념을 설명합니다. `Maybe`의 `fmap`이나 `fromJust`가 마치 값을 꺼내는 것처럼 보이지만, 이는 실제로는 값을 꺼내는 것이 아니라, 원본 타입(`Int`)의 구조를 보존하며 새로운 `Maybe Int` 타입의 값을 "생성"하는 과정이라는 것입니다. 미끄럼틀 비유를 통해, `Maybe Int`의 `Just 1`은 `Int` 값 `1`과 연관되어 있지만, `Just 1` 자체가 `1`을 의미하는 것은 아닙니다. 펑터는 원본 타입의 관계(구조)를 그대로 유지하며 다른 타입으로 변환하는 역할을 합니다. `fmap`은 `Maybe Int` 안의 `Int`를 직접 조작하는 것이 아니라, 원본 `Int` 값의 관계를 바탕으로 새로운 `Maybe Int` 값을 만들어내는 것입니다. 상자 메타포가 유용할 때도 있지만, 펑터의 본질을 오해하게 만들 수 있습니다. 상자 안의 값을 꺼내는 것이 아니라, 값의 "성격"은 값을 다루는 함수들의 동작에 따라 결정된다는 점을 강조합니다. 이 글은 "없을 수도 있는 수를 꺼낸다"는 표현의 모순을 지적하며, 펑터의 개념을 더 깊이 이해하도록 돕습니다.

Read more →
6
1

tanstack query의 initialPageParam에 대하여 오늘 배운 것

자손킴 @jasonkim@hackers.pub

TanStack Query의 `useInfiniteQuery` 훅을 사용할 때 `initialPageParam`이 어떻게 동작하는지에 대한 중요한 통찰을 공유합니다. 이 훅은 초기 렌더링 시 `initialPageParam`을 `pageParams[0]`으로 설정하고, 동일한 `queryKey`를 가진 캐시가 유지되는 동안 이 값을 계속 사용합니다. 따라서 여러 컴포넌트에서 동일한 `queryKey`로 `useInfiniteQuery`를 호출하면서 다른 `initialPageParam` 값을 제공하더라도, 처음 호출된 `initialPageParam` 값으로 고정됩니다. 이는 시작 커서가 다를 경우 `queryKey`를 다르게 지정해야 함을 의미합니다. 이러한 동작은 이해하고 나면 당연하지만, 익숙하지 않은 개발자에게는 혼란스러울 수 있습니다. `initialPageParam`이 `queryKey`와 강하게 연결되어 있다는 점이 InfiniteQueryOptions에서 타입 제약으로 더 명확하게 표현된다면 개발 경험이 향상될 것입니다.

Read more →
6