Clean Code for Humans and LLMs (코딩의 즐거움을 죽이지 않고)
Source: Dev.to
위의 링크에 포함된 텍스트를 번역하려면 실제 내용이 필요합니다. 번역하고자 하는 전체 텍스트(코드 블록이나 URL을 제외한 본문)를 제공해 주시면, 원본 형식과 마크다운 구문을 그대로 유지하면서 한국어로 번역해 드리겠습니다.
LLM‑친화적인 클린 코드
LLM은 코드를 작성할 수 있습니다. 리팩터링도 할 수 있고, 테스트도 생성할 수 있습니다.
오랫동안 코딩해 온 사람이라면 불편함을 느끼기 쉽습니다 — 도구가 나쁘기 때문이 아니라, 개인적인 부분에 닿기 때문입니다: 코딩의 즐거움 중 큰 부분은 똑똑함에서 나오기 때문이죠.
- 우아한 해결책, 문제를 깔끔한 추상화로 축소시키는 모습을 볼 때.
- 그리고 나서 “똑똑함보다 지루함을 선호하라.” 같은 조언을 읽게 됩니다.
- 마치 창의성에 대한 부고문처럼 들립니다.
그럴 필요는 없습니다. 이 글에서는 두 가지를 주장합니다:
- 인간을 위한 클린 코드와 LLM을 위한 클린 코드는 약간 다릅니다 (인간 가독성 vs. 모델 모호성).
- 똑똑함을 적절한 위치에 배치함으로써 코딩의 즐거움을 희생하지 않고 두 가지를 조화시킬 수 있습니다.
Context vs. Prediction
인간은 코드를 맥락과 함께 읽습니다
- 팀 컨벤션
- 아키텍처 히스토리
- 도메인 지식
- “여기서는 이렇게 한다”는 직관
LLM은 그 맥락을 갖고 있지 않습니다.
LLM은 의도가 아니라 예측으로 동작합니다.
따라서 LLM‑친화적인 코드는 다음과 같은 코드를 의미합니다:
- 모호성을 최소화하고
- 명시적인 계약을 가지고 있으며
- 구조적으로 규칙적이고
- 로컬 리팩터링에 안전합니다
이것은 **“기계용 코드를 작성하는 것”**이 아닙니다.
인간이든 자동이든 리팩터링을 견디는 코드를 작성하는 것입니다.
“기계용 코드를 작성한다”는 표현은 다음과 같은 오해를 불러일으킵니다:
- 똑똑함은 나쁘다
- 우아함은 위험하다
- 재미는 비전문적이다
하지만 똑똑함이 적이 되는 것이 아니라 제자리에 있지 않은 똑똑함이 문제입니다.
두 종류의 똑똑함
| 좋은 똑똑함 | 나쁜 똑똑함 |
|---|---|
| 인지 부하를 줄인다 | 복잡한 표현식으로 로직을 압축한다 |
| 도메인을 모델링한다 | 언어의 경계 사례를 이용한다 |
| 안정적인 추상화를 만든다 | 암묵적인 가정에 의존한다 |
| 미래 변경을 단순화한다 | 인상적이지만 깨지기 쉽다 |
| 공학적 예술 | 작성자는 재미있지만 다른 사람은 고통 |
목표는 똑똑함을 금지하는 것이 아니라 올바른 계층으로 옮기는 것입니다: 안쪽은 똑똑하게, 바깥은 지루하게.
조화 방안
- 인간을 위한 클린 코드
- LLM을 위한 클린 코드
- 아름다운 것을 만드는 즐거움
세 가지 모두 바깥쪽을 명시적이고, 안정적이며, 예측 가능하고, 리팩터링하기 쉬운 형태로 만들고, 안쪽은 우아하고 표현력 있게, 도메인에 집중한다면 동시에 존재할 수 있습니다.
예시
1️⃣ 안정적인 시그니처를 가진 Ruby 서비스
class CreateOrder
def self.call(input)
new.call(input)
end
def call(input)
input = Input.new(input)
validate(input)
.bind { persist(input) }
.bind { publish_event(input) }
end
end
- 예측 가능한
.call - 명시적인 입력 강제 변환
- 명확한 파이프라인
- 메타프로그래밍 없음
결과 타입 (똑똑한 설계)
class Result
def self.ok(value = nil) = new(true, value, nil)
def self.err(error) = new(false, nil, error)
attr_reader :value, :error
def initialize(ok, value, error)
@ok = ok
@value = value
@error = error
end
def ok? = @ok
def bind
return self unless ok?
yield(value)
end
end
검증 단계
def validate(input)
return Result.err(:missing_customer) if input.customer_id.nil?
Result.ok(input)
end
도메인 친화적이며, 조합 가능한 파이프라인 → 인간은 조합을 통해 우아함을 얻고, LLM은 명시적인 계약과 안전한 리팩터 경계를 얻는다.
2️⃣ Go HTTP 핸들러 – 명시적이고 단순함
type Server struct {
Orders *OrdersService
}
func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
order, err := s.Orders.Create(r.Context(), req.ToInput())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(order)
}
LLM은 이를 안전하게 리팩터링할 수 있다.
도메인 서비스 – 즐거움이 살아있는 곳
type OrdersService struct {
Repo OrdersRepo
Clock Clock
}
func (s *OrdersService) Create(ctx context.Context, in CreateOrderInput) (*Order, error) {
if err := in.Validate(); err != nil {
return nil, err
}
order := NewOrder(in.CustomerID, s.Clock.Now())
order.AddItem(in.ItemID, in.Qty)
if err := s.Repo.Save(ctx, order); err != nil {
return nil, err
}
return order, nil
}
도메인 모델, 불변성, 높은 응집도 → IO는 지루하고, 도메인은 예술이다.
3️⃣ TypeScript – 타입이 모호성을 줄인다
// Explicit domain result
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
공개 함수 시그니처
export async function createOrder(
input: CreateOrderInput
): Promise<Result<Order, { code: string }>> {
if (!input.customerId) {
return { ok: false, error: { code: "MISSING_CUSTOMER" } };
}
const order = buildOrder(input);
await repo.save(order);
return { ok: true, value: order };
}
명시적이며, 사용하기 쉽고, 오용하기 어렵고, LLM이 안전하게 확장하기에 사소한 작업이다.
강력하게 타입이 지정된 도메인 모델
type Money = { currency: "PLN" | "EUR"; cents: number };
function addMoney(a: Money, b: Money): Money {
if (a.currency !== b.currency) throw new Error("currency mismatch");
return { currency: a.currency, cents: a.cents + b.cents };
}
불변성 강제, 최소한의 인터페이스, 강력한 의미론 → “아름다운 코딩”.
Source: …
Practical Standard You Can Adopt
| 가이드라인 | 이유 |
|---|---|
| 안정적인 함수 시그니처 (키워드 인자 / 타입이 지정된 구조체) | LLM이 의존할 수 있는 계약을 보장 |
명시적인 출력 (Result, 타입이 지정된 오류) | 모호성을 제거 |
| 깨끗한 추상화 (모듈 내부의 작은 DSL) | 영리함을 국지화 |
| 구성 파이프라인 | 복잡성을 누출하지 않으면서 알고리즘적 우아함 제공 |
| 메타프로그래밍이나 DSL을 사용해야 할 경우: • 전용 폴더/모듈에 배치 • 왜 하는지 문서화 | 깨지기 쉬운 트릭이 우연히 퍼지는 것을 방지 |
| 미니‑프레임워크처럼 테스트 작성 | 테스트는 인간과 LLM 모두를 위한 안전망 역할 |
짧다고 나쁜 것이 아니라, 압축된 코드는 안전하게 수정하기 어렵다. LLM은 구조를 리팩터링하고, 의도는 리팩터링하지 않는다. DRY는 가이드라인일 뿐, 종교가 아니다.
TL;DR
- 영리함은 내부에 – 도메인을 모델링하고, 인지 부하를 줄이며, 안정적인 추상화를 만들 때.
- 지루하고 명시적인 코드는 외부에 – 안정적인 시그니처, 명확한 계약, 규칙적인 구조.
이 경계를 지키면 얻을 수 있는 것:
- 인간 및 LLM이 탐색하기 쉬운 코드,
- 안전한 리팩터링 가능성,
- 아름답고 창의적인 솔루션을 만드는 즐거움.
오해를 방지하려면 의도를 반복해서 명시하세요.
코딩의 즐거움이 다음에 있다면:
- 모든 줄을 손수 작성하기
- 구문 마법을 과시하기
- 10개의 아이디어를 1줄에 압축하기
…그렇다면 어느 정도는 사라질 수 있다.
하지만 더 깊은 즐거움은 남아 있고, 더욱 커진다:
- 도메인 모델링
- API 설계
- 추상화 생성
- 복잡성 감소
- 시스템 회복력 강화
LLM은 이를 대체하지 않는다. 오히려 증폭시킨다.
“지루함을 영리함보다 선호하라”는 조언은 불완전하다.
더 나은 버전은:
- 변경이 자주 일어나는 곳에서는 지루함을 선호한다.
- 의미가 살아있는 곳에서는 영리함을 선호한다.
또는 간단히:
내부는 영리하게, 외부는 지루하게.
이것이 인간을 위한 깨끗한 코드이며,
LLM을 위한 깨끗한 코드이다.
그리고 코딩의 즐거움을 살아 있게 만든다.