한 줄 정의

소프트웨어 복잡성을 격리하고 동료 개발자의 불필요한 놀라움(surprise)을 방지하기 위해, 시니어 엔지니어들이 코드 작성 및 아키텍처 판단 단계에서 실천하는 7가지 핵심 코딩 패턴.

핵심 요지

  • 코드는 쓰는 것보다 읽는 비용이 더 비싸다: 시니어 엔지니어는 자신의 지식을 뽐내기 위한 화려한 추상화 대신, 급박한 장애 상황에서 동료가 역공학하지 않고도 직관적으로 의도를 이해할 수 있는 지루하고 명확한 코드를 선호한다.
  • 의사결정의 패턴: 프레임워크 종속적인 기술에 매몰되기보다 버그를 신속히 탐지하고, 비즈니스 요구사항 변경에 유연하게 대응하며, 시스템을 오해하기 어렵게 설계하는 데 집중한다.
  • 7가지 패턴의 본질: 코드 들여쓰기를 최소화하고(Return Early), 구현이 아닌 비즈니스 의미를 강조하며(Business Naming), 외부 시스템의 오염을 차단하고(Boundary), 무효한 데이터 상태를 원천 차단하며(Prevent Invalid State), 판단과 실행을 분리하고(Separation), 추적 가능한 에러를 제공하고(Friendly Error), Git Diff를 깔끔하게 관리(Diff-Driven)하는 실질적 룰이다.
  • 이름의 비즈니스 도메인 언어화: activeUsers 변수명 오해 사례(정지되었으나 삭제되지 않은 사용자 포함) 등 구현 세부사항보다 비즈니스 의미가 반영된 작명이 버그를 줄인다.
  • 부수 효과의 분리: 의사결정(Pure Function)과 실제 부수 효과 유발 실행(Impure Function)을 쪼개어, 복잡한 모킹(Mocking) 없이 비즈니스 규칙만 단독으로 유닛 테스트할 수 있게 설계한다.
  • 가드 절을 통한 예외 경로 조기 반환(Return Early), 구현 형태가 아닌 비즈니스 의도가 실린 네이밍 관념을 고수한다.
  • 서드파티 API 변경이나 DB 형상 누수가 도메인을 오염시키지 않도록 완충 경계(Boundary)를 세우고, 식별 유니온 타입 등으로 유효하지 않은 상태를 컴파일 시점에 차단한다.
  • 의사결정(Pure Function)과 부수 효과 실행을 분리하여 테스트 정합성을 제고하고, 기계적 에러 코드와 requestId를 부여해 장애 온콜 시의 추적을 수월하게 돕는다.
  • 로컬 가동 데모가 아닌 코드 리뷰와 부분 롤백을 안전하게 배려하여 Git Diff 단위로 의미 있게 쪼개어 배포(PR 분할)한다.

상세

1. 조건문 미로 대신 조기 반환(Return Early) 사용

  • 개념: 함수가 깊숙이 들여써지는 터널 구조가 되지 않도록 오류가 날 만한 예외 경로(Guard clauses)를 함수 상단에서 먼저 쳐내는 패턴이다.
  • 장점: 개발자가 장애 상황에서 흐름을 읽는 방식(“이 함수를 멈추는 요인은 무엇인가?”)대로 작성되므로 가독성이 높고 디버깅이 쉬워진다. 정상 경로(happy path)가 중첩 블록 밑바닥에 파묻히는 것을 방지한다.

2. 기술적 구현이 아닌 비즈니스적 의미를 이름으로 지정

  • 개념: data, result, payload, temp 같은 단순 기술 표현 대신 subscription, isBillable, usersEligibleForReactivation 처럼 비즈니스 도메인의 상세 규칙이 투영된 이름을 짓는다.
  • 장점: 구현 세부 사항이 바뀌어도 비즈니스의 본질적인 뜻이 유지되며, 잘못된 코드 사용(예: 삭제되지 않고 정지된 사용자가 activeUsers에 들어가는 등)으로 인한 버그를 줄인다.

3. 외부의 혼돈을 막는 명확한 완충 경계(Boundary) 설정

  • 개념: 서드파티 API(Stripe, Slack, GitHub 등)의 응답 데이터나 데이터베이스의 로우(Raw) 데이터, 프레임워크에 종속적인 요청(Request) 객체가 핵심 도메인 로직에 그대로 노출되지 않도록 완충 어댑터(Mapper/Adapter)를 두는 방식이다.
  • 장점: 외부 API 필드가 임의로 변경되어도 전체 내부 로직이 깨지지 않고 어댑터의 맵핑 코드만 수정하면 되므로 외부의 혼돈이 누수(leakage)되는 것을 완벽히 방어한다.

