계약관계에서 가장 중요한 가치는 사랑이 아니라 믿음, 정확히 말하면 신뢰다. 사랑은 감정에 기반하지만 계약은 약속과 책임에 기반한다. 서로를 좋아하지 않더라도 계약은 성립할 수 있지만, 상대방이 약속을 지키리라는 신뢰가 없다면 계약관계는 유지되기 어렵다.

계약서를 작성하는 이유도 상대방을 무조건 믿기 때문은 아니다. 무엇을 언제까지 수행하고, 그 대가로 무엇을 지급하며, 문제가 발생하면 어떻게 책임질지를 명확하게 정하기 위해서다. 계약서가 관계의 기준을 만든다면, 실제 행동은 그 기준을 지킬 사람인지 증명한다. 결국 신뢰는 막연한 기대가 아니라 명확한 조건과 반복된 이행의 결과다.

물론 존중과 인정도 중요하다. 존중은 상대방을 일방적으로 이용하지 않고 동등한 계약 주체로 대하는 태도다. 인정은 상대방이 수행한 일과 기여를 정당하게 평가하는 것이다. 존중과 인정이 없는 계약은 법적으로 유지될 수는 있어도, 장기적으로 건강한 협력관계가 되기는 어렵다.

성취는 계약관계의 목적이자 결과에 가깝다. 계약 당사자들은 공동의 목표를 달성하거나 각자 필요한 이익을 얻기 위해 관계를 맺는다. 그러나 눈앞의 성과를 위해 약속을 어기거나 상대방에게 책임을 전가한다면 한 번의 성취는 가능해도 다음 계약은 어려워진다. 지속적인 성취 역시 신뢰가 뒷받침되어야 한다.

계약관계에서 사랑까지 요구할 필요는 없다. 회사가 직원을 가족처럼 사랑해야 하거나 직원이 회사를 사랑해야 계약이 유지되는 것은 아니다. 오히려 사랑이나 의리를 강조하면서 계약상의 권리와 책임을 모호하게 만드는 관계를 경계해야 한다. 급여, 업무 범위, 계약기간, 근무조건, 성과 기준과 책임 소재는 감정이 아니라 합의된 조건에 따라 판단해야 한다.

좋은 계약관계는 말보다 이행으로 증명된다. 사용자는 정해진 보수를 제때 지급하고, 근로자나 수행자는 합의된 업무를 책임 있게 완수해야 한다. 불가피한 문제가 생겼다면 숨기지 않고 공유하며, 합의가 필요하면 일방적으로 결정하지 않아야 한다. 이러한 행동이 반복될 때 서로에 대한 예측 가능성이 높아지고 신뢰가 형성된다.

결국 계약관계의 구조는 단순하다. 존중은 관계의 태도이고, 인정은 기여에 대한 평가이며, 성취는 계약의 결과다. 그리고 이 모든 것을 지속하게 만드는 기반이 신뢰다. 계약관계는 상대방의 선의를 막연히 믿는 관계가 아니라, 명확한 약속을 세우고 그 약속을 행동으로 지키면서 신뢰를 축적하는 관계다.

LIST


OpenAI가 주도하고 Google Chrome, Cloudflare, Shopify, Vercel 등이 참여하는 WebMCP Challenge의 핵심 내용을 정리한 카드뉴스입니다. 사람이 사용하는 기존 웹 UI를 AI 에이전트가 해석하던 방식에서 벗어나, 웹사이트가 AI 전용 API 인터페이스(도구)를 직접 제공하는 패러다임 전환을 다루고 있습니다.
​WebMCP 핵심 개념과 동작 방식
​개념 정의: 웹사이트가 AI 에이전트에게 자신의 기능(함수·액션)을 표준화된 규격으로 직접 제공하는 체계입니다.
​기존 방식 (화면 기반 자동화): AI가 DOM이나 렌더링 화면을 읽고, 검색창과 버튼 위치를 찾아 클릭하는 시각적/스크래핑 기반 접근 방식입니다. UI 변경에 취약하고 속도가 느리며 오류율이 높습니다.
​WebMCP 방식 (기능 직접 호출): 쇼핑몰의 상품 검색, 장바구니 추가, 결제 요청과 같은 핵심 비즈니스 로직을 웹사이트가 정식 도구로 노출합니다. AI는 이를 직접 호출하므로 실행 속도가 빠르고 결정론적 안정성을 가집니다.
​활용 시나리오 및 빅테크 연합
​적용 예시: 웹 환경 내 3D 모델 제작, 문서 공동 작업, 동적 콘텐츠 생성·수정, 웹사이트 네이티브 기능 대행 등 인간-AI 협업 환경을 구축합니다.
​생태계 참여 기업: 브라우저(Google Chrome), 인프라·네트워크(Cloudflare, Vercel, Render, Netlify), 이커머스 플랫폼(Shopify)이 합류하여 웹 표준 수준의 생태계를 형성하고 있습니다.
​챌린지 개요 및 혜택
​기간: 8월 25일 ~ 9월 3일 (오후 1시 PT 마감, 9월 23일 발표 예정)
​상금 및 혜택: 총상금 35,000달러(우승 10개 팀 선발, 팀당 3,000달러), Codex Micro, ChatGPT Pro 1년 이용권 및 파트너사 상품 제공
​출품 요건: 웹사이트 기능을 AI 에이전트 도구로 제공하는 자동화, 웹앱, 협업 도구 등을 오픈소스 또는 데모 형태로 제출
​"사람에게는 버튼과 화면을, AI에게는 정식 출입구(API 규격)를 준다"는 원칙 아래, 웹이 '인간 친화적 UI'에서 'AI 에이전트 친화적 인터페이스'로 확장되는 흐름을 보여줍니다.

LIST


RDBMS 설계는 단순히 데이터를 테이블에 나누어 저장하는 작업이 아니다. 업무에서 발생하는 사실과 규칙을 구조로 표현하고, 데이터가 시간이 지나도 일관성과 신뢰성을 유지하도록 만드는 일이다. 좋은 데이터베이스는 현재 요구사항만 처리하는 것이 아니라 데이터 증가, 기능 변경, 장애와 동시성까지 견딜 수 있어야 한다.

업무를 먼저 이해해야 한다

데이터베이스 설계는 테이블이 아니라 업무에서 출발해야 한다. 고객, 주문, 결제처럼 눈에 보이는 명사만 찾아 테이블로 만드는 것으로는 부족하다. 각 데이터가 언제 생성되고, 어떤 상태를 거치며, 무엇과 연결되고, 어느 조건에서 변경되거나 삭제되는지 파악해야 한다.

예를 들어 주문과 결제는 흔히 함께 발생하지만 동일한 개념은 아니다. 하나의 주문에 여러 차례 결제를 시도할 수 있고, 결제 후 부분 취소나 환불이 발생할 수도 있다. 이를 하나의 테이블에 합치면 처음에는 편해 보이지만 업무가 확장될수록 상태와 금액의 의미가 뒤섞인다.

따라서 설계자는 다음 질문에 답할 수 있어야 한다.

- 이 데이터는 어떤 업무상의 사실을 나타내는가?
- 누가 언제 생성하고 변경하는가?
- 다른 데이터와 어떤 관계를 맺는가?
- 반드시 지켜야 할 규칙은 무엇인가?
- 변경 이력과 삭제 기록을 보존해야 하는가?

테이블은 하나의 명확한 책임을 가져야 한다

좋은 테이블은 하나의 업무 개념을 표현한다. 회원 테이블은 회원을, 주문 테이블은 주문을, 결제 테이블은 결제 사실을 관리해야 한다. 서로 다른 생명주기와 변경 사유를 가진 데이터를 한곳에 넣으면 컬럼의 의미가 불명확해지고 NULL이 증가하며 수정 범위도 커진다.

반대로 모든 데이터를 지나치게 잘게 나누는 것도 좋은 설계는 아니다. 분리 기준은 단순히 컬럼 수가 아니라 업무적 책임, 데이터의 생명주기, 변경 빈도와 관계의 독립성이다. 항상 함께 생성되고 함께 조회되며 별도로 존재할 의미가 없다면 하나의 구조로 관리하는 편이 자연스러울 수 있다.

핵심은 높은 응집도와 낮은 결합도다. 한 테이블 안의 데이터는 서로 밀접해야 하고, 다른 테이블과는 필요한 관계만 가져야 한다.

정규화로 중복과 이상 현상을 줄인다

정규화는 데이터 중복을 줄이고 삽입·수정·삭제 과정에서 발생하는 이상 현상을 방지하기 위한 원칙이다.

하나의 주문 행에 고객 이름과 주소를 반복해서 저장하면 고객 정보가 변경될 때 여러 주문을 함께 수정해야 한다. 일부 행만 변경되면 동일한 고객이 서로 다른 정보를 가진 것처럼 보이게 된다. 고객 정보를 회원 테이블에서 관리하고 주문이 회원을 참조하도록 만들면 이러한 불일치를 줄일 수 있다.

일반적인 업무 시스템에서는 제3정규형을 기본으로 검토한다.

- 하나의 컬럼에는 하나의 값만 저장한다.
- 기본키 일부에만 의존하는 컬럼을 분리한다.
- 기본키가 아닌 컬럼에 종속되는 컬럼을 분리한다.

다만 정규화는 무조건 테이블을 많이 만드는 절대 규칙이 아니다. 조회 성능이나 통계 처리 때문에 중복 저장이 필요할 수 있다. 이때는 편의를 위해 무심코 중복시키는 것이 아니라, 원본 데이터와 파생 데이터의 책임을 명확히 하고 갱신 전략까지 설계해야 한다. 반정규화는 정규화를 이해한 뒤 근거를 가지고 선택해야 한다.

기본키는 데이터의 정체성을 나타낸다

모든 주요 테이블에는 각 행을 안정적으로 식별할 수 있는 기본키가 필요하다. 이름, 전화번호, 사업자등록번호 같은 업무 값은 변경되거나 정책에 따라 중복될 가능성이 있으므로 기본키로 사용하기 까다롭다.

대부분의 시스템에서는 숫자형 대리키나 UUID를 식별자로 사용하고, 업무적으로 고유해야 하는 값에는 별도의 UNIQUE 제약조건을 둔다. 이를 통해 내부 식별자와 업무 규칙을 분리할 수 있다.

기본키는 한 번 부여한 뒤 가능하면 변경하지 않아야 한다. 다른 테이블이 해당 키를 외래키로 참조하므로 기본키 변경은 시스템 전체에 영향을 줄 수 있기 때문이다.

관계와 카디널리티를 정확하게 표현한다

관계형 데이터베이스의 핵심은 데이터 사이의 관계다. 설계할 때는 관계가 1:1인지, 1:N인지, N:M인지 명확하게 판단해야 한다.

회원 한 명이 여러 주문을 가질 수 있다면 회원과 주문은 1:N 관계다. 상품과 카테고리처럼 양쪽이 서로 여러 대상을 가질 수 있다면 N:M 관계이며, 이를 연결 테이블로 표현해야 한다.

관계를 정의할 때는 필수 여부도 중요하다. 주문은 반드시 회원을 가져야 하는지, 비회원 주문도 가능한지에 따라 외래키의 NULL 허용 여부와 제약조건이 달라진다. 카디널리티를 잘못 정의하면 애플리케이션 코드가 예외 상황을 계속 보완해야 한다.

무결성은 애플리케이션뿐 아니라 DB에서도 보장해야 한다

데이터 규칙을 서비스 코드에만 맡기면 배치, 운영 SQL, 다른 서비스의 직접 접근 등 우회 경로에서 잘못된 데이터가 들어올 수 있다. 데이터베이스가 보장할 수 있는 규칙은 제약조건으로 표현하는 것이 안전하다.

- PRIMARY KEY로 행의 고유성을 보장한다.
- FOREIGN KEY로 존재하지 않는 데이터에 대한 참조를 막는다.
- UNIQUE로 업무상 중복될 수 없는 값을 보호한다.
- NOT NULL로 필수 값을 강제한다.
- CHECK로 상태와 값의 허용 범위를 제한한다.

