DNS 동작 원리
파드에서 이름 하나에 질의가 8번 나간 이유
코드에서 http.Get을 부르면, 요청이 나가기 전에 이름을 IP 주소로 바꾸는 단계를 먼저 거친다. 이 일을 하는 게 DNS다. 받은 질의를 한 줄씩 찍는 작은 DNS 서버를 띄워 세어 보니, 쿠버네티스 파드와 같은 설정에서는 이름 하나에 질의가 8번 나가 있었다.
읽어 보기공부한 개념부터 직접 부딪혀 본 문제까지, 개발하며 알게 된 것들
파드에서 이름 하나에 질의가 8번 나간 이유
코드에서 http.Get을 부르면, 요청이 나가기 전에 이름을 IP 주소로 바꾸는 단계를 먼저 거친다. 이 일을 하는 게 DNS다. 받은 질의를 한 줄씩 찍는 작은 DNS 서버를 띄워 세어 보니, 쿠버네티스 파드와 같은 설정에서는 이름 하나에 질의가 8번 나가 있었다.
읽어 보기이미지가 있는데 pull한다
고객사에 설치한 서비스가 뜨지 않는다는 연락이 오면, 파드 상태는 대부분 ErrImagePull이었고 가끔 ImagePullBackOff였다. 처음에는 이게 잘 이해되지 않았다.
읽어 보기OOMKilled·Evicted는 누가 죽였나
GOMEMLIMIT 글을 쓰면서 컨테이너 메모리 한도 얘기를 여러 번 했는데, 정작 그 한도를 정하는 쿠버네티스 쪽은 requests는 최소, limits는 최대 정도로만 알고 있었다.
읽어 보기고객사에 설치한 서비스가 뜨지 않는다는 연락이 오면, 파드 상태는 대부분 ErrImagePull이었고 가끔 ImagePullBackOff였다. 처음에는 이게 잘 이해되지 않았다.
GOMEMLIMIT 글을 쓰면서 컨테이너 메모리 한도 얘기를 여러 번 했는데, 정작 그 한도를 정하는 쿠버네티스 쪽은 requests는 최소, limits는 최대 정도로만 알고 있었다.
이직을 준비하면서 매주 같은 일을 반복했다. 채용 사이트를 돌며 공고를 찾고, 하나씩 읽어 보며 나와 맞는지 따지고, 지원할 곳이 생기면 이력서를 고쳤다. 이걸 AI 에이전트에게 맡기면 금방 끝날 줄 알았는데, 막상 해 보니 제일 오래 고민한 건 AI에게 어디까지 맡겨야 하나였다.
고루틴을 만들기는 쉽다. 어려운 건 누가 데이터를 바꾸고, 누가 일을 끝내고, 언제 함수가 반환해도 되는지 정하는 일이었다. 채널을 잘못 쓰면 고루틴이 영원히 갇히는데, GC는 갇힌 고루틴을 치워 주지 않는다.
앞 글에서 GOGC와 GOMEMLIMIT을 바꾸는 동안 할당 누계 256MiB는 그대로였다. 이 숫자를 줄이는 방법 중 하나가 짧게 쓰고 버리는 버퍼를 sync.Pool로 다시 쓰는 것이다.
요즘은 코드를 짤 때도, 글을 정리할 때도 하루의 상당 부분을 AI와 함께 보낸다. 그런데 정작 이 모델이 어떻게 만들어졌고, 내가 보낸 문장을 어떤 순서로 계산해서 답을 내는지는 설명하지 못했다.
컨테이너 limit이 1GB인데 힙 800MB에서 OOMKilled가 났다. GC가 있는 언어인데 왜 메모리가 넘쳤을까. GC는 살아 있는 힙의 비율로 돌고, 컨테이너는 절대값으로 프로세스를 죽인다.
go를 붙이면 함수가 동시에 진행된다. 그렇다면 고루틴을 10만 개 만들면 스레드도 10만 개가 생길까. GOMAXPROCS를 1로 두면 한 번에 한 작업만 처리할까.
인터페이스, 슬라이스, defer, 임베딩, context — 다섯 편을 쓰고 돌아보니 함정이 대부분 두 개의 축에서 나오고 있었다. 하나는 "모든 것은 복사된다", 다른 하나는 "클래스 없이 객체지향을 한다".
context.WithTimeout으로 1시간을 걸면 그 요청은 1시간까지 기다린다고 생각했다. 그런데 30초에 죽었다. context는 끝까지 갔다. 다만 그게 유일한 타이머가 아니었다. 그리고 타이머를 고치고 나니, 진짜 문제는 따로 있었다.
내부 테스트에서 클러스터 리소스 목록 조회가 느려서 인덱스를 걸었다. 실행시간이 233ms에서 104ms로 줄어서 처음엔 해결된 줄 알았다. 그런데 EXPLAIN ANALYZE에서 읽은 행 수를 보니 205,000에서 200,025로 거의 그대로였다. 숫자가 이상했다.
좋아서 쓰는 도구인데, 쓰면 쓸수록 회의감이 커졌다. 그 감정이 어디서 오는지는 한 인터뷰를 보고 나서야 조금 알게 됐다.
ctx를 첫 인자로 받아서 넘기는 것까지는 하고 있었다. 그런데 취소하면 고루틴이 정말 멈추는지는 확인해 본 적이 없었다. 면접에서 그 질문에 말이 막힌 뒤 context를 처음부터 다시 봤다.
검증 단계가 타임아웃으로 죽었다. 30분 만에 "N+1이니까 배치로 묶자"는 답이 나왔고, 그건 절반만 맞았다. 최종 설계에 도착하기까지 두 번 방향을 틀어야 했는데, 남은 게 코드보다 그 과정이라서 정리해둔다.
Go에는 클래스도 상속도 없다. 그런데 객체지향이 아니라고 하기도 어렵다. "Go는 객체지향 언어인가"에 대한 공식 FAQ의 답이 "Yes and no"인 이유를, 상속을 뺀 자리에 무엇을 넣었는지 따라가며 정리했다.
Go는 예외를 뺐고, 그 자리를 defer와 값으로서의 error로 채웠다. 셋 다 문법은 금방 배우는데 함정은 몇 년째 나온다. 각각이 정확히 언제 무엇을 붙잡는지를 기준으로 다시 정리했다.
Go에서 자주 만나는 버그는 들고 있다고 생각한 것과 실제로 들고 있는 것이 다를 때 생긴다. 슬라이스는 데이터가 아니라 창이고, map은 주소를 보장하지 않고, string은 문자 배열이 아니다. 세 자료구조의 레이아웃을 하나씩 열어 본다.
nil 포인터를 반환했는데 err != nil이 참이 되는 이유는 인터페이스 값이 타입과 데이터 두 칸으로 되어 있기 때문이다. 두 칸 구조에서 메서드셋, 인터페이스를 둘 위치, 동적 디스패치 비용까지 따라간다.
DB 총정리 셋째 편, 설계 이야기. 중복이 만드는 이상 현상과 1·2·3정규형, 측정한 병목에만 쓰는 반정규화, 그리고 비즈니스 로직을 ORM에 둘지 Stored Procedure에 둘지.
DB 총정리 둘째 편. ACID는 트랜잭션의 약속이고, 격리 수준은 그중 격리성을 성능과 맞바꾸는 다이얼이며, Redo/Undo는 장애가 나도 원자성과 지속성을 지키는 장치다.
컴파일 언어와 정적 타입이 런타임에 어떤 이득으로 바뀌는지 Go를 기준으로 따라갔다. 메모리 레이아웃, 컴파일러 최적화, escape analysis, GC와 스케줄러까지 도구로 직접 확인한다.
DB 총정리 첫 편. 데드락과 라이브락, 비관적 락과 낙관적 락을 고르는 기준, 그리고 WHERE가 빠진 UPDATE 한 줄로 전체 유저가 로그아웃된 사고.
사람마다 local CLAUDE.md가 달라서 같은 코드베이스에서도 Claude가 다르게 움직였다. 팀 파일을 git 공유 하나로 합치고, 줄이고, PR마다 키우는 문서로 바꾼 과정.
Superpowers, agentmemory, context-mode, codebase-memory-mcp, RTK를 직접 깔아 보고 역할별로 나눴다. 토큰을 줄이는 건 context-mode와 RTK이고, 둘은 줄이는 층이 다르다.
rtk init -g가 성공 메시지를 냈는데도 PreToolUse 훅이 등록되지 않았다. 원인은 settings.json에 남은 쉼표 하나였다.
pod를 여러 개 띄우자 같은 job을 두 pod가 동시에 완료 처리하는 경합이 생겼다. 분산락 없이 조건부 UPDATE 한 문장, CAS 패턴으로 막은 과정.
Thariq Shihipar의 Field Guide to Fable 발표를 보고 정리했다. Fable에서 결과를 가르는 건 모델의 지능보다, 내가 모르는 것을 시작 전에 얼마나 드러내느냐였다.