4. 올바르지 않은 상태가 발생하는 것 자체를 아예 차단 (Prevent Invalid State)

  • 개념: 모든 필드를 옵셔널(?)로 설정하여 컴파일러의 경고만 우회하는 느슨한 설계를 피하고, TypeScript의 식별 가능한 유니온(Discriminated Union) 등을 사용해 상호 배제적이고 정밀한 상태를 표현한다.
  • 예시: 가입 중인 유저(DraftUser)와 영구 저장된 유저(SavedUser)를 결합하지 않고 분리함으로써, 이메일이 채워져 있는지, ID가 있는지 등의 불필요한 방어 코드 도배를 막고 잘못된 비즈니스 요청을 컴파일 타임에 즉시 잡아낸다.

5. 의사결정과 실제 실행의 명확한 분리 (Separation of Decision and Execution)

  • 개념: “판단하는 비즈니스 규칙(Pure Function)“과 “외부에 부수 효과를 일으키는 실행 로직(Side Effect - DB 쓰기, 결제, 이메일 전송 등)“을 분리한다.
  • 장점: 비즈니스 판단 규칙만 따로 떼어내서 테스트할 때 모킹(Mocking) 지옥에 빠지지 않고 독립적으로 완벽하게 검증(Unit Test)할 수 있다.

6. 다음 동료에게 이정표가 되는 명확한 에러 설계

  • 개념: 내부 인프라나 보안 취약점을 노출하지 않는 안전한 한편, 백엔드/프론트엔드 및 온콜(On-call) 담당자가 역추적할 수 있도록 기계적 식별 코드(code), 구체적 사유, 요청 ID(requestId)를 포함한 구조화된 에러를 제공한다.
  • 주의: 프론트엔드가 에러 메시지 텍스트를 구문 분석(error.message.includes("exists"))하게 방치하면 텍스트 수정 시 오작동하는 폭탄이 된다. 에러는 사람이 읽고, 에러 코드는 시스템이 식별하도록 구조화한다.

7. 로컬 동작 시연이 아니라 Git Diff를 배려하여 코딩

  • 개념: 로컬에서 겨우 동작만 완료하고 수백 줄의 리팩토링, 로직 수정, 마이그레이션이 한데 뒤엉킨 PR을 제출하는 대신, 코드 리뷰어가 안전하게 검토하고 필요시 부분 롤백할 수 있도록 Git 변경 이력(Diff)을 배려해 작업을 조각낸다.
  • 작업 조각 예시:
    • PR 1: 기능 수정 없이 결제 필드 이름 변경
    • PR 2: 환불 자격 확인 헬퍼 함수 및 테스트 추가
    • PR 3: 환불 자격 여부를 결제 흐름에 연동

[Aviation Rule] 10,000피트 규칙 (Sterile Cockpit Rule)

  • 비행 규칙의 기원: 1981년 미국 민간항공국(FAA)은 지상 활주(taxi), 이륙, 착륙을 포함하여 10,000피트 이하의 고도에서 비행하는 중요한 단계 동안 운항승무원이 안전 운항에 필요한 필수 업무 외에 사담, 서류 작업 등 주의를 분산시키는 일체의 활동을 금지하는 규칙(규칙 121.542, ‘운항승무원의 의무’)을 제정하였다. 이는 1974년 착륙 도중 조종석의 주의가 분산되어 72명의 목숨을 앗아간 휴먼 에러 사고의 뼈아픈 교훈에서 탄생한 통제다.
  • 주의력 관리와 0-1 모드: 10,000피트 이하의 고도에서는 타협이나 회색지대가 없는 선명한 이진법적 기준(0 또는 1)인 **0-1 모드**를 가동한다. 이는 정신적 자원을 핵심 문제 해결에 100% 쏟아붓도록 가이드라인을 세우고, 타협(예: “이번 한 번만”)에 의한 의지력 낭비와 의사결정 피로를 완전히 배제하는 효과가 있다. 시니어들의 코딩 패턴 또한 기발함보다 안전한 작동 경계를 설정하여 시스템 오용이나 주의 분산의 위험(Surprise)을 제어하는 본질을 공유한다.

완충 경계 및 에러 설계

  • 완충 경계 (Boundary): Stripe, Slack, GitHub 등 서드파티 API나 DB 로우 데이터가 도메인 깊숙이 박히지 않도록 mapBillingCustomer 같은 완충 어댑터(Mapper)를 두어, 외부 스펙 변경 시 어댑터만 수정하도록 설계 격리.
  • 추적 가능한 에러: Something went wrong 같은 모호한 문장을 지양하고, 시스템이 식별할 errorCode와 고유 requestId를 포함해 구조화하여 프론트엔드가 에러 텍스트를 구문 분석(error.message.includes("exists"))하지 않도록 한다.