예를 들어 주문 수량이 반드시 1 이상이어야 한다면 애플리케이션 검증과 함께 데이터베이스의 CHECK 제약조건으로도 보호할 수 있다. 데이터베이스는 데이터를 보관하는 장소가 아니라 마지막 무결성 방어선이다.

상태와 이력을 구분해야 한다

현재 상태만 저장할 것인지, 상태가 변한 과정까지 남길 것인지 결정해야 한다. 주문 테이블의 상태를 "PAID"에서 "CANCELLED"로 변경하면 현재 상태는 알 수 있지만 언제, 누구에 의해, 어떤 이유로 변경되었는지는 알 수 없다.

감사, 정산, 금융, 승인 업무에서는 현재 상태와 별도로 이력 테이블이나 원장성 데이터를 관리하는 것이 필요하다. 특히 금액 관련 데이터는 기존 값을 덮어쓰기보다 취소나 조정 내역을 새로운 행으로 기록하는 방식이 추적과 검증에 유리하다.

상태값도 의미 없이 숫자로만 정의하기보다 허용되는 상태와 전이 규칙을 명확히 해야 한다. 존재할 수 없는 상태 조합이 발생하지 않도록 데이터 구조와 애플리케이션 로직을 함께 설계해야 한다.

금액과 시간은 정확한 자료형으로 저장해야 한다

금액을 부동소수점 자료형으로 저장하면 계산 과정에서 미세한 오차가 발생할 수 있다. 금액은 "DECIMAL"이나 "NUMERIC"처럼 정확한 고정소수점 자료형을 사용하고, 통화와 소수 자릿수 정책을 명확히 해야 한다.

시간 데이터는 단순한 문자열이 아니라 날짜·시간 자료형으로 저장해야 한다. 여러 지역에서 사용하는 시스템이라면 UTC 기준 저장과 사용자 시간대 변환 원칙도 필요하다. 생성 시각, 수정 시각, 업무 발생 시각은 의미가 다르므로 하나의 컬럼으로 혼용해서는 안 된다.

자료형은 저장 공간만 결정하지 않는다. 입력 가능한 값의 범위와 데이터의 의미를 규정한다. 따라서 “일단 문자열로 저장하자”는 방식은 검증과 검색, 정렬, 연산을 어렵게 만든다.

트랜잭션 경계를 업무 단위로 설계해야 한다

트랜잭션은 여러 데이터 변경을 하나의 작업으로 묶어 모두 성공하거나 모두 실패하게 만든다. 주문 생성과 재고 차감이 하나의 업무적 원자성을 가져야 한다면 중간 상태가 남지 않도록 트랜잭션 범위를 설계해야 한다.

다만 트랜잭션을 지나치게 길게 유지하면 잠금 시간이 증가하고 동시 처리 성능이 떨어진다. 외부 API 호출이나 사용자 입력 대기를 데이터베이스 트랜잭션 안에 포함하는 것은 피해야 한다.

분산 시스템에서는 하나의 데이터베이스 트랜잭션만으로 전체 정합성을 보장하기 어렵다. 이 경우 Outbox, 보상 트랜잭션, 멱등성, 재처리 전략 등과 함께 최종적 일관성을 설계해야 한다. 중요한 것은 모든 데이터를 즉시 일치시키는 것이 아니라, 불일치가 발생했을 때 탐지하고 복구할 수 있는 구조를 만드는 것이다.

인덱스는 실제 조회 패턴을 기준으로 설계한다

인덱스는 검색 성능을 향상시키지만 많다고 좋은 것은 아니다. 인덱스가 늘어나면 저장 공간이 증가하고 INSERT, UPDATE, DELETE 비용도 커진다.

인덱스는 다음 요소를 기준으로 결정해야 한다.

- 자주 사용되는 조회 조건
- 조인에 사용되는 컬럼
- 정렬 및 범위 검색 조건
- 컬럼의 선택도
- 복합 인덱스의 컬럼 순서

복합 인덱스는 쿼리 조건과 데이터 분포를 고려해야 한다. 단순히 WHERE 절에 나오는 모든 컬럼을 묶는 방식은 효율적이지 않다. 실행 계획과 실제 운영 데이터를 확인하며 조정해야 한다.

삭제 정책을 명확히 해야 한다

데이터 삭제에는 물리 삭제와 논리 삭제가 있다. 물리 삭제는 행을 실제로 제거하고, 논리 삭제는 삭제 여부나 삭제 시각을 표시해 조회 대상에서 제외한다.

모든 테이블에 습관적으로 "deleted_yn"을 추가하면 쿼리마다 삭제 조건이 필요하고 UNIQUE 제약조건이나 인덱스 설계도 복잡해진다. 반대로 이력 보존이 필요한 데이터를 즉시 물리 삭제하면 감사와 복구가 불가능해질 수 있다.

따라서 법적 보존 의무, 개인정보 파기, 업무 이력, 복구 필요성에 따라 테이블별 삭제 정책을 정해야 한다. 삭제는 구현 세부사항이 아니라 데이터 생명주기 설계의 일부다.

성능보다 정합성을 먼저 세운다

초기 설계 단계에서 예상 성능만을 이유로 데이터 구조를 왜곡하면 복잡성과 오류가 커질 가능성이 높다. 먼저 업무 의미와 정합성을 기준으로 모델을 만들고, 실제 데이터와 쿼리를 측정한 뒤 인덱스, 캐시, 파티셔닝, 읽기 모델, 반정규화를 적용하는 것이 합리적이다.

성능 문제는 측정과 개선이 가능하지만, 의미가 뒤섞이고 신뢰를 잃은 데이터는 복구하기 어렵다. 특히 주문, 결제, 정산처럼 돈과 책임이 연결된 영역에서는 빠른 처리보다 정확한 데이터가 우선이다.

좋은 RDBMS 설계의 기준

좋은 설계는 테이블 수가 적거나 SQL이 짧은 설계가 아니다. 다음 질문에 명확하게 답할 수 있는 설계다.

- 각 테이블이 어떤 업무 개념을 나타내는가?
- 각 데이터의 원본은 어디인가?
- 중복과 불일치를 어떻게 방지하는가?
- 잘못된 값이 저장되는 것을 어디서 차단하는가?
- 동시 수정과 장애가 발생해도 정합성을 유지하는가?
- 변경 과정과 책임을 추적할 수 있는가?
- 데이터가 증가해도 주요 조회를 처리할 수 있는가?
- 업무 규칙이 바뀌었을 때 영향 범위를 통제할 수 있는가?

RDBMS 설계의 본질은 현실의 업무를 데이터 구조로 번역하는 데 있다. 정규화, 키, 관계, 제약조건, 트랜잭션과 인덱스는 그 목적을 위한 수단이다. 좋은 데이터베이스는 애플리케이션 뒤에 숨어 있는 저장소가 아니라 시스템의 규칙과 신뢰를 지탱하는 핵심 구조다.

LIST



과거에는 하나의 서비스를 만들기 위해 기획자, 디자이너, 개발자, 인프라 엔지니어, 운영 담당자가 필요했다. 그러나 AI와 자동화 도구가 발전하면서 한 사람이 아이디어를 구체화하고, 코드를 작성하고, 배포한 뒤 고객의 반응까지 확인할 수 있는 환경이 만들어졌다. 이른바 ‘1인 빌더’의 시대다.

사진 속 작업 환경도 이러한 변화를 보여준다. ChatGPT는 조사와 기획, 문서 작성, 코드 분석을 지원하고, 터미널과 코드 에디터는 실제 개발 작업을 수행하는 공간이 된다. GitHub는 코드와 변경 이력을 관리하며, Google Drive와 Calendar, Gmail은 문서·일정·외부 소통을 담당한다. 여러 터미널과 작업을 묶어 관리하는 도구는 동시에 진행되는 개발 흐름을 정리해 준다.

여기서 중요한 것은 도구의 개수가 아니다. 각각의 도구가 하나의 작업 흐름으로 연결돼 있느냐가 중요하다.

1인 빌더는 혼자 일하지만 모든 일을 직접 처리하지 않는다. AI에게 자료 조사와 초안 작성을 맡기고, 에이전트에게 테스트와 반복 작업을 위임하며, 자동화 시스템이 빌드와 배포를 수행하도록 만든다. 사람은 무엇을 만들 것인지 결정하고, 결과를 검증하며, 다음 우선순위를 판단한다.

결국 혼자 일한다는 의미가 바뀌고 있다. 과거의 1인 개발자는 기획부터 배포까지 모든 작업을 자신의 손으로 처리해야 했다. 지금의 1인 빌더는 여러 AI와 자동화 도구를 지휘하는 작은 조직의 책임자에 가깝다.

하지만 도구가 많다고 생산성이 자동으로 높아지는 것은 아니다. 작업 환경이 복잡해지면 알림과 창을 관리하는 데 더 많은 시간을 쓰게 된다. AI가 만든 결과를 검증하지 못하면 개발 속도는 빨라져도 제품의 품질은 떨어진다. 잘못된 방향으로 빠르게 달리는 것 역시 실패다.

따라서 1인 빌더에게 필요한 핵심 역량은 세 가지다.

첫째는 문제를 정의하는 능력이다. 무엇을 만들 수 있는가보다 누구의 어떤 문제를 해결할 것인가가 먼저다.

둘째는 작업을 분해하고 위임하는 능력이다. 사람이 판단할 일, AI가 처리할 일, 자동화할 일을 구분해야 한다.

셋째는 결과를 검증하는 능력이다. 코드가 실행된다는 사실만으로 제품이 완성되는 것은 아니다. 테스트, 보안, 데이터 정합성, 운영 가능성까지 확인해야 한다.

1인 빌더의 진짜 경쟁력은 혼자서 많은 코드를 작성하는 데 있지 않다. 아이디어가 제품이 되고, 제품이 고객에게 전달되며, 고객의 반응이 다시 개선으로 이어지는 구조를 만드는 데 있다.

AI는 한 사람의 생산성을 크게 확장해 준다. 그러나 방향과 기준까지 대신 책임지지는 않는다. 좋은 1인 빌더란 도구를 많이 사용하는 사람이 아니라, 도구와 AI를 하나의 시스템으로 묶어 실제 결과를 만들어 내는 사람이다.

결국 1인 빌더의 작업 환경은 단순한 앱 모음이 아니다. 한 사람이 기획자이자 개발자, 운영자, 의사결정자로 일할 수 있도록 만든 작은 회사의 운영체제다.

LIST



Claude Code를 프로젝트에 도입하면 많은 개발자가 가장 먼저 "CLAUDE.md"를 작성한다. 프로젝트 구조와 기술 스택, 코딩 규칙, 빌드 명령어, 금지 사항을 정리하면 AI가 프로젝트를 더 정확하게 이해하고 일관된 코드를 만들 것이라고 기대하기 때문이다.

그런데 프로젝트가 진행되면서 "CLAUDE.md"에는 새로운 규칙이 계속 추가된다. AI가 실수할 때마다 주의사항을 넣고, 새로운 예외가 발견되면 또 다른 설명을 붙인다. 처음에는 유용했던 문서가 어느 순간 수백 줄의 규칙집으로 변한다.

문제는 문서의 길이만이 아니다. 이미 변경된 구조, 더는 사용하지 않는 기술, 서로 충돌하는 지침, 특정 상황에서만 필요했던 예외까지 그대로 남는다. 이런 상태에서는 규칙이 없는 것보다 오히려 나쁠 수 있다.

AI 코딩 지침도 코드와 마찬가지로 유지보수가 필요하다.

CLAUDE.md는 AI를 위한 프로젝트 운영 매뉴얼이다

"CLAUDE.md"는 단순한 설명서가 아니다. Claude Code가 작업을 시작할 때 참고하는 프로젝트의 상위 컨텍스트다. 사람으로 비유하면 신규 개발자에게 제공하는 온보딩 문서와 작업 규칙을 합친 것에 가깝다.

여기에 무엇을 적느냐에 따라 AI의 행동이 달라진다.

- 어떤 디렉터리를 먼저 확인할 것인가
- 기존 구조를 유지할 것인가, 새 구조를 만들 것인가
- 테스트를 어디까지 실행할 것인가
- 어떤 명령은 사용하지 말아야 하는가
- 작업 완료를 무엇으로 판단할 것인가
- 기존 코드와 충돌할 때 어떤 원칙을 우선할 것인가