1. 시니어 엔지니어의 7대 코딩 판단 규격

  • 1. 조기 반환 (Return Early): 함수의 정상(happy) 로직이 들여쓰기 5~6단계 아래에 동굴처럼 파묻히지 않도록, 실패 경로를 guard clauses로 최상단에서 우선 배출한다.
  • 2. 비즈니스 네aming: data, payload 같은 기술 우연적 이름은 유지보수 비용을 증가시킨다. activeUsers 대신 usersEligibleForReactivation 처럼 구현이 바뀌어도 존속하는 비즈니스 개념을 부여한다.
  • 3. 완충 경계 (Boundary): Stripe, Slack 등의 API 스펙 변경이 내부 코드를 망가뜨리지 않게 mapBillingCustomer 같은 매퍼/어댑터를 경계에 둔다. 환경 변수 파싱과 DB 로우의 무분별한 유출을 차단한다.
  • 4. 정직한 타입 모델링 (Prevent Invalid State): 모든 필드를 ?(optional)로 뭉뚱그려 방어 코드 도배를 유발하는 대신, DraftUserSavedUser처럼 유효하지 않은 상태 결합이 컴파일러 선에서 존재할 수 없도록 엄격히 모델링한다.
  • 5. 결정과 실행 분리: 결제 환불 판단(getRefundEligibility 순수 함수)과 실제 이메일 전송/DB 쓰기(부수 효과 실행)를 분할하여, 복잡한 목(Mock) 인프라 없이 비즈니스 규칙만 단독으로 유닛 테스트한다.
  • 6. 에러 통신 설계: 사용자에게 민감한 스택 정보를 덤프하지 않으며, 프론트엔드가 에러 텍스트를 구문 분석(error.message.includes("exists"))하지 않게 기계가 구별할 coderequestId, 로그용 컨텍스트를 구조화해 제공한다.
  • 7. Diff-driven 개발: 동작 확인만 끝낸 거대한 변경 사항을 한 PR에 내던지는 대신, 리뷰어의 가독성과 안전한 롤백을 위해 리팩토링과 기능 추가를 철저히 조각(PR 분할)내어 점진 배포한다.

예시

// [Separation of Decision & Execution 적용 예시]
// 의사결정: Pure Function (Mocking 필요 없음)
function getRefundEligibility(invoice: Invoice): RefundEligibility {  
  if (invoice.status !== "paid") {  
    return { allowed: false, reason: "Invoice is not paid" };  
  }  
  if (invoice.refundedAt) {  
    return { allowed: false, reason: "Invoice is already refunded" };  
  }  
  return { allowed: true };  
}
 
// 실제 실행: Impure Function (부수 효과 유발)
async function refundInvoice(invoiceId: string) {  
  const invoice = await getInvoice(invoiceId);  
  const eligibility = getRefundEligibility(invoice);  
  
  if (!eligibility.allowed) {  
    throw new ValidationError(eligibility.reason);  
  }  
  
  await paymentProvider.refund(invoice.paymentId);  
  await markInvoiceRefunded(invoice.id);  
}

Git Diff 배려 조각 PR 분할 예시

로컬 동작 시연 완료 후 대규모 리팩토링과 로직 수정을 뭉뚱그린 거대 PR을 지양하고, 코드 리뷰어의 피로와 롤백 위험을 제어하기 위해 의미 단위로 PR을 쪼갠다.

  • PR 1: 기능 수정 없이 결제 필드 이름 변경 (Refactoring)
  • PR 2: 환불 자격 확인 헬퍼 함수 및 테스트 추가 (Feature logic)
  • PR 3: 환불 자격 여부를 결제 흐름에 연동 (Integration)
  • PR 4: 환불 사유를 보여주도록 대시보드 UI 업데이트 (UI)

2. 가드 절(Guard Clauses) 및 의사결정 분리 타입스크립트 적용 사례

// 1. 의사결정: Pure Function (Mocking 불필요, 완벽한 단독 테스트 가능)
function getRefundEligibility(invoice: Invoice): RefundEligibility {
  if (invoice.status !== "paid") return { allowed: false, reason: "INVOICE_NOT_PAID" };
  if (invoice.refundedAt) return { allowed: false, reason: "ALREADY_REFUNDED" };
  if (invoice.amount <= 0) return { allowed: false, reason: "INVALID_AMOUNT" };
  return { allowed: true };
}
 
// 2. 가드 절 및 실행 연동: Impure Function
async function refundInvoice(invoiceId: string) {
  const invoice = await getInvoice(invoiceId); // 외부 DB
  if (!invoice) throw new NotFoundError("INVOICE_NOT_FOUND");
 
  const eligibility = getRefundEligibility(invoice);
  if (!eligibility.allowed) {
    throw new ValidationError(eligibility.reason);
  }
 
  await paymentProvider.refund(invoice.paymentId); // 외부 결제 API
  await markInvoiceRefunded(invoice.id); // DB 쓰기
}

충돌

  • 엄격한 경계 vs 단순 구조의 생산성: 소규모 프로토타입 앱에서 모든 외부 엔터티에 매핑 레이어(어댑터)를 추가하는 것은 오버엔지니어링일 수 있다. 그러나 데이터가 여러 레이어와 기능에 걸쳐 쓰이기 시작한다면 완충지대가 반드시 필요하다.

관련 노트