따라서 "CLAUDE.md"의 품질은 AI가 생성하는 코드의 품질뿐 아니라 탐색 범위, 작업 속도, 토큰 사용량과도 연결된다.

중요한 것은 규칙의 개수가 아니다. 현재 프로젝트에 필요한 규칙이 정확하고 짧게 유지되고 있는가다.

규칙을 계속 추가하면 왜 문제가 되는가

AI가 실수했을 때 가장 쉬운 대응은 "CLAUDE.md"에 새로운 규칙을 추가하는 것이다.

“이 디렉터리는 수정하지 마라.”
“DTO에는 해당 어노테이션을 사용하지 마라.”
“테스트를 실행한 뒤 완료하라.”

이런 규칙은 당장의 문제를 해결할 수 있다. 그러나 실수가 발생할 때마다 설명을 추가하면 문서는 점점 비대해진다. 오래된 규칙과 새로운 규칙이 충돌하거나, 특정 모듈에만 필요한 지침이 전체 프로젝트에 적용될 수도 있다.

사람도 수백 개의 규칙을 동시에 정확하게 따르기 어렵다. AI 역시 긴 문서를 모두 같은 중요도로 해석하지 않는다. 핵심 규칙이 사소한 설명 사이에 묻히면 오히려 준수율이 낮아진다.

규칙을 추가하기 전에 먼저 물어야 한다.

- 이 실수는 정말 지침이 없어서 발생했는가?
- 코드 구조나 테스트로 방지할 수는 없는가?
- 특정 폴더에만 필요한 규칙은 아닌가?
- 기존 규칙과 중복되거나 충돌하지 않는가?
- 지금도 유효한 규칙인가?

모든 문제를 문장으로 통제하려 하면 "CLAUDE.md"는 프로젝트의 기술부채를 설명하는 문서가 된다.

첫째, 핵심 문서는 짧게 유지한다

최상위 "CLAUDE.md"에는 프로젝트 전체에 항상 적용되는 정보만 남기는 것이 좋다. 이미지에서는 이를 200줄 이하로 유지할 것을 제안한다. 200줄이 절대적인 기준은 아니지만, 핵심 지침이 한눈에 들어오는 수준으로 제한한다는 원칙은 타당하다.

최상위 문서에는 다음과 같은 내용이 적합하다.

- 프로젝트의 목적과 주요 기술
- 핵심 디렉터리 구조
- 반드시 지켜야 하는 아키텍처 원칙
- 공통 빌드·테스트·검증 명령어
- 보안 및 데이터 취급 원칙
- 전체 프로젝트에 적용되는 금지 사항
- 상세 규칙 문서의 위치

배경 설명과 변하지 않는 원칙은 남기되, 특정 작업에서만 필요한 절차와 세부 사례는 분리해야 한다.

문서를 줄인다는 것은 정보를 삭제하는 일이 아니다. 정보의 위치와 적용 범위를 명확하게 만드는 일이다.

둘째, 필요한 규칙만 조건부로 불러온다

모든 규칙을 항상 읽게 할 필요는 없다. 프론트엔드 작업에 데이터베이스 마이그레이션 규칙이 필요하지 않고, 백엔드 API 수정에 디자인 시스템의 상세 규칙이 항상 필요한 것도 아니다.

규칙을 영역별로 분리하면 필요한 작업에서만 해당 컨텍스트를 제공할 수 있다.

.claude/
└── rules/
    ├── backend.md
    ├── frontend.md
    ├── database.md
    ├── testing.md
    └── security.md

또는 각 하위 디렉터리에 별도의 "CLAUDE.md"를 두어 해당 영역을 수정할 때만 적용되게 할 수 있다.

project/
├── CLAUDE.md
├── backend/
│   └── CLAUDE.md
├── frontend/
│   └── CLAUDE.md
└── infrastructure/
    └── CLAUDE.md

이렇게 하면 최상위 문서는 공통 원칙을 담당하고, 하위 문서는 해당 영역의 구체적인 제약조건을 담당한다. 컨텍스트가 짧아질 뿐 아니라 규칙의 책임 범위도 분명해진다.

이는 소프트웨어를 높은 응집도와 낮은 결합도로 설계하는 것과 같은 원리다.

셋째, 모든 실수가 아니라 반복되는 실패만 기록한다

AI가 한 번 실수했다고 해서 모두 규칙으로 남길 필요는 없다. 우연한 오류, 요구사항의 모호함, 일회성 작업에서 발생한 문제까지 문서화하면 규칙의 밀도만 낮아진다.

기록할 가치가 있는 것은 다음에도 반복될 가능성이 높은 실패다.

- 프로젝트의 관례와 일반적인 구현 방식이 다른 경우
- 자동 탐색만으로 알기 어려운 제약이 있는 경우
- 실수할 경우 데이터나 운영 환경에 큰 피해가 발생하는 경우
- 여러 작업에서 동일한 오류가 반복되는 경우
- 테스트나 정적 분석만으로 차단하기 어려운 경우

실패 사례를 기록할 때는 장황한 경위보다 행동 규칙을 짧게 남겨야 한다.

나쁜 예는 다음과 같다.

«지난 작업에서 Claude가 엔티티를 직접 반환하여 문제가 발생했으므로 앞으로 컨트롤러 작업을 할 때는 주의해야 한다.»

좋은 예는 다음과 같다.

«Controller에서 JPA Entity를 직접 반환하지 않는다. API 응답은 Response DTO로 변환한다.»

실패의 역사는 길게 남길 필요가 없다. 다음 행동을 바꿀 수 있는 규칙만 남기면 된다.

넷째, 반복 작업은 규칙보다 스킬로 분리한다

표현 하나까지 모든 절차를 "CLAUDE.md"에 넣으면 핵심 문서가 금방 비대해진다. 코드 리뷰, 장애 분석, API 추가, 데이터베이스 마이그레이션처럼 반복되는 작업은 별도의 작업 지침이나 스킬로 분리하는 편이 낫다.

예를 들어 API 개발 절차를 다음과 같이 독립된 문서로 관리할 수 있다.

- 기존 유사 API 탐색
- 요청·응답 모델 정의
- 권한과 입력값 검증
- 서비스 로직 구현
- 단위·통합 테스트 작성
- API 명세 갱신
- 전체 검증 명령 실행

최상위 "CLAUDE.md"에는 해당 절차의 존재와 호출 조건만 기록한다.

이렇게 하면 공통 컨텍스트에는 원칙만 남고, 구체적인 작업 절차는 필요할 때 불러올 수 있다. 사람의 업무에서도 회사 규정과 작업 매뉴얼을 분리하는 것과 같다.

다섯째, 규칙 문서도 코드 리뷰 대상으로 삼는다

"CLAUDE.md"를 한 사람이 임의로 수정하면 개인의 취향이 프로젝트 전체 규칙으로 굳어질 수 있다. 따라서 코드와 마찬가지로 변경 이력을 남기고 리뷰하는 것이 좋다.

규칙을 변경하는 PR에서는 다음을 확인해야 한다.

- 어떤 실패나 필요 때문에 추가하는가
- 프로젝트 전체에 적용해야 하는가
- 기존 규칙과 중복되거나 충돌하지 않는가
- 더 짧고 명확하게 표현할 수 없는가
- 문서가 아니라 코드, 테스트, 린터로 강제할 수 없는가

특히 마지막 질문이 중요하다. 반드시 지켜야 하는 규칙이라면 자연어 지침에만 의존해서는 안 된다.

포맷은 formatter로, 코드 규칙은 linter와 정적 분석으로, 아키텍처 경계는 ArchUnit과 같은 테스트로, 빌드 가능 여부는 CI로 검증하는 편이 더 확실하다.

자연어 문서는 판단의 방향을 알려주고, 자동화된 도구는 위반을 차단해야 한다.

여섯째, 정기적으로 삭제한다

규칙 문서에는 추가하는 사람은 많지만 삭제하는 사람은 적다. 그 결과 프로젝트에서는 이미 없어진 구조와 종료된 정책이 계속 살아남는다.

따라서 정기적으로 다음 항목을 점검해야 한다.

- 더 이상 존재하지 않는 경로와 명령어
- 현재 기술 스택과 맞지 않는 설명
- 서로 중복되는 규칙
- 반대되는 지침을 가진 규칙
- 코드나 자동화로 이미 해결된 규칙
- 특정 시점에만 필요했던 임시 규칙
- 실제로 AI의 행동에 영향을 주지 않는 장황한 설명

문서를 줄이는 일은 정보 손실이 아니라 신뢰도 회복이다. 모든 규칙이 현재 유효하다는 확신이 있어야 개발자와 AI 모두 문서를 믿고 사용할 수 있다.

CLAUDE.md 관리도 하나의 피드백 루프다

체계적인 관리 과정은 다음과 같은 순환 구조를 가진다.

1. AI 작업에서 반복되는 실패를 관찰한다.
2. 실패 원인이 지침 부족인지 구조적 문제인지 분석한다.
3. 필요한 경우 가장 짧은 규칙을 추가한다.
4. 전체 규칙이 아니라 적절한 범위에 배치한다.
5. 코드 리뷰를 통해 중복과 부작용을 확인한다.
6. 테스트와 자동화로 옮길 수 있는 규칙은 이전한다.
7. 주기적으로 오래된 내용을 삭제한다.

즉, "CLAUDE.md"는 한 번 잘 작성하고 끝내는 문서가 아니다. 프로젝트와 AI의 작업 결과를 보면서 계속 수정하는 운영 자산이다.

규칙이 많다고 통제력이 높아지는 것은 아니다

AI 코딩 환경에서 좋은 지침은 모든 상황을 설명하는 백과사전이 아니다. 프로젝트의 중요한 맥락을 빠르게 전달하고, 잘못된 선택을 줄이며, 필요한 상세 지식으로 연결하는 안내 체계다.

최상위 문서는 짧게 유지하고, 세부 규칙은 조건에 따라 분리한다. 반복되는 실패만 기록하고, 작업 절차는 스킬이나 별도 문서로 위임한다. 강제할 수 있는 규칙은 테스트와 CI로 옮기며, 더 이상 필요하지 않은 내용은 삭제한다.

이 원칙은 Claude Code의 "CLAUDE.md"에만 해당하지 않는다. Codex의 "AGENTS.md"나 다른 AI 개발 도구의 프로젝트 지침도 마찬가지다.

AI에게 더 많은 내용을 제공하는 것이 항상 더 좋은 결과를 만들지는 않는다. 중요한 것은 필요한 순간에 정확한 정보를 제공하는 것이다.

좋은 규칙 문서는 계속 커지는 문서가 아니다. 프로젝트가 변할수록 함께 정리되고, 더 짧고 정확해지는 문서다.

규칙을 추가하는 것은 쉽다. 그러나 신뢰할 수 있는 규칙만 남기는 것이 진짜 관리다.

LIST


개발자의 역할을 단순하게 정의하면 요구사항을 코드로 구현하는 사람이다. 그러나 조직이 개발자에게 기대하는 궁극적인 결과는 코드 그 자체가 아니다. 조직은 기술을 통해 비용을 줄이고, 매출을 만들고, 위험을 통제하며, 고객에게 더 나은 서비스를 제공하려 한다.

따라서 개발자의 공헌은 “얼마나 많은 코드를 작성했는가”가 아니라 “기술을 통해 조직의 어떤 문제를 해결했는가”로 평가해야 한다.

1. 구현하기 전에 목적을 이해한다

개발자는 업무를 받으면 곧바로 구현 방법부터 생각하기 쉽다. 어떤 프레임워크를 사용할지, 데이터베이스를 어떻게 설계할지, API를 어떤 구조로 나눌지 고민한다. 모두 필요한 일이지만 그보다 먼저 확인해야 할 것은 해당 기능이 필요한 이유다.

같은 기능도 목적에 따라 설계가 달라진다. 관리자 화면을 만드는 이유가 단순 조회인지, 고객 문의 시간을 줄이기 위한 것인지, 감사 대응을 위한 것인지에 따라 필요한 데이터와 권한, 로그의 수준이 달라진다.

개발자는 다음을 질문할 수 있어야 한다.

- 이 기능으로 해결하려는 문제는 무엇인가?
- 누가 사용하며, 어떤 행동이 달라지는가?
- 구현하지 않으면 어떤 손실이나 위험이 발생하는가?
- 성공 여부를 무엇으로 판단할 수 있는가?

요구사항을 그대로 구현하는 개발자는 작업을 완료한다. 요구사항의 목적을 이해하는 개발자는 조직의 문제를 해결한다.

2. 코드보다 결과를 중심으로 판단한다

좋은 코드가 언제나 좋은 성과를 의미하지는 않는다. 구조가 정교하고 최신 기술을 사용했더라도 고객이 사용하지 않거나 운영비가 지나치게 높다면 조직에는 좋은 결과가 아니다.

반대로 화려하지 않은 기술을 사용했더라도 장애를 줄이고, 반복 업무를 자동화하고, 고객 이탈을 막았다면 명확한 공헌이다.

개발자는 기술적 결과를 조직의 언어로 번역할 필요가 있다.

- 응답 속도를 개선했다 → 고객의 대기시간과 이탈 가능성을 줄였다.
- 배포 자동화를 구축했다 → 배포시간과 작업자의 실수 가능성을 줄였다.
- 테스트 코드를 추가했다 → 변경에 따른 회귀 오류와 검증 비용을 낮췄다.
- 감사 로그를 구현했다 → 장애와 분쟁 발생 시 원인을 추적할 수 있게 했다.
- 정산 로직의 멱등성을 보장했다 → 중복 처리로 인한 금전 오류를 방지했다.

기술적 완성도는 중요하다. 그러나 조직에서의 공헌은 그 기술이 만들어 낸 변화로 설명될 때 비로소 드러난다.

3. 적정한 기술을 선택한다

개발자는 새로운 기술에 관심을 가져야 하지만, 모든 프로젝트에 가장 최신의 기술을 적용할 필요는 없다. 기술 선택의 기준은 개인의 흥미가 아니라 조직의 상황이어야 한다.

사용자가 수백 명인 초기 서비스에 복잡한 분산 시스템을 도입하면 확장성보다 운영 부담이 커질 수 있다. 개발자가 적은 조직에서 지나치게 많은 마이크로서비스를 운영하면 장애 지점과 배포 대상만 늘어날 수 있다. 반대로 거래량이 많고 장애의 파급력이 큰 시스템을 단순하게만 구성하면 향후 더 큰 비용을 치르게 된다.

좋은 개발자는 가장 복잡한 구조를 설계하는 사람이 아니라, 현재의 문제와 미래의 변화에 적합한 복잡도를 선택하는 사람이다.

이를 위해서는 다음 조건을 함께 고려해야 한다.

- 현재 사용자와 트래픽 규모
- 조직의 개발 및 운영 인력
- 장애가 사업에 미치는 영향
- 예상되는 기능 변화와 확장 속도
- 구축비용과 장기 유지보수 비용
- 팀원이 해당 기술을 이해하고 운영할 수 있는지

기술적 야심보다 지속 가능한 선택이 조직에는 더 큰 공헌이 될 수 있다.

4. 품질을 개인의 자존심이 아니라 조직의 자산으로 만든다

품질은 단순히 코드가 깔끔하다는 의미가 아니다. 다른 사람이 이해하고 수정할 수 있으며, 변경했을 때 문제가 발생하지 않고, 장애가 발생하면 원인을 확인할 수 있는 상태를 뜻한다.

한 사람만 이해할 수 있는 코드는 그 개발자가 뛰어나다는 증거가 아니라 조직이 특정 개인에게 의존하고 있다는 신호일 수 있다.

조직에 남는 품질을 만들기 위해서는 다음과 같은 장치가 필요하다.

- 의도가 드러나는 코드와 일관된 구조
- 핵심 규칙을 보호하는 자동화 테스트
- 중요한 의사결정과 제약조건을 설명하는 문서
- 장애 원인을 확인할 수 있는 로그와 모니터링
- 반복 가능한 빌드와 배포 절차
- 복구할 수 있는 백업과 장애 대응 체계

개발자가 퇴사하거나 담당 업무가 변경되어도 시스템이 유지될 수 있어야 한다. 개인의 지식을 팀의 자산으로 전환하는 것이 장기적인 공헌이다.

5. 동료의 생산성을 높인다

개발자의 생산성은 자신이 처리한 업무량만으로 측정할 수 없다. 동료의 업무가 더 빨라지고 안전해지도록 만드는 것도 중요한 공헌이다.

명확한 코드 리뷰, 재사용 가능한 공통 모듈, 개발 환경 자동화, 장애 대응 문서, 기술 공유는 당장 기능 하나를 추가하는 것보다 눈에 덜 띌 수 있다. 하지만 이런 활동은 팀 전체의 반복적인 비용을 줄인다.

특히 숙련된 개발자일수록 자신의 개인 생산성보다 조직의 생산성을 높이는 방향으로 역할을 확장해야 한다.

- 반복되는 질문은 문서로 만든다.
- 반복되는 작업은 자동화한다.
- 반복되는 실수는 테스트와 검증 절차로 차단한다.
- 복잡한 지식은 동료가 이해할 수 있는 언어로 설명한다.
- 문제를 발견했을 때 사람보다 구조와 프로세스를 개선한다.

한 명이 빠르게 일하는 것보다 팀 전체가 안정적으로 일하게 만드는 것이 더 큰 성과가 될 수 있다.

6. 문제를 조기에 드러낸다

조직에서는 문제가 발생한 사실보다 문제가 늦게 공유된 것이 더 큰 피해를 만들 때가 많다. 일정 지연, 기술적 제약, 데이터 오류 가능성, 보안 위험을 숨기면 의사결정권자는 잘못된 정보를 바탕으로 판단하게 된다.

책임감 있는 개발자는 모든 문제를 혼자 해결한 뒤 보고하려 하지 않는다. 문제의 영향과 선택지를 정리하여 적절한 시점에 공유한다.

단순히 “어렵습니다”라고 말하는 것으로는 충분하지 않다. 다음과 같이 전달해야 한다.

- 현재 확인된 문제
- 발생 원인 또는 추정 원인
- 일정과 서비스에 미치는 영향
- 가능한 대응안과 각각의 비용
- 추천하는 방안과 그 이유

문제를 조기에 알리는 것은 무능을 드러내는 행동이 아니다. 조직이 대응할 시간을 확보하게 하는 전문적인 위험관리다.

7. 사업과 고객의 언어를 배운다

개발자가 모든 사업 영역의 전문가가 될 필요는 없다. 그러나 자신이 만드는 시스템이 어떤 방식으로 수익, 비용, 고객 경험, 규제와 연결되는지는 이해해야 한다.

이커머스 개발자라면 주문과 결제뿐 아니라 취소, 환불, 정산에서 어떤 분쟁이 발생하는지 알아야 한다. 공공 프로젝트의 개발자라면 기능 구현뿐 아니라 접근권한, 개인정보, 감사, 장애 보고가 왜 중요한지 이해해야 한다. 금융 시스템 개발자라면 정합성과 추적 가능성이 단순한 기술적 취향이 아니라 사업의 신뢰를 지키는 조건이라는 점을 알아야 한다.

도메인을 이해할수록 개발자는 지시받은 기능만 구현하는 사람에서, 문제를 함께 정의하고 대안을 제시하는 사람으로 성장한다.

8. 기여를 측정하고 설명한다

개발자의 업무는 눈에 보이지 않는 경우가 많다. 장애를 예방한 일, 기술부채를 줄인 일, 데이터 오류 가능성을 제거한 일은 문제가 발생하지 않았기 때문에 오히려 성과가 드러나지 않는다.

따라서 개발자는 자신의 기여를 가능한 범위에서 기록하고 측정해야 한다.

- 배포 소요시간이 얼마나 줄었는가
- 장애 건수와 복구시간이 어떻게 달라졌는가
- 자동화로 몇 시간의 반복 업무를 제거했는가
- 응답속도와 처리량이 얼마나 개선됐는가
- 테스트를 통해 어떤 핵심 위험을 방지했는가
- 고객 문의나 운영자의 수작업이 얼마나 감소했는가

성과를 과장해서는 안 되지만, 설명하지 않는 것도 바람직하지 않다. 조직은 보이지 않는 가치를 평가하기 어렵기 때문이다. 기술적 활동과 사업적 효과 사이의 연결을 보여주는 것도 개발자의 책임이다.

공헌은 조직의 목적과 기술을 연결하는 일이다

개발자는 코드를 통해 일하지만, 코드만을 위해 일하는 사람은 아니다. 조직의 문제를 정확히 이해하고, 적절한 기술을 선택하고, 안정적인 결과를 만들며, 그 결과가 지속될 수 있도록 팀의 역량과 시스템을 개선하는 사람이다.

개발자가 조직에 공헌하기 위해서는 세 가지 질문을 계속해야 한다.

1. 지금 해결해야 할 가장 중요한 문제는 무엇인가?
2. 내가 선택한 기술과 방식이 조직의 비용과 위험을 줄이는가?
3. 내가 만든 결과가 개인의 작업으로 끝나지 않고 조직의 자산으로 남는가?

결국 뛰어난 개발자는 많은 기능을 만든 사람만을 의미하지 않는다. 조직이 올바른 문제를 더 빠르고 안전하게 해결하도록 만든 사람이다.

코드는 개발자의 산출물이지만, 공헌은 코드가 조직에 만들어 낸 변화다.

LIST

 많은 이들이 AI가 코드를 대신 짜주는 시대에 개발자의 종말을 이야기합니다. 하지만 9년 차 이상의 시니어라면 직감하고 있습니다. 엔진이 강력해질수록 핸들의 유격은 더 정교해야 한다는 것을요. 고대 신화 속 자신의 꼬리를 삼키며 무한히 순환하는 우로보로스(Ouroboros)는, 이제 우리에게 단순한 상징을 넘어 'AI 오케스트레이션의 최종 진화 모델'을 제시합니다.

1. 뱀의 머리: "명세(Specification)는 가장 고차원적인 코딩이다"

우로보로스 프로젝트의 철학은 명확합니다. "프롬프팅을 멈추고, 스펙을 써라." 자바 시니어라면 익숙한 인터페이스 기반 설계(Interface-driven Design)와 맞닿아 있습니다. 우리가 Interface를 정의할 때 구현체에 관심이 없듯, AI 에이전트에게 필요한 것은 "어떻게(How)"가 아니라, 완벽하게 모호성이 제거된 "무엇을(What)"에 대한 명세입니다.

  • 모호성 점수(Ambiguity Score)의 관리: 훌륭한 아키텍트는 AI에게 명령하기 전, 스스로 질문합니다. "이 명세에 해석의 여지가 남아 있는가?"
  • Seed YAML의 힘: 우로보로스에서 사용하는 Seed 명세처럼, 도메인 주도 설계(DDD)의 유비쿼터스 언어를 활용해 '비즈니스 요구사항'을 '기계적 명세'로 치환하는 능력이 AI 시대 시니어의 첫 번째 덕목입니다.

2. 뱀의 몸통: "오케스트레이션은 검증의 연속이다"

명세가 정확하다면, 그다음은 수많은 에이전트를 조율(Orchestration)하는 단계입니다. 단순히 코드를 생성하는 것이 아니라, 생성된 코드가 명세를 배반하지 않는지 끝없이 순환하며 확인하는 과정이죠.

우로보로스가 제안하는 3단계 검증 루프는 자바의 견고한 인프라와 결합할 때 폭발적인 ROI를 냅니다.

  • Mechanical Gate: 자바의 강력한 Type SystemLinter가 문법적 무결성을 1차 검증합니다.
  • Semantic Gate: Property-based Testing(Fast Check)을 통해 로직의 엣지 케이스를 무차별적으로 공격하여 논리적 결함을 찾아냅니다.
  • Consensus Gate: 여러 LLM 모델이 서로의 코드를 리뷰하며 '최선의 합의점'을 도출합니다.

이 과정에서 아키텍트는 단순 코더가 아닌, '검증 파이프라인의 설계자'가 됩니다.

3. 뱀의 꼬리: "자기 참조적 진화(Self-Correction)"

우로보로스의 뱀이 제 꼬리를 먹으며 몸집을 키우듯, 좋은 AI 아키텍처는 [에러 피드백 -> 명세 수정 -> 코드 재생성]의 루프가 인간의 개입 없이 돌아가야 합니다.

에러가 발생했을 때 사람이 코드를 직접 고치는 순간, 그 프로젝트의 AI ROI는 멈춥니다. 대신 "왜 명세가 이 에러를 막지 못했는가?"를 고민하고 명세를 수정해야 합니다. 꼬리(에러/결과물)를 머리(명세)로 다시 보내는 이 순환 루프가 완성될 때, 비로소 시스템은 스스로 진화하는 '살아있는 유기체'가 됩니다.


맺으며: "명세는 영원하고, 코드는 찰나다"

자바 25가 나오고 버추얼 스레드가 세상을 바꿔도, '도메인의 본질을 꿰뚫는 명세'의 가치는 변하지 않습니다. AI라는 거대한 파도를 타는 유능한 서퍼는 파도를 통제하려 하지 않습니다. 대신 자신이 가고자 하는 목적지(Specification)를 명확히 하고, 보드(Infra)를 견고하게 다질 뿐입니다.

검증 인프라에 투자하고 명세를 예술의 경지로 끌어올리는 프로젝트만이, 이 우로보로스의 순환 루프 안에서 무한히 성장할 수 있습니다.

"당신은 오늘, AI에게 코드를 짜라고 명령했습니까? 아니면 완벽한 명세를 선물했습니까?"

 

 

LIST

1. 10년 전이면 고수의 글, 지금은 딸각

10년 전 기술 블로그를 떠올려 본다. 커널 패닉을 며칠 밤새 추적한 흔적, GC 로그를 한 줄씩 뜯어 본 기록, 분산 트랜잭션의 엣지 케이스를 직접 재현해 본 이야기. 그런 글에는 손때가 묻어 있었다. 글쓴이가 그 깊이까지 닿았다는 증명이 글 자체였다. 그런데 지금은 같은 수준의 문장이 Claude 한 번 "딸각"이면 그럴듯하게 쏟아진다. 정보의 양은 폭증했는데 진짜 신호를 가려내기는 더 어려워졌다. 자신감 넘치는 어조와 잘 정돈된 목차, 매끄러운 코드 블록—그 안이 비어 있어도 외형은 완벽하다. 빈수레일수록 더 요란하게 굴러간다. 과거에는 깊이가 형식을 만들었다면, 지금은 형식이 깊이를 흉내 낸다. 글의 모양만 보고 사람을 판단하던 기준이 더 이상 작동하지 않는다.

https://www.youtube.com/shorts/qtxUtMhDqYI

 

2. 고수의 재정의 — 한 명의 천재에서 여러 전문가의 오케스트레이터로

그렇다면 고수란 무엇인가. 과거의 고수는 단수(單數)였다. 한 분야를 깊이 파고들어 그 영역에서는 누구보다 멀리 본 사람. 그러나 미래의 고수는 복수(複數)를 다룬다. 과거의 고수 여럿을 agent로 부리는 사람이다. 이는 단순히 프롬프트를 잘 쓰는 능력과 다르다. 각 영역에서 어느 수준의 전문성이 필요한지 판별하고, AI가 내놓은 결과물이 진짜인지 그럴듯한 모조품인지 검증하고, 도메인 판단력으로 여러 산출물을 통합해 의사결정하는 능력이다. Claude Code 한두 개 띄워 본 사람과 멀티 에이전트를 조직처럼 운영해 본 사람의 격차는, 10년 전 IDE를 막 쓰는 신입과 디버거를 손발처럼 다루던 시니어의 격차와 닮았다. 도구가 평준화될수록 도구를 부리는 판단력이 유일한 차별화 요소로 남는다. 고수의 자리는 사라진 것이 아니라 한 단계 위로 이동했다.

3. 중간 관리자 개발자의 위치 애매성

이 지각 변동에서 가장 위태로운 자리가 4~7년차 중간 관리자급 개발자다. 위로는 시니어 아키텍트의 시야와 통찰에 아직 미치지 못하고, 아래로는 AI가 주니어급 코딩을 빠른 속도로 흡수하고 있다. 정확히 말하면 "어중간한 깊이의 작업"이 가장 먼저 자동화되는 영역이다. CRUD 한 세트, 평이한 리팩터링, 무난한 API 설계, 매뉴얼 수준의 트러블슈팅. 이 일들로 연봉의 정당성을 만들어 왔다면, 이제 그 정당성이 흔들린다. 회사 입장에서 어중간한 미들 레벨 다섯 명보다 시니어 한 명 + AI 도구 잘 쓰는 주니어 두 명이 더 효율적인 시대가 오고 있다. 미들 개발자의 진짜 위기는 일자리가 사라지는 것이 아니라, 자기 포지션이 무엇인지 스스로도 설명하지 못하는 상태다. "두루두루 합니다"는 더 이상 강점이 아니라 약점에 가깝다.

4. 두 갈래 길 — 깊이 있는 개발자 혹은 전천후 오케스트레이터

이 애매한 지대에서 빠져나가는 길은 두 가지다. 하나는 깊이로 가는 길. 분산 시스템, 보안, 컴파일러, 도메인 모델링—AI가 표면을 흉내 낼 수는 있어도 진짜 엣지 케이스에서 책임을 질 수는 없는 영역이 있다. 그곳에서 한 점을 깊게 뚫고 들어간 사람은 AI 시대에도 대체되지 않는다. 다른 하나는 전천후 오케스트레이터로 가는 길. 모든 코드를 직접 짜지는 못해도 전체 아키텍처를 머릿속에 그릴 수 있고, 도메인 판단력과 시스템 사고로 여러 agent의 산출물을 통합해 책임지는 사람. 본인이 모든 깊이를 갖지 않아도 어느 깊이가 필요한지 판단할 수 있다면 그 자체가 고수다. 두 길의 공통점은 결국 판단력이다. 깊이는 한 점에 집중된 판단력이고, 전천후는 넓은 영역에 분산된 통합 판단력이다. 가장 위험한 선택은 어느 쪽도 아닌 중간에 머무르는 것. 빈수레의 요란함을 흉내 내는 데 시간을 쓰지 말고, 어느 방향이든 무게가 실리는 수레를 만들어야 한다. 10년 후의 고수는 지금 그 무게를 쌓고 있는 사람이다.

LIST

1일차: 클라우드 및 Docker 개념 학습

클라우드 컴퓨팅이 필요한 이유

  1. 비용절감
    • 하드웨어 및 소프트웨어를 구입하고 데이터 센터 설치 및 운영 비용을 줄일 수 있습니다.
    • 서버 랙, 전원 및 냉각에 사용되는 전기료, 인프라 관리를 위한 IT 전문가 인건비 등도 절약됩니다.
  2. 속도향상
    • 주문형 셀프서비스로 제공되기 때문에 다양한 컴퓨팅 리소스를 몇번의 마우스 클릭으로 설정할 수 있습니다.
  3. 확장성
    • 필요할 때 적절하게 스토리지, 네트워크 대역폭 등 IT 자원을 확장할 수 있습니다.
  4. 생산성
    • 일반적인 온사이트 데이터센터에서는 하드웨어 설치, 소프트웨어 패치 및 시간이 오래걸리는 IT 운영 작업이 필요하지만, 클라우드 컴퓨팅을 사용하면 이 모든게 불필요해지므로 IT 팀은 인프라 외에 비즈니스에 집중할 수 있습니다.

Docker Compose vs Kubernetes

 

 

 

2일차: Docker 이미지 생성 실습

Docker 이미지 주요 명령어

# 현재 시스템에서 사용 가능한 모든 Docker 이미지 목록 확인
$ docker images

# 특정 이미지 다운로드
$ docker pull <이미지 이름>

# 이미지 정보 확인
$ docker inspect <이미지 이름>

# 이미지 삭제
$ docker rmi <이미지 이름>

 

 불필요한 파일 줄이기 (.dockerignore 활용)

  • .**dockerignore** 파일은 Docker 빌드 과정에서 제외할 파일과 디렉터리를 지정하는 역할을 합니다.
  • Docker는 docker build를 실행할 때, 현재 디렉터리(.)의 모든 파일을 Docker 컨텍스트에 복사합니다.
  • 이때 .dockerignore 파일을 사용하면 불필요한 파일을 제외하여 빌드 속도를 향상시키고, 보안성을 강화할 수 있습니다.
  • .dockerignore 파일이 필요한 이유
    1. 빌드 속도 향상 → 불필요한 파일을 제외하여 Docker 컨텍스트 크기를 줄임
    2. 보안 강화 → 환경 변수 파일(.env), 인증 정보(.git, node_modules) 등이 이미지에 포함되지 않도록 방지
    3. 이미지 크기 최소화 → 최적화된 Docker 이미지를 생성하여 배포 용량을 줄임

 

 

레이어 캐싱 (Layer Caching)

도커의 가장 중요한 특징 중 하나는 레이어 캐싱입니다. 이미지를 빌드할 때, Dockerfile의 각 명령어를 순서대로 실행하며 레이어를 만듭니다. 이때 명령어의 내용이 이전 빌드와 동일하다면, 새로운 레이어를 만들지 않고 기존에 만들어 두었던 레이어를 재사용합니다. 이를 '캐시(Cache)를 사용한다'고 말합니다.

  • 빌드 속도 향상: 변경되지 않은 부분은 다시 실행할 필요가 없으므로 이미지 빌드 시간이 크게 단축됩니다.
  • 효율적인 저장 공간 사용: 여러 이미지가 동일한 레이어를 공유하는 경우, 해당 레이어는 디스크에 한 번만 저장됩니다. 예를 들어, ubuntu:22.04를 기반으로 하는 10개의 다른 이미지가 있더라도, 우분투 베이스 레이어는 단 한 번만 저장됩니다.

Dockerfile을 작성할 때, 자주 변경되지 않는 내용은 앞부분에, 자주 변경되는 내용은 뒷부분에 배치하는 것이 레이어 캐싱을 효율적으로 활용하는 좋은 방법입니다. 소스 코드는 자주 바뀌므로 COPY . . 명령어는 보통 뒷부분에 위치시킵니다.

 

 

Docker 이미지 Versioning 과 Semantic Versioning

Docker 이미지 versioning

  • Docker 이미지는 태그(Tag) 를 사용하여 버전 관리를 수행합니다.
  • latest 태그를 사용할 수도 있지만, 명확한 버전 관리를 위해 시멘틱 버저닝(Semantic Versioning) 을 사용하는 것이 좋습니다.

시멘틱 버저닝(Semantic Versioning)이란

  • 시멘틱 버저닝(SemVer)은 주버전(Major), 부버전(Minor), 패치버전(Patch) 으로 구성됩니다.
    • 형식: MAJOR.MINOR.PATCH
    • MAJOR (주 버전): 호환성이 깨지는 큰 변경이 있을 때 올립니다.
      • 예: 1.5.2 → 2.0.0
      • 기존의 사용법이나 기능이 완전히 바뀌어, 이 버전을 사용하려면 내 코드도 수정해야 할 수 있다는 강력한 신호입니다. 마치 자동차 엔진이 바뀌는 것과 같습니다.
    • MINOR (부 버전): 기존 버전과 호환되면서 새로운 기능이 추가될 때 올립니다.
      • 예: 1.9.2 → 1.10.0
      • 기존 기능은 그대로 잘 작동하며, 새로운 옵션이나 기능이 추가된 경우입니다. 자동차에 새로운 내비게이션 시스템이 추가된 것과 같습니다. 기존 운전 방식에는 영향이 없습니다.
    • PATCH (패치 버전): 기존 버전과 호환되는 작은 버그 수정이 있을 때 올립니다.
      • 예: 1.5.2 → 1.5.3
      • 기능 변화 없이, 기존 기능의 오류를 바로잡은 경우입니다. 자동차의 사소한 부품 결함을 수리한 것과 같습니다.

버전 형식 설명

1.0.0 초기 릴리즈 버전
1.1.0 기능(feature) 추가 (하위 호환 유지)
1.1.1 버그 수정 (패치 버전)
2.0.0 기존 기능을 변경하거나 호환되지 않는 수정

Docker에서 Semantic Versioning 적용 예제

  • Dockerfile을 사용하여 시멘틱 버저닝을 적용할 때, 태그를 명확하게 지정합니다.
  • 시멘틱 버저닝을 활용하면 배포 관리가 쉬워지고, 특정 버전으로 롤백이 용이해집니다.
# 1.0.0 버전으로 빌드
docker build --build-arg PROFILE=dev -t spring-docker:1.0.0 .

# 1.0.1 버전으로 빌드 (패치 업데이트 포함)
docker build --build-arg PROFILE=dev -t spring-docker:1.0.1 .

 

3일차: Docker 네트워크 및 데이터 관리

 

  • Docker 네트워크 개념 및 종류 이해
  • 컨테이너 간 네트워크 설정 및 통신 실습
  • Docker 볼륨을 활용한 데이터 관리 실습
  • 보안 및 성능 최적화 기법 적용

1.3 컨테이너 간 기본 네트워크 통신 확인

 

# 현재 Docker 네트워크 확인
docker network ls

# 사용자 지정 브리지 네트워크 생성
# sleep infinity : 컨테이너 내부에서 종료되지 않는 프로세스를 실행하여 유지
docker run -d --name svc1 --network bridge busybox sleep infinity
docker run -d --name svc2 --network bridge busybox sleep infinity

docker exec -it svc1 ping svc2

-------------------------------------------------
ping: bad address 'svc2'

 

  • 기본 bridge 네트워크에서 컨테이너 간 직접 통신이 안 되는 이유
    1. 기본 bridge 네트워크에서는 컨테이너 이름 기반 DNS 해석이 지원되지 않음
    2. IP 주소를 직접 사용해야 하지만, IP는 컨테이너 재시작 시 변경될 수 있어 관리가 어려움
    3. 사용자 정의 네트워크를 만들면 컨테이너 이름을 통해 자동으로 통신 가능
  • 해결책
    • 기본 bridge 네트워크를 사용할 경우, 컨테이너 간 통신 시 IP 주소를 사용해야 합니다.
    • 컨테이너 이름으로 통신하려면 사용자 정의 네트워크(docker network create <네트워크명>)를 사용해야 합니다.

 

4일차: Docker 를 활용한 Log 관리

 

 

1. 표준 로그 스트림 및 출력 관리

1.1 표준 에러 스트림과 표준 출력 스트림

  1. 컨테이너 실행 및 로그 생성
    • sh -c 명령어를 통해 stdout과 stderr로 각각 로그를 출력합니다.
    • echo "stderr log" >&2는 표준 에러로 메시지를 보냅니다.
  2. docker run --name log-test -d alpine sh -c ' for i in $(seq 1 10); do echo "$i: stdout log"; echo "$i: stderr log" >&2; done'
  3. 로그 확인하기
    • 실행 중인 컨테이너의 모든 로그 출력
    docker logs log-test
    
    • 최근 5개의 로그 출력
    docker logs --tail 5 log-test
    
  1. 실시간 로그 스트림 확인
    • f 옵션을 사용하면 컨테이너의 실시간 로그 스트림을 확인할 수 있습니다.
  2. docker logs -f log-test
  1. 로그 출력 시 타임스탬프 포함
    • 각 로그에 타임스탬프를 추가하여 출력합니다.
    docker logs --timestamps log-test
    
  2. 실습 종료 및 컨테이너 정리

docker stop log-test && docker rm log-test

2. 컨테이너 로그 수집 및 관리

2.1 syslog 드라이버를 이용해 로그 전송 설정하기

Docker 컨테이너의 로그를 외부 syslog 서버로 전송 설정합니다.

docker run --log-driver=syslog --log-opt syslog-address=udp://localhost:514 -d ubuntu echo "Logging to syslog"

2.2 로그 로테이션 설정

Docker 컨테이너의 로그 드라이버는 기본적으로 json-file을 사용하며, 이 드라이버는 로그 로테이션 옵션이 있습니다. max-size와 max-file 옵션을 사용하여 로그 파일의 최대 크기와 보관할 로그 파일의 수를 제어할 수 있습니다.

# Docker 컨테이너를 로그 로테이션 설정과 함께 실행
docker run -d \\
  --name=log-rotation-test \\
  --log-opt max-size=10m \\
  --log-opt max-file=3 \\
  ubuntu bash -c 'while true; do echo "$(date) - Writing logs to fill up space..."; sleep 1; done'
  
# 로그 확인
docker logs -f log-rotation-test

# 종료 
docker stop log-rotation-test
docker rm log-rotation-test

2.3 로그 모니터링 실습

2.3.1 로그 모니터링 스크립트 생성

#!/bin/bash

# 첫 번째 인자로 컨테이너 이름을 받음
CONTAINER_NAME=$1
# 로그 라인 수 임계값 설정
THRESHOLD=1000

# 무한 루프로 계속 모니터링
while true; do
    # 현재 컨테이너의 로그 라인 수를 계산
    LOG_SIZE=$(docker logs $CONTAINER_NAME 2>&1 | wc -l)
    
    # 현재 시간 저장
    CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
    
    # 로그 크기가 임계값을 초과하면 경고
    if [ $LOG_SIZE -gt $THRESHOLD ]; then
        echo "[$CURRENT_TIME] Warning: Log size ($LOG_SIZE lines) exceeds threshold ($THRESHOLD) for $CONTAINER_NAME"
        
        # 선택적: 관리자에게 이메일 발송
        # mail -s "Docker Log Alert" admin@example.com <<< "Container $CONTAINER_NAME log size exceeds threshold"
    else
        echo "[$CURRENT_TIME] Log size ($LOG_SIZE lines) is normal for $CONTAINER_NAME"
    fi
    
    # 1초 대기
    sleep 1
done

2.3.2 실습용 로그 생성 컨테이너 생성

# 로그를 지속적으로 생성하는 테스트용 Dockerfile
cat > Dockerfile << 'EOF'
FROM ubuntu:20.04

RUN apt-get update && apt-get install -y stress

CMD while true; do date; echo "Test log message"; sleep 1; done
EOF

# 이미지 빌드
docker build -t log-generator .

# 컨테이너 실행
docker run -d --name log-test1 log-generator

2.3.3 모니터링 실행 및 테스트

# 모니터링 스크립트 실행
./monitor-logs.sh log-test1

# 별도 터미널에서 로그 확인
docker logs -f log-test1

2.3.4 모니터링 설정 (확장편)

#!/bin/bash

CONTAINER_NAME=$1
THRESHOLD=1000
LOG_FILE="container_logs.txt"
ALERT_EMAIL="admin@example.com"

monitor_logs() {
    LOG_SIZE=$(docker logs $CONTAINER_NAME 2>&1 | wc -l)
    CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
    DISK_USAGE=$(docker inspect $CONTAINER_NAME --format='{{.LogPath}}' | xargs du -h 2>/dev/null | cut -f1)
    
    echo "[$CURRENT_TIME] Container: $CONTAINER_NAME" >> $LOG_FILE
    echo "Log lines: $LOG_SIZE" >> $LOG_FILE
    echo "Disk usage: $DISK_USAGE" >> $LOG_FILE
    
    if [ $LOG_SIZE -gt $THRESHOLD ]; then
        MESSAGE="Warning: Container $CONTAINER_NAME has exceeded log threshold
        Current size: $LOG_SIZE lines
        Disk usage: $DISK_USAGE
        Time: $CURRENT_TIME"
        
        echo "$MESSAGE" >> $LOG_FILE
        echo "$MESSAGE" | mail -s "Docker Log Alert" $ALERT_EMAIL
        
        # 로그 백업 및 클리어
        docker logs $CONTAINER_NAME > "${CONTAINER_NAME}_logs_${CURRENT_TIME}.txt"
        docker container restart $CONTAINER_NAME
    fi
}

# 메인 모니터링 루프
while true; do
    if docker ps | grep -q $CONTAINER_NAME; then
        monitor_logs
    else
        echo "[$CURRENT_TIME] Container $CONTAINER_NAME is not running!" >> $LOG_FILE
    fi
    sleep 60
done
  • 로그용 Dockerfile 및 로그 생성 스크립트 작성
# 로그 생성용 Dockerfile 생성
cat > Dockerfile << 'EOF'
FROM ubuntu:20.04

# 스트레스 테스트 도구 설치
RUN apt-get update && apt-get install -y stress

# 로그 생성 스크립트
COPY generate-logs.sh /
RUN chmod +x /generate-logs.sh

CMD ["/generate-logs.sh"]
EOF

# 로그 생성 스크립트 작성
cat > generate-logs.sh << 'EOF'
#!/bin/bash

while true; do
    date
    echo "Application log: Process running"
    echo "System status: OK"
    echo "Memory usage: $(free -m | grep Mem | awk '{print $3}') MB"
    sleep 1
done
EOF

# 이미지 빌드
docker build -t log-generator .

# 컨테이너 실행
docker run -d --name test-container log-generator
  • 모니터링 실행 및 테스트
# 터미널 1: 모니터링 스크립트 실행
sh advanced-monitor-logs.sh test-container

# 터미널 2: 로그 생성 테스트
docker exec -it test-container bash -c 'for i in {1..1500}; do echo "Test log entry $i"; done'
  • 로그 확인 및 분석
# 모니터링 로그 확인
cat container_logs.txt

# 컨테이너 로그 확인
docker logs test-container

 

3. Docker 디스크 사용량 모니터링 실습

3.1 기본 디스크 사용량 모니터링

# Docker 전체 디스크 사용량 확인
docker system df

# 상세한 디스크 사용량 확인
docker system df -v

# 실시간 디스크 사용량 모니터링 스크립트 생성
cat > monitor-disk-usage.sh << 'EOF'
#!/bin/bash

while true; do
    echo "=== Docker 디스크 사용량 모니터링 === ($(date))"
    echo "1. 컨테이너 디스크 사용량:"
    docker ps --size --format "table {{.Names}}\\t{{.Size}}\\t{{.State}}"
    
    echo -e "\\n2. 이미지 디스크 사용량:"
    docker system df -v | grep "Images space"
    
    echo -e "\\n3. 볼륨 디스크 사용량:"
    docker system df -v | grep "Volumes space"
    
    echo -e "\\n4. 빌드 캐시 사용량:"
    docker system df -v | grep "Build cache"
    echo "======================================="
    sleep 60
done
EOF

chmod +x monitor-disk-usage.sh

3.2 테스트용 컨테이너 생성 및 모니터링

# 테스트용 컨테이너 생성
docker run -d --name disk-test-1 ubuntu:20.04 tail -f /dev/null
docker run -d --name disk-test-2 nginx

# 데이터 생성 스크립트
cat > generate-data.sh << 'EOF'
#!/bin/bash

for i in {1..10}; do
    echo "Generating data file $i..."
    docker exec disk-test-1 dd if=/dev/zero of=/tmp/file$i bs=1M count=100
    sleep 1
done
EOF

chmod +x generate-data.sh

3.3 디스크 모니터링 스크립트 작성

cat > advanced-disk-monitor.sh << 'EOF'
#!/bin/bash

LOG_FILE="disk_usage_log.txt"
THRESHOLD_PERCENT=80
ALERT_EMAIL="admin@example.com"

log_message() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}

get_container_sizes() {
    docker ps -s --format "{{.Names}}: {{.Size}}" | while read line; do
        log_message "Container $line"
    done
}

check_disk_usage() {
    # Docker 루트 디렉토리 사용량 확인
    DOCKER_ROOT=$(docker info | grep "Docker Root Dir" | cut -d: -f2 | tr -d ' ')
    USAGE_PERCENT=$(df -h $DOCKER_ROOT | awk 'NR==2 {print $5}' | tr -d '%')
    
    log_message "Docker root directory ($DOCKER_ROOT) usage: ${USAGE_PERCENT}%"
    
    if [ $USAGE_PERCENT -gt $THRESHOLD_PERCENT ]; then
        MESSAGE="Warning: Docker disk usage is at ${USAGE_PERCENT}%"
        log_message "$MESSAGE"
        
        # 가장 큰 컨테이너 5개 리스트
        log_message "Top 5 largest containers:"
        docker ps -s --format "{{.Size}}\\t{{.Names}}" | sort -hr | head -n 5 | \\
            while read line; do
                log_message "$line"
            done
        
        # 사용하지 않는 리소스 정리 추천
        log_message "Recommended cleanup commands:"
        log_message "docker system prune -f"
        log_message "docker volume prune -f"
        log_message "docker image prune -a -f"
    fi
}

monitor_volumes() {
    log_message "Volume usage:"
    docker volume ls -q | while read vol; do
        USAGE=$(docker run --rm -v $vol:/vol alpine du -sh /vol)
        log_message "Volume $vol: $USAGE"
    done
}

# 메인 모니터링 루프
while true; do
    log_message "=== Starting disk usage check ==="
    check_disk_usage
    get_container_sizes
    monitor_volumes
    log_message "=== Completed disk usage check ==="
    echo -e "\\n"
    sleep 300  # 5분 간격으로 체크
done
EOF

chmod +x advanced-disk-monitor.sh

3.4 실습 시나리오

3.4.1 모니터링 시작

# 터미널 1: 기본 모니터링 실행
./monitor-disk-usage.sh

# 터미널 2: 데이터 생성
./generate-data.sh

3.4.2 모니터링 테스트

# 터미널 1: 고급 모니터링 실행
./advanced-disk-monitor.sh

# 터미널 2: 대용량 파일 생성
docker run -d --name big-data-test ubuntu:20.04 tail -f /dev/null
docker exec big-data-test dd if=/dev/zero of=/bigfile bs=1G count=2

3.4.3 디스크 정리

# 사용하지 않는 리소스 정리
docker system prune -f

# 사용하지 않는 볼륨 정리
docker volume prune -f

# 사용하지 않는 이미지 정리
docker image prune -a -f

# 정리 후 사용량 확인
docker system df -v

3.4.4 컨테이너 별 상세 모니터링

# 컨테이너별 디스크 사용량 모니터링 스크립트
cat > container-disk-monitor.sh << 'EOF'
#!/bin/bash

echo "=== Container Disk Usage Details ==="
docker ps --format "{{.Names}}" | while read container; do
    echo "Container: $container"
    echo "Size details:"
    docker ps -s --filter "name=$container" --format "Virtual Size: {{.Size}}"
    echo "Top 5 largest directories:"
    docker exec $container du -h / 2>/dev/null | sort -hr | head -n 5
    echo "------------------------"
done
EOF

chmod +x container-disk-monitor.sh
./container-disk-monitor.sh
LIST

Claude Code나 GPT-5 Codex를 매일 쓰는 개발자라면 한 번쯤 마주치는 역설이 있다.
같은 프롬프트를 두 번 넣으면 다른 코드가 나온다. 어떤 때는 Python 함수가 한 줄짜리 list comprehension으로 나오고, 다음엔 for 루프 세 줄로 나온다. 변수명이 다르고, 주석 표현이 다르고, 예외 처리 순서까지 바뀐다. 결정적(deterministic)이지 않다. 학부 시절 우리가 "좋은 소프트웨어"의 필수 조건으로 배운 바로 그 속성이 깨진다.
그런데 막상 빌드는 성공한다. 테스트는 통과한다. 종종 사람보다 낫다. 2026년 현재 "시니어 엔지니어 대부분이 실제 코드는 거의 쓰지 않는다"는 보고가 나온다. 이 사람들이 순진한 것도, 품질을 포기한 것도 아니다. 오히려 그 반대다 — 프로덕션 배포되는 코드의 양은 늘었는데 사람이 직접 타이핑한 비율은 줄었다.
이 역설이 이 글의 출발점이다. 주사위를 굴려 나온 코드가 왜 빌드에 성공하는가. 그리고 우리는 그 사실을 어떻게 받아들여야 하는가.


1. 먼저 — LLM 비결정성의 구조

이 논의를 하려면 비결정성이 어디서 오는지부터 정확히 써야 한다.
LLM은 다음 토큰을 확률 분포로 예측한다. 어휘에 있는 모든 토큰마다 "이 자리에 이 토큰이 올 확률"이 매겨진다. 생성 단계에서 모델은 이 분포에서 하나를 샘플링한다. 이 샘플링 과정에 세 종류의 무작위성이 개입한다.

  • Temperature: 분포를 평평하게 할지 뾰족하게 할지 결정하는 스칼라. T=0이면 가장 확률 높은 토큰만 뽑고, T가 높아질수록 덜 확률 높은 토큰에도 기회가 간다.
  • Top-k / Top-p 샘플링: 후보를 상위 k개 또는 누적 확률 p까지로 제한.
  • Random seed: 같은 분포에서도 난수 시드가 다르면 다른 토큰이 뽑힌다.

"온도 0으로 하면 결정적이지 않나?"라는 질문이 자주 나온다. 답은 '아니오'다. 2024년 ACM Transactions on Software Engineering and Methodology에 실린 실증 연구는 ChatGPT의 코드 생성 비결정성을 829개 과제에 대해 측정했는데, 기본 설정에서는 같은 프롬프트에 대해 "모든 요청이 동일한 테스트 출력을 내는 과제 비율"이 CodeContests에서 24.24%, APPS에서 49%, HumanEval에서 52.44%에 불과했다. 온도를 0으로 내려도 비결정성은 줄어들지만 완전히 사라지지 않았다.
그 이유는 여러 층에 있다. GPU 병렬 연산의 부동소수점 누적 순서, batching에서의 다른 요청과의 상호작용, 모델 서빙 인프라의 라우팅 비결정성 등. 결국 LLM은 구조적으로 비결정적이고, 이는 "설정으로 끌 수 있는 버그"가 아니라 시스템의 본질적 속성이다.


2. 그런데도 빌드는 성공한다 — 두 번째 역설

여기가 흥미로워지는 지점이다. LLM이 비결정적이라는 사실은 10년 전이라면 "소프트웨어 엔지니어링 도구로는 실격"이라는 결론으로 가야 했다. 그런데 2026년 현실은 반대다.

  • Rakuten은 Opus 4.1로 오픈소스 프로젝트를 7시간 자율 리팩터링, 코드 품질 30% 개선.
  • Anthropic 내부 팀은 Claude Code로 PR 처리 속도 3배 개선.
  • 시니어 엔지니어들이 "코드의 100%는 AI가 쓰고, 내가 쓴 건 0%다"라고 말하고 있다.

"비결정적 생성"과 "프로덕션 품질"이 어떻게 공존 가능한가. 이게 이 글이 파고들 질문이다.


3. 해답의 첫 번째 층 — 분포가 충분히 뾰족하면 된다

가장 먼저 짚어야 할 지점은 "비결정적"과 "무작위"가 다르다는 것이다.
온도 0.7의 LLM이 def factorial(n): 다음에 생성할 토큰을 상상해보자. 확률 분포를 보면 \n if n 같은 토큰 시퀀스의 확률이 압도적으로 높고, 그 외의 선택지들은 사실상 0에 가깝다. 분포가 뾰족하다(peaked). 이런 경우 샘플링을 10번 해도 거의 매번 같은 토큰이 뽑힌다. 겉보기에 "결정적으로 보이는 비결정성"이다.
이 관찰을 실증적으로 확인한 것이 2024년의 AdapT Sampling 연구다. 연구자들은 코드 토큰을 "도전적 토큰(challenging tokens)"과 "자신 있는 토큰(confident tokens)" 으로 분류했다. 자신 있는 토큰은 분포가 뾰족해서 어떤 온도를 써도 같은 것이 뽑힌다. 도전적 토큰은 분포가 상대적으로 평평해서 실제로 다른 선택이 나온다.
흥미로운 발견은 도전적 토큰은 코드 블록의 첫 번째 위치에 집중적으로 나타난다는 것. 파이썬에서 이는 79.8%(HumanEval), 83.4%(MBPP), 82.5%(APPS)의 예측 난도를 보였다. 즉 LLM이 고민하는 지점은 "어떤 제어 구조를 쓸 것인가"(if/for/while/try)와 같은 구조 결정 순간이고, 그 이후의 토큰들은 상당히 결정론적으로 이어진다.
코드의 많은 부분이 "주사위"가 아니라 "자석"이다. 분포가 충분히 뾰족해서 샘플링해도 같은 결과로 끌려간다. 이게 첫 번째 해답이다.


4. 해답의 두 번째 층 — 원래 결정적이었던 적 없다

Paul Bernard의 2026년 2월 Medium 에세이 "Your Code Was Never Deterministic"이 찌르는 지점이 이것이다.
그는 이렇게 쓴다. 우리가 "결정적 코드"라고 부르던 것의 실체를 보자. 컴파일러는 결정적이다. 같은 소스 코드를 두 번 컴파일하면 같은 바이너리가 나온다. 맞다. 그런데 "같은 소스 코드"가 과연 결정적으로 쓰여졌는가는 다른 문제다.
10명의 시니어 개발자에게 "이 요구사항을 코드로 옮겨 주세요"라고 똑같이 말해보자. 10개의 다른 구현이 나온다. 변수명, 함수 분리 방식, 에러 처리 전략, 성능과 가독성의 트레이드오프 선택. 이 모든 것이 사람의 비결정성이다. 그런데 우리는 이걸 "비결정성"이라고 부르지 않았다. "스타일"이라고 불렀다.
더 중요한 지점. 우리가 실제 프로덕션에서 겪는 대부분의 장애는 컴파일러가 주사위를 굴려서 생긴 것이 아니라, 사람이 불완전한 의도를 코드로 옮기면서 생긴 것이다. 요구사항을 오해했거나, 엣지 케이스를 놓쳤거나, 동시성 가정을 틀리게 했거나.
Bernard의 핵심 통찰은 이것이다. "LLM은 비결정적이니까 믿을 수 없다"고 말할 때, 우리는 이전에 도대체 무엇을 믿고 있었는가? 테스트 커버리지를 믿고 있었나? 대부분 시스템에서 실질적 커버리지는 30~50% 수준이다. 코드 리뷰를 믿고 있었나? 진지한 리뷰와 LGTM 스탬프를 구분해야 한다. 우리가 믿었던 건 "코드가 동일 입력에 동일 출력을 낸다"는 컴파일러의 충실함이지, "코드가 올바르다" 는 보장이 아니었다.
그런데 컴파일러의 충실함은 LLM 코드에도 적용된다. LLM이 생성한 코드도 컴파일러가 결정적으로 처리한다. 같은 테스트를 돌리면 같은 결과가 나온다. 생성 단계는 비결정적이지만, 생성된 결과물은 여전히 결정적으로 검증 가능하다.
이것이 두 번째 해답이다. 비결정성의 위치가 "실행 시점"에서 "생성 시점"으로 이동했을 뿐이고, 우리는 둘 중 후자를 훨씬 잘 다룰 수 있다.


5. 해답의 세 번째 층 — 검증이 생성을 잡는다

가장 근본적인 해답은 여기 있다. 현대 AI 코딩 워크플로우는 "생성-검증 루프"로 설계되어 있다.
Claude Code, GPT-5 Codex, Cursor가 작동하는 방식을 생각해보자.

  1. 모델이 코드를 생성한다 (비결정적).
  2. 그 코드가 컴파일된다 (결정적).
  3. 타입 체커가 돌아간다 (결정적).
  4. 린터가 돈다 (결정적).
  5. 테스트 스위트가 실행된다 (결정적).
  6. 실패가 있으면 그 정보가 다시 모델에 피드백된다.
  7. 모델이 수정한 코드를 재생성한다 (비결정적).
  8. 루프가 수렴할 때까지 반복.

이 루프에서 비결정성의 역할은 "탐색(exploration)"이다. 모델이 여러 후보 구현을 시도해볼 수 있게 해주는 도구. 결정적(T=0) 모델은 한 번 막히면 계속 같은 곳에서 막힌다. 비결정적 모델은 같은 프롬프트에서 다른 경로를 시도해볼 수 있고, 그중 하나가 검증을 통과하면 그게 답이다.
이 구조가 진화적 알고리즘과 비슷하다는 점에 주목할 필요가 있다. 유전 알고리즘은 무작위 변이와 결정적 선택을 조합해서 해를 찾는다. 변이 단계는 비결정적이어야 한다 — 그래야 새로운 해공간을 탐색할 수 있다. 선택 단계는 결정적이어야 한다 — 그래야 나쁜 해가 걸러진다.
Claude Code 같은 에이전틱 코딩 도구는 정확히 이 구조다. 모델은 변이를 만들고, 테스트와 타입 시스템이 선택을 한다. 최종 산출물의 품질은 모델의 비결정성이 아니라 검증 시스템의 엄격함에 의해 결정된다.
이 관점으로 보면 우리가 AI 시대에 개발 인프라에 투자해야 할 것이 바뀐다. "모델을 더 정확하게 만드는 것"보다 "검증을 더 촘촘하게 만드는 것" 이 레버가 된다.


6. 한 층 더 — 명세는 변하지 않는다

Bernard의 에세이가 말하는 더 깊은 층이 있다. "명세"의 역할.
AI 코딩 파이프라인에서 개발자가 하는 일은 무엇인가. 명세를 쓴다. 자연어로, 구조화된 acceptance criteria로, property-based test로, 실행 가능한 스펙으로. 이걸 LLM이 구현으로 옮긴다. 구현은 run마다 다를 수 있다. 하지만 명세는 다르지 않다.
검증 인프라가 명세 층에 anchoring되어 있으면, 구현의 변동성은 더 이상 무서운 것이 아니다. 내부 구현 디테일일 뿐이다. 레지스터 할당이나 분기 예측처럼, 한 추상화 층 위로 올라갔을 때 보이지 않게 되는 종류의 디테일.
이 비유는 정확하다. 1970년대 개발자는 어셈블리로 레지스터 할당을 직접 했다. C 컴파일러가 등장하면서 레지스터 할당은 컴파일러가 상황에 따라 다르게 하는 내부 디테일이 됐다. 개발자는 더 이상 레지스터를 보지 않는다. 레지스터 할당의 "비결정성"(컴파일러 버전에 따라, 최적화 플래그에 따라 다른 결과)은 문제가 되지 않는다. 왜냐하면 C 명세가 그대로 유지되기 때문이다.
AI 코딩이 만드는 추상화의 도약도 같은 구조다. 개발자가 명세를 쓰고, 구현은 AI가 매번 다르게 할 수 있다. 명세 층에서 검증만 제대로 되면 구현의 변동은 무관한 내부 디테일이 된다.
이 틀에서 보면 "AI가 쓴 코드를 한 줄 한 줄 리뷰해야 한다"는 주장은 1970년대 C 개발자에게 "레지스터 할당을 한 줄 한 줄 리뷰해야 한다"고 말하는 것과 같다. 아래 층위에서의 결정을 위 층위에서 감시하려는 시도. 추상화의 이점을 스스로 포기하는 것.


7. 반론과 그 반론에 대한 반박

여기까지 읽은 독자의 머릿속에 떠오르는 반론들이 있을 것이다. 하나씩 다룬다.

반론 1: "테스트 코드도 코드다. 그걸 누가 검증하나?"

맞다. 그런데 비대칭이 존재한다. 명세는 프로덕션 코드보다 훨씬 짧고, 훨씬 선언적이며, 훨씬 변경이 드물다. 10,000줄의 구현을 생성하기 위한 명세는 200줄일 수 있다. 200줄은 사람이 리뷰할 수 있다. Property-based test로 쓰면 더 짧아진다. 리뷰해야 할 표면적 자체가 50배 줄어든다.

반론 2: "LLM이 테스트도 같이 생성한다. 같이 틀릴 수 있다."

타당한 지적이다. 하지만 해결책은 있다. 테스트와 구현을 다른 세션에서, 다른 모델로, 다른 프롬프트로 생성하는 것. 한 Claude가 테스트를 쓰고, 다른 Claude가 그 테스트를 통과하는 구현을 쓴다. 두 인스턴스가 공통으로 틀리려면 훨씬 정교한 체계적 오류가 필요하다. 이는 다수 인스턴스 합의(N-of-M consensus)와 구조적으로 같다.

반론 3: "미세한 엣지 케이스는 테스트로 못 잡는다."

사실이다. 하지만 이건 AI 시대 이전에도 사실이었다. 사람이 쓴 코드도 엣지 케이스에서 터진다. 오히려 AI는 알려진 엣지 케이스 패턴에 대한 기억이 사람보다 넓다. HackerNews의 C++/Qt 프로젝트 사례에서 Claude Code가 20분에 해결한 UI 버그는 숙련된 사람이 2시간 걸린 것이었다. 엣지 케이스 커버리지에서 AI가 사람보다 낫다는 증거가 이미 쌓이고 있다.

반론 4: "생성 루프가 무한히 돌 수 있다."

구조적 문제가 있을 때 그렇다. 해결은 타임아웃과 예산 설정, 그리고 "실패 시 사람에게 에스컬레이션"이다. 이는 마이크로서비스에서 circuit breaker가 하는 역할과 같다. 무한 루프는 아키텍처 결함이지 AI 코딩의 본질이 아니다.

반론 5: "그래도 나는 한 줄씩 읽어야 마음이 놓인다."

그건 정서의 문제지 엔지니어링의 문제가 아니다. 공감은 한다. 나도 처음에 Claude Code가 쓴 PR을 마우스로 한 줄씩 긁으며 읽었다. 그런데 그 시간이 실제로 버그를 잡았는가? 대부분은 아니었다. 마음이 편해지는 효과는 있었지만, 품질 보장 효과는 테스트와 타입 시스템이 이미 다 하고 있었다. 불안은 대체 가능한 의식이지, 대체 불가능한 검증이 아니다.


8. 실무자의 관점 — 이 통찰을 어떻게 써야 하나

이론을 썼으니 실무로 내려간다. 이 통찰이 2026년 개발자의 하루하루에 어떻게 적용되는가.

8.1 투자 우선순위를 다시 짜라

"좋은 AI 코딩 환경"의 조건이 바뀌었다.

  • 빠른 타입 체커 (rustc, tsc, mypy)
  • 빠른 테스트 러너 (pytest-xdist, jest parallel, cargo nextest)
  • 엄격한 린터 (ruff, eslint, clippy)
  • Property-based testing 프레임워크 (hypothesis, fast-check, proptest)
  • 계약 기반 테스트 (contract testing, consumer-driven contracts)

이 인프라가 허약한 프로젝트에서는 AI 코딩의 ROI가 낮다. 강한 프로젝트에서는 ROI가 폭발적이다. "검증 인프라에 투자한 프로젝트가 AI 시대의 수혜를 독점한다."

8.2 명세 쓰기가 핵심 역량이 된다

개발자의 시간 배분이 바뀐다.

  • 과거: 70% 구현, 20% 설계, 10% 테스트
  • AI 시대: 20% 명세 작성, 30% 검증 설계, 30% 리뷰·디버깅, 20% 통합

이 변화를 빨리 받아들이는 개발자일수록 경쟁력이 크다. "타이핑 속도가 더 이상 개발 속도와 상관없다." 대신 "명확한 명세를 언어로 옮기는 능력"이 중요해진다.

8.3 Writer/Reviewer 패턴을 기본 워크플로우로

지금 가장 효과적인 AI 코딩 방법론 중 하나는 두 개의 모델 인스턴스를 병렬 운영하는 것이다.

  • Writer: 구현을 생성하는 역할. 빠르고 공격적으로.
  • Reviewer: 별도 세션에서 구현을 평가하는 역할. 의심 많고 보수적으로.
  • Human: 두 결과를 보고 최종 판단.

Claude Code와 GPT-5 Codex를 병렬로 돌리며 서로를 교차 검증하게 하는 기법은 2026년 현재 시니어 엔지니어들 사이에서 표준이 되어가는 중이다. 한 모델의 비결정성을 다른 모델의 비결정성으로 상쇄시키는 설계.

8.4 명세 자체를 재사용 자산으로

한 번 잘 쓴 명세는 모델이 바뀌어도, 언어가 바뀌어도, 프레임워크가 바뀌어도 재사용 가능하다. Ruby on Rails로 구현한 명세를 Spring Boot로 다시 구현하는 것이, 명세가 있을 때는 AI에게 일이고 없을 때는 사람에게 대공사다.


9. 한 걸음 물러서서 — 추상화의 역사에 놓기

이 모든 변화가 어디에 놓이는지 역사적 관점으로 보면 선명해진다.
소프트웨어 역사에서 추상화는 한 방향으로만 올라왔다.

  • 기계어 → 어셈블리: 니모닉과 레이블의 도입.
  • 어셈블리 → C: 레지스터, 스택, 메모리 할당을 컴파일러에 맡김.
  • C → Java/Python: 수동 메모리 관리를 GC에 맡김.
  • Java/Python → 프레임워크: HTTP 파싱, ORM, 의존성 주입을 프레임워크에 맡김.
  • 프레임워크 → 서비스: 인증, 결제, 검색을 외부 서비스에 맡김.

각 단계에서 아래 층의 "결정"들이 비가시적이 됐다. 그 비가시성은 처음엔 두려운 것이었다. "GC가 언제 돌지 모르는데 어떻게 실시간 시스템을 짜냐" "ORM이 어떤 SQL을 쏠지 모르는데 어떻게 성능을 보장하냐." 이런 질문들이 그 시대의 엔지니어들을 실제로 밤잠 못 이루게 했다.
답은 매번 같았다. 한 층 위에서 추상화를 붙들면 아래 층의 비결정성은 관리 가능한 내부 디테일이 된다. GC의 동작은 JVM 튜닝 파라미터로 관리되고, ORM의 쿼리는 모니터링으로 잡히고, 서비스의 장애는 circuit breaker로 격리된다.
2026년 AI 코딩도 같은 위치에 있다. 개발자가 명세를 쓰고, 구현의 비결정성은 검증 층에서 흡수된다. 처음에는 두렵지만, 한 세대 지나면 "이 레벨에서 왜 걱정했지?"라고 돌이켜볼 추상화 층이 된다.


10. 마무리 — 주사위는 구르고, 빌드는 선다

글의 처음으로 돌아간다. 주사위를 굴려 나온 코드가 왜 빌드에 성공하는가.
세 층의 답을 정리하면.
첫 번째 층: 분포가 뾰족해서 대부분의 토큰은 사실상 결정적이다. "비결정적"이라는 꼬리표가 주는 인상만큼 혼란스럽지 않다.
두 번째 층: 우리가 "결정적 코드"라고 불렀던 것들도 인간의 비결정성 위에 서 있었다. 위치만 바뀐 것이지 전에 없던 성질이 생긴 것이 아니다.
세 번째 층: 현대 AI 코딩은 비결정적 생성과 결정적 검증의 루프로 설계된다. 생성의 변동성은 탐색의 도구이고, 품질은 검증의 엄격함에서 온다.
이 셋을 합쳐 보면 역설은 역설이 아니다. 비결정성은 문제가 아니라 구조다. 그리고 그 구조 위에서 잘 작동하는 개발 인프라를 가진 팀이 2026년의 승자가 될 것이다.
Paul Bernard가 에세이 끝에 던진 질문을 조금 바꿔 다시 쓴다.

LLM이 비결정적이라서 쓸 수 없다고 말하기 전에, 우리는 이전에 무엇을 믿고 있었는지 먼저 물어야 한다. 우리의 테스트? 우리의 리뷰? 우리의 직감? 그중 무엇도 "결정적"이지 않았다. 우리가 진짜로 믿었던 건 단 하나, 검증 가능한 명세를 세우는 우리의 능력이었다. 그 능력은 AI 시대에도 그대로 우리 것이고, 오히려 레버가 훨씬 길어진다.

주사위는 계속 구른다. 그런데 빌드는 선다. 그 사실이 우리에게 말하는 것은 이것이다. 우리는 이미 답을 알고 있었다. 답의 위치가 바뀌었을 뿐이다.


참고 자료

LIST

+ Recent posts