Skip to content

Split View: 클린 코드 실전 원칙 2025: 읽기 좋은 코드를 작성하는 30가지 규칙

✨ Learn with Quiz
|

클린 코드 실전 원칙 2025: 읽기 좋은 코드를 작성하는 30가지 규칙

들어가며

"동작하는 코드"와 "읽기 좋은 코드" 사이에는 넘을 수 없을 것 같은 간극이 있습니다. 대부분의 개발자는 기능 구현에 집중하느라 코드 품질을 후순위로 미루고, 결국 유지보수 비용이 눈덩이처럼 불어나는 경험을 합니다.

Robert C. Martin(Uncle Bob)의 "Clean Code"가 출판된 지 17년이 지났지만, 그 원칙들은 여전히 유효합니다. 다만 TypeScript, Python, Rust 같은 현대 언어와 도구들이 등장하면서 실천 방법이 진화했습니다.

이 글에서는 30가지 실전 규칙을 6개 카테고리로 나누어, 각각 Bad/Good 코드 비교와 함께 설명합니다. 모든 예제는 TypeScript와 Python으로 제공됩니다.

보이스카우트 규칙(Boy Scout Rule): 코드를 발견했을 때보다 더 깨끗한 상태로 남겨라.


카테고리 1: 네이밍 (Naming) — 6가지 규칙

규칙 1: 의도를 드러내는 이름을 사용하라

변수, 함수, 클래스의 이름만으로 "무엇을 하는지", "왜 존재하는지"를 알 수 있어야 합니다.

Bad:

// 무엇을 의미하는지 전혀 알 수 없다
const d: number = 86400;
const list1: number[] = [1, 2, 3];

function calc(a: number, b: number): number {
  return a * b * 0.1;
}

Good:

// 이름만 봐도 의미가 명확하다
const SECONDS_PER_DAY: number = 86400;
const activeUserIds: number[] = [1, 2, 3];

function calculateDiscountedPrice(originalPrice: number, quantity: number): number {
  const DISCOUNT_RATE = 0.1;
  return originalPrice * quantity * DISCOUNT_RATE;
}

Python 버전:

# Bad
d = 86400
lst = [1, 2, 3]

def calc(a, b):
    return a * b * 0.1

# Good
SECONDS_PER_DAY = 86400
active_user_ids = [1, 2, 3]

def calculate_discounted_price(original_price: float, quantity: int) -> float:
    DISCOUNT_RATE = 0.1
    return original_price * quantity * DISCOUNT_RATE

규칙 2: 축약어를 피하라

축약어는 작성자에게만 명확하고, 다른 개발자에게는 수수께끼입니다.

Bad:

const usrMgr = new UserManager();
const btnClkHndlr = () => {};
const genRpt = (dt: Date) => {};
const tmpUsr = getUser();

Good:

const userManager = new UserManager();
const handleButtonClick = () => {};
const generateReport = (reportDate: Date) => {};
const temporaryUser = getUser();  // 맥락상 임시 변수라면 이조차도 재고

허용되는 축약어: 업계 표준인 경우에만 — id, url, api, db, io, html, css, http

규칙 3: 검색 가능한 이름을 사용하라

매직 넘버나 한 글자 변수는 프로젝트 전체에서 검색이 불가능합니다.

Bad:

// 5가 무엇을 의미하는가? 프로젝트에서 '5'를 검색하면 수백 개 결과가 나온다
if (user.role === 5) {
  setTimeout(callback, 86400000);
}

Good:

const ADMIN_ROLE_ID = 5;
const ONE_DAY_IN_MILLISECONDS = 86400000;

if (user.role === ADMIN_ROLE_ID) {
  setTimeout(callback, ONE_DAY_IN_MILLISECONDS);
}

규칙 4: 발음 가능한 이름을 사용하라

코드 리뷰에서 이름을 소리 내어 읽을 수 있어야 합니다.

Bad:

class DtaRcrd102 {
  private genymdhms: Date;  // "젠와이엠디에이치엠에스"??
  private modymdhms: Date;
  private pszqint: number;
}

Good:

class Customer {
  private generationTimestamp: Date;
  private modificationTimestamp: Date;
  private recordId: number;
}

규칙 5: 일관된 어휘를 사용하라

동일한 개념에 여러 단어를 혼용하지 마세요.

Bad:

// 같은 개념에 fetch, retrieve, get, obtain을 혼용
class UserService {
  fetchUser(id: number) {}
  retrieveAccount(id: number) {}
  getUserProfile(id: number) {}
  obtainUserSettings(id: number) {}
}

Good:

// get으로 통일
class UserService {
  getUser(id: number) {}
  getAccount(id: number) {}
  getUserProfile(id: number) {}
  getUserSettings(id: number) {}
}

팀 네이밍 컨벤션 예시:

동작규칙예시
조회 (단건)get + 명사getUser, getOrder
조회 (목록)list + 명사(복수)listUsers, listOrders
생성create + 명사createUser, createOrder
수정update + 명사updateUser, updateOrder
삭제delete + 명사deleteUser, deleteOrder
존재 확인has/is + 형용사/명사hasPermission, isActive
변환to + 목적형태toJSON, toString

규칙 6: 스코프 길이에 비례하는 이름 길이

짧은 스코프에서는 짧은 이름이, 넓은 스코프에서는 설명적인 이름이 적합합니다.

Bad:

// 루프 변수에 긴 이름
for (let arrayIndex = 0; arrayIndex < items.length; arrayIndex++) {
  process(items[arrayIndex]);
}

// 전역 상수에 짧은 이름
const T = 3000;  // 무슨 타임아웃?

Good:

// 루프 변수는 짧게
for (let i = 0; i < items.length; i++) {
  process(items[i]);
}

// 전역/모듈 수준은 설명적으로
const API_REQUEST_TIMEOUT_MS = 3000;

카테고리 2: 함수 (Functions) — 6가지 규칙

규칙 7: 함수는 작게 만들어라

함수의 이상적인 길이는 20줄 이내입니다. 한 화면에 들어와야 합니다.

Bad:

function processOrder(order: Order): OrderResult {
  // 검증 (20줄)
  if (!order.items || order.items.length === 0) {
    throw new Error('No items');
  }
  if (!order.customer) {
    throw new Error('No customer');
  }
  // ... 더 많은 검증

  // 가격 계산 (30줄)
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  const shipping = total > 100 ? 0 : 10;
  // ... 더 많은 계산

  // 결제 처리 (20줄)
  const paymentResult = chargeCard(order.customer.cardId, total + tax + shipping);
  // ... 결제 로직

  // 알림 전송 (15줄)
  sendEmail(order.customer.email, 'Order confirmed');
  // ... 알림 로직

  return { orderId: generateId(), total, status: 'completed' };
}

Good:

function processOrder(order: Order): OrderResult {
  validateOrder(order);
  const pricing = calculatePricing(order.items);
  const paymentResult = processPayment(order.customer, pricing.total);
  notifyCustomer(order.customer, paymentResult);

  return createOrderResult(paymentResult, pricing);
}

function validateOrder(order: Order): void {
  if (!order.items?.length) throw new InvalidOrderError('No items');
  if (!order.customer) throw new InvalidOrderError('No customer');
}

function calculatePricing(items: OrderItem[]): Pricing {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const tax = subtotal * TAX_RATE;
  const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
  return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}

규칙 8: 한 가지만 하라 (Single Responsibility)

함수는 한 가지 일만 해야 하고, 그 일을 잘해야 합니다.

Bad:

function emailClients(clients: Client[]): void {
  for (const client of clients) {
    const record = database.lookup(client.id);
    if (record.isActive()) {
      email(client.email, 'Welcome!');
    }
  }
}

Good:

function emailActiveClients(clients: Client[]): void {
  const activeClients = getActiveClients(clients);
  activeClients.forEach(client => sendWelcomeEmail(client));
}

function getActiveClients(clients: Client[]): Client[] {
  return clients.filter(client => isClientActive(client.id));
}

function isClientActive(clientId: number): boolean {
  const record = database.lookup(clientId);
  return record.isActive();
}

function sendWelcomeEmail(client: Client): void {
  email(client.email, 'Welcome!');
}

규칙 9: 매개변수는 적게 유지하라 (이상적으로 2개 이하)

매개변수가 3개 이상이면 객체로 묶어 전달하세요.

Bad:

function createUser(
  name: string,
  email: string,
  age: number,
  role: string,
  department: string,
  isActive: boolean
): User {
  // ...
}

createUser('Alice', 'alice@example.com', 30, 'admin', 'engineering', true);

Good:

interface CreateUserParams {
  name: string;
  email: string;
  age: number;
  role: UserRole;
  department: string;
  isActive: boolean;
}

function createUser(params: CreateUserParams): User {
  // ...
}

createUser({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  role: UserRole.ADMIN,
  department: 'engineering',
  isActive: true,
});

규칙 10: 사이드 이펙트를 피하라

함수가 예상 밖의 동작을 하면 안 됩니다.

Bad:

let currentUser: User | null = null;

function checkPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  if (user && user.password === hash(password)) {
    // 사이드 이펙트! 비밀번호 확인 함수가 세션을 초기화한다
    currentUser = user;
    initializeSession(user);
    return true;
  }
  return false;
}

Good:

function verifyPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  return user !== null && user.password === hash(password);
}

function login(userName: string, password: string): LoginResult {
  if (!verifyPassword(userName, password)) {
    return { success: false };
  }
  const user = database.findUser(userName);
  const session = initializeSession(user);
  return { success: true, user, session };
}

규칙 11: 명령-쿼리 분리 (Command-Query Separation)

함수는 "무언가를 수행"하거나 "무언가를 반환"해야지, 둘 다 하면 안 됩니다.

Bad:

// set이 성공 여부도 반환하고, 값도 설정한다
function setAndCheckAttribute(name: string, value: string): boolean {
  const attribute = findAttribute(name);
  if (attribute) {
    attribute.value = value;
    return true;  // 설정 + 반환
  }
  return false;
}

// 호출하는 코드가 혼란스럽다
if (setAndCheckAttribute('username', 'alice')) {
  // ???
}

Good:

function attributeExists(name: string): boolean {
  return findAttribute(name) !== null;
}

function setAttribute(name: string, value: string): void {
  const attribute = findAttribute(name);
  if (!attribute) throw new AttributeNotFoundError(name);
  attribute.value = value;
}

// 호출하는 코드가 명확하다
if (attributeExists('username')) {
  setAttribute('username', 'alice');
}

규칙 12: DRY (Don't Repeat Yourself)

중복 코드는 버그의 온상입니다.

Bad:

function getAdminUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'admin')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

function getModeratorUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'moderator')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

Good:

function getUsersByRole(role: UserRole): User[] {
  const users = database.query('SELECT * FROM users WHERE role = ?', [role]);
  return users
    .map(addDisplayName)
    .sort(byDisplayName);
}

function addDisplayName(user: User): User {
  return { ...user, displayName: `${user.firstName} ${user.lastName}` };
}

function byDisplayName(a: User, b: User): number {
  return a.displayName.localeCompare(b.displayName);
}

카테고리 3: 주석 (Comments) — 4가지 규칙

규칙 13: "왜"를 설명하고, "무엇"은 코드에 맡겨라

코드 자체로 "무엇"을 표현하고, 주석은 "왜 이렇게 했는지"를 설명해야 합니다.

Bad:

// 사용자 이름을 가져온다
const userName = user.getName();

// 배열을 순회한다
for (const item of items) {
  // 아이템을 처리한다
  processItem(item);
}

// 1을 더한다
count += 1;

Good:

// 외부 API 제한으로 최대 100건까지만 요청 가능 (API docs: https://...)
const MAX_BATCH_SIZE = 100;

// 레거시 시스템 호환을 위해 ISO-8859-1로 인코딩해야 함
// 새 시스템 전환 후 UTF-8로 변경 예정 (JIRA-4521)
const encodedData = encode(data, 'ISO-8859-1');

// 동시성 이슈로 인해 락을 먼저 획득해야 함
// 자세한 내용: https://wiki.internal/concurrency-fix-2024
await acquireLock(resourceId);

규칙 14: 노이즈 주석을 피하라

아무런 정보도 추가하지 않는 주석은 코드를 더 읽기 어렵게 만듭니다.

Bad:

/**
 * 생성자
 */
constructor() {}

/**
 * id를 반환한다
 * @returns id
 */
getId(): number {
  return this.id;
}

// 기본 생성자
// 이 클래스는 사용자를 나타냅니다
// 작성자: 홍길동
// 작성일: 2024-01-01
// 수정일: 2024-06-15
// 수정자: 김철수

Good:

// 주석이 필요 없을 만큼 명확한 코드
class User {
  constructor(
    private readonly id: number,
    private readonly name: string,
  ) {}

  getId(): number {
    return this.id;
  }
}

규칙 15: TODO 주석은 표준 형식을 따르라

TODO는 해결해야 할 기술 부채를 추적하는 유용한 도구입니다.

Bad:

// TODO 나중에 고치기
// FIXME 이거 작동 안 함
// HACK 임시로 이렇게 함
// XXX 위험한 코드

Good:

// TODO(team-auth): OAuth2 토큰 갱신 로직 추가 [JIRA-1234] @2025-Q2
// FIXME(alice): 동시 요청 시 race condition 발생. 분산 락 도입 필요 [JIRA-5678]
// HACK: Safari 17.2 flexbox 버그 워크어라운드. Safari 18에서 수정 예정 후 제거

TODO 주석 형식:

// TODO(담당자/팀): 설명 [이슈번호] @예상시기
// FIXME(담당자): 버그 설명 [이슈번호]
// HACK: 워크어라운드 설명. 제거 조건 명시

규칙 16: JSDoc/Docstring을 활용하라

공개 API에는 구조화된 문서를 작성하세요.

TypeScript (JSDoc):

/**
 * 주어진 기간 동안의 사용자 활동 통계를 계산합니다.
 *
 * @param userId - 통계를 조회할 사용자 ID
 * @param startDate - 조회 시작일 (포함)
 * @param endDate - 조회 종료일 (포함)
 * @returns 활동 통계 객체
 * @throws {UserNotFoundError} 사용자가 존재하지 않는 경우
 * @throws {InvalidDateRangeError} 시작일이 종료일보다 나중인 경우
 *
 * @example
 * const stats = await getUserActivityStats('user-123', '2025-01-01', '2025-03-31');
 * console.log(stats.totalLogins); // 45
 */
async function getUserActivityStats(
  userId: string,
  startDate: string,
  endDate: string,
): Promise<ActivityStats> {
  // ...
}

Python (Docstring):

def get_user_activity_stats(
    user_id: str,
    start_date: str,
    end_date: str,
) -> ActivityStats:
    """주어진 기간 동안의 사용자 활동 통계를 계산합니다.

    Args:
        user_id: 통계를 조회할 사용자 ID
        start_date: 조회 시작일 (포함, ISO 8601 형식)
        end_date: 조회 종료일 (포함, ISO 8601 형식)

    Returns:
        ActivityStats: 활동 통계 객체

    Raises:
        UserNotFoundError: 사용자가 존재하지 않는 경우
        InvalidDateRangeError: 시작일이 종료일보다 나중인 경우

    Example:
        >>> stats = get_user_activity_stats('user-123', '2025-01-01', '2025-03-31')
        >>> stats.total_logins
        45
    """
    ...

카테고리 4: 에러 처리 (Error Handling) — 4가지 규칙

규칙 17: 에러 코드 대신 예외를 사용하라

에러 코드는 호출자에게 즉각적인 처리를 강제하여 코드를 복잡하게 만듭니다.

Bad:

function withdraw(account: Account, amount: number): number {
  if (amount > account.balance) return -1;  // 잔액 부족
  if (amount < 0) return -2;                // 음수 금액
  if (account.isFrozen) return -3;          // 계좌 동결

  account.balance -= amount;
  return 0;  // 성공
}

// 호출하는 코드
const result = withdraw(myAccount, 100);
if (result === -1) {
  // 잔액 부족 처리
} else if (result === -2) {
  // 음수 금액 처리
} else if (result === -3) {
  // 동결 계좌 처리
}

Good:

class InsufficientFundsError extends Error {
  constructor(public readonly balance: number, public readonly amount: number) {
    super(`Insufficient funds: balance ${balance}, requested ${amount}`);
    this.name = 'InsufficientFundsError';
  }
}

class AccountFrozenError extends Error {
  constructor(public readonly accountId: string) {
    super(`Account ${accountId} is frozen`);
    this.name = 'AccountFrozenError';
  }
}

function withdraw(account: Account, amount: number): void {
  if (amount < 0) throw new InvalidAmountError(amount);
  if (account.isFrozen) throw new AccountFrozenError(account.id);
  if (amount > account.balance) throw new InsufficientFundsError(account.balance, amount);

  account.balance -= amount;
}

// 호출하는 코드
try {
  withdraw(myAccount, 100);
} catch (error) {
  if (error instanceof InsufficientFundsError) {
    showAlert(`잔액이 부족합니다. 현재 잔액: ${error.balance}`);
  }
}

규칙 18: 빠르게 실패하라 (Fail Fast)

잘못된 상태를 최대한 빨리 감지하고, 즉시 알려야 합니다.

Bad:

function processPayment(orderId: string, amount: number): PaymentResult {
  // ... 100줄의 로직 ...

  // 마지막에서야 입력값 검증
  if (!orderId) {
    return { success: false, error: 'Invalid order ID' };
  }
  if (amount < 0) {
    return { success: false, error: 'Invalid amount' };
  }

  // ... 실제 결제 처리 ...
}

Good:

function processPayment(orderId: string, amount: number): PaymentResult {
  // 가드 절(Guard Clause): 가장 먼저 검증
  if (!orderId) throw new InvalidArgumentError('orderId is required');
  if (amount <= 0) throw new InvalidArgumentError('amount must be positive');

  // 핵심 로직만 남는다
  const order = orderRepository.findById(orderId);
  if (!order) throw new OrderNotFoundError(orderId);

  return paymentGateway.charge(order, amount);
}

규칙 19: 커스텀 에러를 정의하라

비즈니스 로직에 맞는 에러 계층 구조를 만드세요.

// 에러 계층 구조
abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly isOperational: boolean;

  constructor(message: string, public readonly cause?: Error) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly isOperational = true;
}

class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly isOperational = true;

  constructor(
    message: string,
    public readonly field: string,
    public readonly value: unknown,
  ) {
    super(message);
  }
}

class DatabaseError extends AppError {
  readonly statusCode = 500;
  readonly isOperational = false;  // 시스템 에러 — 운영자에게 알림
}

Python 버전:

class AppError(Exception):
    """애플리케이션 기본 에러"""
    status_code: int = 500
    is_operational: bool = True

    def __init__(self, message: str, cause: Exception | None = None):
        super().__init__(message)
        self.cause = cause


class NotFoundError(AppError):
    status_code = 404


class ValidationError(AppError):
    status_code = 400

    def __init__(self, message: str, field: str, value: object):
        super().__init__(message)
        self.field = field
        self.value = value


class DatabaseError(AppError):
    status_code = 500
    is_operational = False

규칙 20: 범용 예외를 catch하지 마라

catch (error) 또는 except Exception은 예상치 못한 에러를 숨깁니다.

Bad:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  // 모든 에러를 무시한다! 네트워크 에러? DB 에러? 타입 에러? 전부 삼킨다
  console.log('Something went wrong');
}

Good:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  if (error instanceof NetworkError) {
    logger.warn('Network error fetching user data', { userId, error });
    await retryWithBackoff(() => fetchUserData(userId));
  } else if (error instanceof ValidationError) {
    logger.error('Invalid user data', { userId, field: error.field });
    throw error;  // 상위로 전파
  } else if (error instanceof DatabaseError) {
    logger.fatal('Database error', { error });
    alertOpsTeam(error);
    throw error;
  } else {
    // 예상하지 못한 에러는 반드시 re-throw
    throw error;
  }
}

카테고리 5: 코드 구조화 (Code Organization) — 5가지 규칙

규칙 21: 수직 형식을 지켜라 (Vertical Formatting)

연관된 코드는 가까이, 비연관 코드는 빈 줄로 구분하세요.

Bad:

import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';
const MAX_RETRIES = 3;
const logger = new Logger('UserService');
export class UserService {
  private db: Database;
  private repo: UserRepository;
  constructor(db: Database) {
    this.db = db;
    this.repo = new UserRepository(db);
  }
  async getUser(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  }
  async createUser(data: CreateUserDTO): Promise<User> {
    const existing = await this.repo.findByEmail(data.email);
    if (existing) throw new ConflictError('Email already exists');
    return this.repo.create(data);
  }
}

Good:

import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';

const MAX_RETRIES = 3;
const logger = new Logger('UserService');

export class UserService {
  private db: Database;
  private repo: UserRepository;

  constructor(db: Database) {
    this.db = db;
    this.repo = new UserRepository(db);
  }

  async getUser(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  }

  async createUser(data: CreateUserDTO): Promise<User> {
    const existing = await this.repo.findByEmail(data.email);
    if (existing) throw new ConflictError('Email already exists');
    return this.repo.create(data);
  }
}

규칙 22: 신문 기사 은유 (Newspaper Metaphor)

파일을 위에서 아래로 읽을 때, 추상도가 점점 내려가야 합니다.

// 1. 가장 먼저: 퍼블릭 인터페이스 (헤드라인)
export class OrderProcessor {
  async processOrder(orderId: string): Promise<OrderResult> {
    const order = await this.validateAndFetchOrder(orderId);
    const pricing = this.calculatePricing(order);
    return this.executePayment(order, pricing);
  }

  // 2. 중간: 비즈니스 로직 (핵심 내용)
  private async validateAndFetchOrder(orderId: string): Promise<Order> {
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new OrderNotFoundError(orderId);
    this.validateOrderState(order);
    return order;
  }

  private calculatePricing(order: Order): Pricing {
    const subtotal = this.sumItems(order.items);
    const discount = this.applyDiscounts(subtotal, order.coupons);
    const tax = this.calculateTax(subtotal - discount);
    return { subtotal, discount, tax, total: subtotal - discount + tax };
  }

  // 3. 마지막: 세부 구현 (디테일)
  private validateOrderState(order: Order): void { /* ... */ }
  private sumItems(items: OrderItem[]): number { /* ... */ }
  private applyDiscounts(amount: number, coupons: Coupon[]): number { /* ... */ }
  private calculateTax(amount: number): number { /* ... */ }
}

규칙 23: Import 정리

Import 문은 일관된 규칙으로 정렬하세요.

// 1. Node.js 내장 모듈
import { readFile } from 'fs/promises';
import path from 'path';

// 2. 외부 라이브러리 (node_modules)
import express from 'express';
import { z } from 'zod';

// 3. 내부 모듈 — 절대 경로
import { AppConfig } from '@/config';
import { Logger } from '@/utils/logger';

// 4. 내부 모듈 — 상대 경로
import { UserService } from './services/user';
import { OrderService } from './services/order';

// 5. 타입 Import (TypeScript)
import type { User, Order } from './types';

ESLint 자동 정렬 설정:

{
  "rules": {
    "import/order": [
      "error",
      {
        "groups": ["builtin", "external", "internal", "parent", "sibling", "type"],
        "newlines-between": "always",
        "alphabetize": { "order": "asc" }
      }
    ]
  }
}

규칙 24: 파일 크기를 제한하라

기준권장경고위험
파일 줄 수200줄 이하200~400줄400줄 이상
함수 줄 수20줄 이하20~50줄50줄 이상
클래스 메서드 수10개 이하10~20개20개 이상
매개변수 수2개 이하3~4개5개 이상
중첩 깊이2단계 이하3단계4단계 이상

규칙 25: 모듈 경계를 명확히 하라

src/
├── modules/
│   ├── user/
│   │   ├── index.ts          # 퍼블릭 APIexport
│   │   ├── user.service.ts
│   │   ├── user.repository.ts
│   │   ├── user.controller.ts
│   │   ├── user.types.ts
│   │   └── __tests__/
│   │       ├── user.service.test.ts
│   │       └── user.repository.test.ts
│   ├── order/
│   │   ├── index.ts
│   │   ├── order.service.ts
│   │   └── ...
│   └── payment/
│       ├── index.ts
│       └── ...
├── shared/
│   ├── errors/
│   ├── utils/
│   └── types/
└── infrastructure/
    ├── database/
    ├── cache/
    └── messaging/

핵심 원칙: 모듈은 index.ts를 통해서만 외부에 공개합니다. 내부 구현은 숨깁니다.

// modules/user/index.ts — 퍼블릭 API만 노출
export { UserService } from './user.service';
export type { User, CreateUserDTO } from './user.types';
// UserRepository는 export하지 않는다 — 내부 구현

카테고리 6: 테스트 (Testing) — 5가지 규칙

규칙 26: FIRST 원칙

원칙설명
Fast테스트는 빠르게 실행되어야 한다 (ms 단위)
Independent테스트 간 의존성이 없어야 한다
Repeatable어떤 환경에서든 동일한 결과
Self-validatingPass/Fail이 자동으로 판단되어야 한다
Timely프로덕션 코드 직전/직후에 작성

규칙 27: AAA 패턴 (Arrange-Act-Assert)

describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      // Arrange — 테스트 데이터 준비
      const userRepo = new InMemoryUserRepository();
      const service = new UserService(userRepo);
      const userData: CreateUserDTO = {
        name: 'Alice',
        email: 'alice@example.com',
      };

      // Act — 테스트 대상 실행
      const result = await service.createUser(userData);

      // Assert — 결과 검증
      expect(result.name).toBe('Alice');
      expect(result.email).toBe('alice@example.com');
      expect(result.id).toBeDefined();
      expect(await userRepo.count()).toBe(1);
    });
  });
});

규칙 28: 하나의 테스트에 하나의 Assert 개념

Bad:

it('should handle user lifecycle', async () => {
  // 생성, 조회, 수정, 삭제를 모두 테스트
  const user = await service.createUser(data);
  expect(user.id).toBeDefined();

  const fetched = await service.getUser(user.id);
  expect(fetched.name).toBe(data.name);

  await service.updateUser(user.id, { name: 'Bob' });
  const updated = await service.getUser(user.id);
  expect(updated.name).toBe('Bob');

  await service.deleteUser(user.id);
  await expect(service.getUser(user.id)).rejects.toThrow();
});

Good:

describe('UserService', () => {
  it('should create user and return with generated id', async () => {
    const user = await service.createUser(validUserData);
    expect(user.id).toBeDefined();
    expect(user.name).toBe(validUserData.name);
  });

  it('should retrieve existing user by id', async () => {
    const created = await service.createUser(validUserData);
    const fetched = await service.getUser(created.id);
    expect(fetched).toEqual(created);
  });

  it('should update user name', async () => {
    const user = await service.createUser(validUserData);
    await service.updateUser(user.id, { name: 'Bob' });
    const updated = await service.getUser(user.id);
    expect(updated.name).toBe('Bob');
  });

  it('should throw NotFoundError when getting deleted user', async () => {
    const user = await service.createUser(validUserData);
    await service.deleteUser(user.id);
    await expect(service.getUser(user.id)).rejects.toThrow(NotFoundError);
  });
});

규칙 29: 테스트를 읽기 쉽게 작성하라

Bad:

it('test1', async () => {
  const r = await s.calc({ a: 100, b: 2, c: 0.1, d: true });
  expect(r).toBe(180);
});

Good:

it('should apply 10% discount when premium member orders 2 items at 100 each', async () => {
  // Given
  const order = createOrder({
    items: [{ price: 100, quantity: 2 }],
    isPremiumMember: true,
    discountRate: 0.1,
  });

  // When
  const totalPrice = await pricingService.calculateTotal(order);

  // Then
  expect(totalPrice).toBe(180); // (100 * 2) - 10% discount = 180
});

규칙 30: 테스트 이름은 시나리오를 설명하라

네이밍 패턴: should [예상 결과] when [조건]

describe('PasswordValidator', () => {
  // Good: 시나리오가 명확하다
  it('should return valid when password has 8+ chars with mixed case and numbers', () => {});
  it('should return invalid when password is shorter than 8 characters', () => {});
  it('should return invalid when password lacks uppercase letters', () => {});
  it('should return invalid when password lacks numbers', () => {});
  it('should throw EmptyPasswordError when password is empty string', () => {});
});

Python (pytest) 스타일:

class TestPasswordValidator:
    def test_valid_password_with_mixed_case_and_numbers(self):
        """8자 이상, 대소문자 혼합, 숫자 포함 시 유효"""
        ...

    def test_invalid_when_shorter_than_8_chars(self):
        """8자 미만 비밀번호는 무효"""
        ...

    def test_raises_error_when_password_is_empty(self):
        """빈 문자열 입력 시 에러 발생"""
        ...

코드 냄새 카탈로그 (Code Smells)

Martin Fowler의 리팩토링에서 정의한 대표적인 코드 냄새와 해결책입니다.

코드 냄새증상해결책
Long Method함수가 50줄 이상Extract Method
Large Class클래스의 책임이 3가지 이상Extract Class
Primitive Obsession원시 타입만으로 도메인 표현Value Object 도입
Long Parameter List매개변수 4개 이상Parameter Object
Data Clumps여러 곳에서 같은 데이터 그룹이 반복Extract Class
Feature Envy다른 클래스의 데이터를 과도하게 사용Move Method
Shotgun Surgery하나의 변경이 여러 클래스에 영향Move Method, Inline Class
Divergent Change하나의 클래스가 다양한 이유로 변경Extract Class
Comments코드를 설명하는 주석이 과도Rename, Extract Method
Dead Code사용되지 않는 코드삭제
Speculative Generality미래를 위한 과도한 추상화Collapse Hierarchy
Duplicated Code같은 코드가 2곳 이상Extract Method, Template Method

리팩토링 기법 Before/After

기법 1: 가드 절(Guard Clause)로 중첩 제거

Before:

function getPaymentAmount(employee: Employee): number {
  let result: number;
  if (employee.isSeparated) {
    result = separatedAmount();
  } else {
    if (employee.isRetired) {
      result = retiredAmount();
    } else {
      result = normalPayAmount();
    }
  }
  return result;
}

After:

function getPaymentAmount(employee: Employee): number {
  if (employee.isSeparated) return separatedAmount();
  if (employee.isRetired) return retiredAmount();
  return normalPayAmount();
}

기법 2: 전략 패턴으로 조건문 제거

Before:

function calculateShippingCost(order: Order): number {
  if (order.shippingMethod === 'standard') {
    return order.weight * 1.5;
  } else if (order.shippingMethod === 'express') {
    return order.weight * 3.0 + 5;
  } else if (order.shippingMethod === 'overnight') {
    return order.weight * 5.0 + 15;
  } else if (order.shippingMethod === 'international') {
    return order.weight * 10.0 + 25;
  }
  throw new Error('Unknown shipping method');
}

After:

interface ShippingStrategy {
  calculate(weight: number): number;
}

const shippingStrategies: Record<string, ShippingStrategy> = {
  standard: { calculate: (w) => w * 1.5 },
  express: { calculate: (w) => w * 3.0 + 5 },
  overnight: { calculate: (w) => w * 5.0 + 15 },
  international: { calculate: (w) => w * 10.0 + 25 },
};

function calculateShippingCost(order: Order): number {
  const strategy = shippingStrategies[order.shippingMethod];
  if (!strategy) throw new UnknownShippingMethodError(order.shippingMethod);
  return strategy.calculate(order.weight);
}

기법 3: 파이프라인으로 루프 리팩토링

Before:

function getActiveAdminEmails(users: User[]): string[] {
  const result: string[] = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].isActive) {
      if (users[i].role === 'admin') {
        result.push(users[i].email.toLowerCase());
      }
    }
  }
  result.sort();
  return result;
}

After:

function getActiveAdminEmails(users: User[]): string[] {
  return users
    .filter(user => user.isActive)
    .filter(user => user.role === 'admin')
    .map(user => user.email.toLowerCase())
    .sort();
}

인지 복잡도 (Cognitive Complexity)

SonarQube에서 측정하는 인지 복잡도는 코드를 "이해하는 데 필요한 정신적 노력"을 수치화합니다.

인지 복잡도 증가 요인

요인증가량예시
if, else if, else+1분기
switch case+1분기
for, while, do-while+1반복
catch+1예외 처리
break/continue to label+1흐름 변경
중첩 (nesting)+1 per level중첩된 if/for
논리 연산자 체인 변경+1a && b || c
재귀 호출+1자기 참조

인지 복잡도 예시

// 인지 복잡도: 13 (높음!)
function processData(items: Item[]): Result[] {   // +0
  const results: Result[] = [];
  for (const item of items) {                     // +1 (루프)
    if (item.isValid) {                           // +2 (if + 중첩1)
      if (item.type === 'A') {                    // +3 (if + 중첩2)
        results.push(processTypeA(item));
      } else if (item.type === 'B') {             // +1 (else if)
        for (const sub of item.children) {        // +4 (루프 + 중첩3)
          if (sub.isActive) {                     // +5 (if + 중첩4) -- 이미 이해 불가
            results.push(processTypeB(sub));
          }
        }
      }
    }
  }
  return results;                                 // 총: 1+2+3+1+4+5 = 16
}

리팩토링 후 인지 복잡도: 4

function processData(items: Item[]): Result[] {
  return items
    .filter(item => item.isValid)
    .flatMap(item => processByType(item));
}

function processByType(item: Item): Result[] {
  switch (item.type) {                            // +1
    case 'A': return [processTypeA(item)];
    case 'B': return processActiveBChildren(item);
    default: return [];
  }
}

function processActiveBChildren(item: Item): Result[] {
  return item.children
    .filter(child => child.isActive)
    .map(child => processTypeB(child));
}

권장 임계값:

수준인지 복잡도조치
좋음0~5유지
경고6~10리팩토링 검토
위험11~15반드시 리팩토링
심각16 이상즉시 리팩토링

코드 리뷰 체크리스트

PR 리뷰 시 확인할 항목

## 네이밍
- [ ] 변수/함수/클래스 이름이 의도를 드러내는가?
- [ ] 약어를 사용하지 않았는가?
- [ ] 팀 네이밍 컨벤션을 따르는가?

## 함수
- [ ] 함수가 한 가지 일만 하는가?
- [ ] 매개변수가 3개 이하인가?
- [ ] 사이드 이펙트가 없는가?

## 에러 처리
- [ ] 적절한 커스텀 에러를 사용하는가?
- [ ] 에러를 삼키지 않는가?
- [ ] 가드 절로 빠르게 실패하는가?

## 테스트
- [ ] 새 기능에 대한 테스트가 있는가?
- [ ] 엣지 케이스를 커버하는가?
- [ ] 테스트 이름이 시나리오를 설명하는가?

## 코드 구조
- [ ] 파일이 400줄을 넘지 않는가?
- [ ] 중첩 깊이가 3단계 이하인가?
- [ ] DRY 원칙을 따르는가?

## 보안
- [ ] 사용자 입력을 검증하는가?
- [ ] SQL 인젝션, XSS 가능성은 없는가?
- [ ] 민감 정보를 로그에 남기지 않는가?

SOLID 원칙 빠른 참조

S — 단일 책임 원칙 (Single Responsibility Principle)

// Bad: 한 클래스가 여러 책임
class UserService {
  createUser(data: CreateUserDTO) {}      // 비즈니스 로직
  sendEmail(to: string, body: string) {}  // 이메일 전송
  generatePDF(user: User) {}              // PDF 생성
  logAction(action: string) {}            // 로깅
}

// Good: 각 클래스가 하나의 책임
class UserService { createUser(data: CreateUserDTO) {} }
class EmailService { send(to: string, body: string) {} }
class PDFGenerator { generate(user: User) {} }
class AuditLogger { log(action: string) {} }

O — 개방-폐쇄 원칙 (Open/Closed Principle)

// 확장에는 열려 있고, 수정에는 닫혀 있어야 한다
interface PaymentProcessor {
  process(amount: number): Promise<PaymentResult>;
}

class StripeProcessor implements PaymentProcessor {
  async process(amount: number) { /* Stripe API */ }
}

class PayPalProcessor implements PaymentProcessor {
  async process(amount: number) { /* PayPal API */ }
}

// 새 결제 수단 추가 시 기존 코드 수정 없이 새 클래스만 추가
class CryptoProcessor implements PaymentProcessor {
  async process(amount: number) { /* Crypto API */ }
}

L — 리스코프 치환 원칙 (Liskov Substitution Principle)

// 하위 타입은 상위 타입을 대체할 수 있어야 한다
class Bird {
  fly(): void { /* 날아간다 */ }
}

// Bad: 펭귄은 날 수 없으므로 Bird를 상속하면 안 된다
class Penguin extends Bird {
  fly(): void { throw new Error('Cannot fly!'); }
}

// Good: 인터페이스 분리
interface FlyingBird { fly(): void; }
interface SwimmingBird { swim(): void; }

class Eagle implements FlyingBird { fly() {} }
class Penguin implements SwimmingBird { swim() {} }

I — 인터페이스 분리 원칙 (Interface Segregation Principle)

// Bad: 뚱뚱한 인터페이스
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
}

// Good: 분리된 인터페이스
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Meetable { attendMeeting(): void; }

class Developer implements Workable, Feedable, Meetable {
  work() {}
  eat() {}
  attendMeeting() {}
}

class Robot implements Workable {
  work() {}
  // eat(), attendMeeting() 필요 없음
}

D — 의존성 역전 원칙 (Dependency Inversion Principle)

// Bad: 고수준 모듈이 저수준 모듈에 직접 의존
class OrderService {
  private mysqlDb = new MySQLDatabase();  // 구체 클래스에 의존

  save(order: Order) {
    this.mysqlDb.insert('orders', order);
  }
}

// Good: 추상화에 의존
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {}

  async save(order: Order) {
    await this.repo.save(order);
  }
}

// 구체 구현은 별도로
class MySQLOrderRepository implements OrderRepository { /* ... */ }
class MongoOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* ... */ }  // 테스트용

실전 퀴즈

퀴즈 1: 다음 코드의 문제점은 무엇인가요?
function proc(lst: any[], flg: boolean): any[] {
  let res: any[] = [];
  for (let i = 0; i < lst.length; i++) {
    if (flg) {
      if (lst[i].a > 0) {
        res.push(lst[i].a * 2);
      }
    } else {
      if (lst[i].b !== null) {
        res.push(lst[i].b);
      }
    }
  }
  return res;
}

문제점:

  1. 네이밍: proc, lst, flg, res, a, b 모두 의미 없는 이름
  2. 타입: any 타입 남용
  3. 함수 책임: 플래그로 동작이 달라지는 건 두 개의 함수가 필요하다는 신호
  4. 중첩 깊이: 3단계 중첩
  5. 명령형 루프: filter/map 파이프라인으로 대체 가능

리팩토링:

function getDoubledPositiveAmounts(transactions: Transaction[]): number[] {
  return transactions
    .filter(t => t.amount > 0)
    .map(t => t.amount * 2);
}

function getNonNullBalances(accounts: Account[]): number[] {
  return accounts
    .filter(a => a.balance !== null)
    .map(a => a.balance);
}
퀴즈 2: 이 에러 처리 코드를 개선하세요.
async function getData(id: string) {
  try {
    const res = await fetch(`/api/data/${id}`);
    const data = await res.json();
    return data;
  } catch (e) {
    console.log(e);
    return null;
  }
}

문제점:

  1. 모든 에러를 삼킨다 (catch에서 null 반환)
  2. 에러 로깅이 console.log로 부실하다
  3. HTTP 상태 코드를 확인하지 않는다
  4. 반환 타입이 불명확하다 (null 가능)

리팩토링:

class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly endpoint: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchDataById(id: string): Promise<UserData> {
  const endpoint = `/api/data/${id}`;
  const response = await fetch(endpoint);

  if (!response.ok) {
    throw new ApiError(
      `Failed to fetch data: ${response.statusText}`,
      response.status,
      endpoint,
    );
  }

  return response.json() as Promise<UserData>;
}
퀴즈 3: 이 클래스의 SOLID 위반을 찾으세요.
class ReportGenerator {
  generateReport(data: any[]): string {
    // 데이터 검증
    if (!data.length) throw new Error('No data');

    // HTML 생성
    let html = '<table>';
    for (const row of data) {
      html += `<tr><td>${row.name}</td><td>${row.value}</td></tr>`;
    }
    html += '</table>';

    // 파일 저장
    fs.writeFileSync('/tmp/report.html', html);

    // 이메일 전송
    sendEmail('admin@company.com', 'Report', html);

    return html;
  }
}

위반 사항:

  • SRP 위반: 검증, HTML 생성, 파일 저장, 이메일 전송 — 4가지 책임
  • OCP 위반: PDF 형식 추가 시 클래스 수정 필요
  • DIP 위반: fs, sendEmail에 직접 의존

리팩토링:

interface ReportFormatter {
  format(data: ReportData[]): string;
}

interface ReportExporter {
  export(content: string): Promise<void>;
}

class HtmlFormatter implements ReportFormatter {
  format(data: ReportData[]): string { /* ... */ }
}

class FileExporter implements ReportExporter {
  async export(content: string) { /* ... */ }
}

class EmailExporter implements ReportExporter {
  async export(content: string) { /* ... */ }
}

class ReportService {
  constructor(
    private formatter: ReportFormatter,
    private exporters: ReportExporter[],
  ) {}

  async generate(data: ReportData[]): Promise<string> {
    if (!data.length) throw new EmptyDataError();
    const content = this.formatter.format(data);
    await Promise.all(this.exporters.map(e => e.export(content)));
    return content;
  }
}
퀴즈 4: 다음 함수의 인지 복잡도를 계산하세요.
function calculatePrice(product: Product, user: User): number {
  let price = product.basePrice;

  if (user.isPremium) {                    // +1
    if (product.category === 'electronics') {  // +2 (중첩)
      price *= 0.8;
    } else {                                  // +1
      price *= 0.9;
    }
  } else {                                    // +1
    if (user.hasCoupon) {                     // +2 (중첩)
      price *= 0.95;
    }
  }

  if (product.isOnSale && user.isPremium) {   // +1
    price *= 0.9;
  }

  return price;
}

인지 복잡도: 8 (경고 수준, 리팩토링 검토 필요)

리팩토링:

function calculatePrice(product: Product, user: User): number {
  const basePrice = product.basePrice;
  const memberDiscount = getMemberDiscount(user, product.category);
  const couponDiscount = getCouponDiscount(user);
  const saleDiscount = getSaleDiscount(product, user);

  return basePrice * memberDiscount * couponDiscount * saleDiscount;
}

function getMemberDiscount(user: User, category: string): number {
  if (!user.isPremium) return 1;
  return category === 'electronics' ? 0.8 : 0.9;
}

function getCouponDiscount(user: User): number {
  return !user.isPremium && user.hasCoupon ? 0.95 : 1;
}

function getSaleDiscount(product: Product, user: User): number {
  return product.isOnSale && user.isPremium ? 0.9 : 1;
}
퀴즈 5: 이 테스트 코드를 개선하세요.
test('test1', () => {
  const s = new CalcService();
  expect(s.add(1, 2)).toBe(3);
  expect(s.add(-1, 1)).toBe(0);
  expect(s.add(0, 0)).toBe(0);
  expect(s.subtract(5, 3)).toBe(2);
  expect(s.multiply(3, 4)).toBe(12);
  expect(s.divide(10, 2)).toBe(5);
  expect(() => s.divide(1, 0)).toThrow();
});

문제점:

  1. 테스트 이름이 의미 없다
  2. 하나의 테스트에서 여러 기능을 검증
  3. 실패 시 어떤 검증이 실패했는지 파악 어려움

리팩토링:

describe('CalcService', () => {
  const calculator = new CalcService();

  describe('add', () => {
    it('should return sum of two positive numbers', () => {
      expect(calculator.add(1, 2)).toBe(3);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-1, 1)).toBe(0);
    });

    it('should return zero when both inputs are zero', () => {
      expect(calculator.add(0, 0)).toBe(0);
    });
  });

  describe('divide', () => {
    it('should return quotient of two numbers', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw DivisionByZeroError when divisor is zero', () => {
      expect(() => calculator.divide(1, 0)).toThrow(DivisionByZeroError);
    });
  });
});

도구와 자동화

ESLint + Prettier 설정

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "max-lines": ["warn", { "max": 400 }],
    "max-lines-per-function": ["warn", { "max": 50 }],
    "max-params": ["warn", { "max": 3 }],
    "max-depth": ["warn", { "max": 3 }],
    "complexity": ["warn", { "max": 10 }],
    "no-else-return": "error",
    "prefer-const": "error",
    "no-var": "error"
  }
}

SonarQube Quality Gate

# sonar-project.properties
sonar.qualitygate.conditions:
  - metric: cognitive_complexity
    operator: GT
    error: 15
  - metric: duplicated_lines_density
    operator: GT
    error: 3
  - metric: coverage
    operator: LT
    error: 80

참고 자료

  1. Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship" (2008)
  2. Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018, 2nd Edition)
  3. G. Ann Campbell, "Cognitive Complexity: A new way of measuring understandability" — SonarSource (2023)
  4. Google Engineering Practices — Code Review Guidelines
  5. Airbnb JavaScript Style Guide
  6. TypeScript Deep Dive — Style Guide
  7. PEP 8 — Python Style Guide
  8. "The Art of Readable Code" by Dustin Boswell and Trevor Foucher (2011)
  9. SonarQube Documentation — Cognitive Complexity
  10. ESLint Rules Reference
  11. Kent Beck, "Test Driven Development: By Example" (2002)
  12. Michael Feathers, "Working Effectively with Legacy Code" (2004)
  13. "A Philosophy of Software Design" by John Ousterhout (2018)
  14. Clean Code JavaScript — GitHub (ryanmcdermott/clean-code-javascript)

Clean Code Principles 2025: 30 Rules for Writing Readable Code

Introduction

There is a seemingly insurmountable gap between "working code" and "readable code." Most developers focus on feature implementation and push code quality to the back burner, eventually watching maintenance costs snowball out of control.

Robert C. Martin's "Clean Code" was published 17 years ago, yet its principles remain as relevant as ever. However, with the emergence of modern languages and tools like TypeScript, Python, and Rust, the ways we practice those principles have evolved.

In this guide, we present 30 practical rules across 6 categories, each with Bad/Good code comparisons. All examples are in TypeScript and Python.

The Boy Scout Rule: Always leave the code cleaner than you found it.


Category 1: Naming — 6 Rules

Rule 1: Use Intention-Revealing Names

Variable, function, and class names should tell you "what it does" and "why it exists."

Bad:

const d: number = 86400;
const list1: number[] = [1, 2, 3];

function calc(a: number, b: number): number {
  return a * b * 0.1;
}

Good:

const SECONDS_PER_DAY: number = 86400;
const activeUserIds: number[] = [1, 2, 3];

function calculateDiscountedPrice(originalPrice: number, quantity: number): number {
  const DISCOUNT_RATE = 0.1;
  return originalPrice * quantity * DISCOUNT_RATE;
}

Python version:

# Bad
d = 86400
lst = [1, 2, 3]

def calc(a, b):
    return a * b * 0.1

# Good
SECONDS_PER_DAY = 86400
active_user_ids = [1, 2, 3]

def calculate_discounted_price(original_price: float, quantity: int) -> float:
    DISCOUNT_RATE = 0.1
    return original_price * quantity * DISCOUNT_RATE

Rule 2: Avoid Abbreviations

Abbreviations are clear only to the author and are puzzles for everyone else.

Bad:

const usrMgr = new UserManager();
const btnClkHndlr = () => {};
const genRpt = (dt: Date) => {};

Good:

const userManager = new UserManager();
const handleButtonClick = () => {};
const generateReport = (reportDate: Date) => {};

Acceptable abbreviations: Industry standards only — id, url, api, db, io, html, css, http

Rule 3: Use Searchable Names

Magic numbers and single-letter variables are impossible to search across a project.

Bad:

if (user.role === 5) {
  setTimeout(callback, 86400000);
}

Good:

const ADMIN_ROLE_ID = 5;
const ONE_DAY_IN_MILLISECONDS = 86400000;

if (user.role === ADMIN_ROLE_ID) {
  setTimeout(callback, ONE_DAY_IN_MILLISECONDS);
}

Rule 4: Use Pronounceable Names

You should be able to say names aloud during code reviews.

Bad:

class DtaRcrd102 {
  private genymdhms: Date;
  private modymdhms: Date;
  private pszqint: number;
}

Good:

class Customer {
  private generationTimestamp: Date;
  private modificationTimestamp: Date;
  private recordId: number;
}

Rule 5: Use Consistent Vocabulary

Do not mix multiple words for the same concept.

Bad:

class UserService {
  fetchUser(id: number) {}
  retrieveAccount(id: number) {}
  getUserProfile(id: number) {}
  obtainUserSettings(id: number) {}
}

Good:

class UserService {
  getUser(id: number) {}
  getAccount(id: number) {}
  getUserProfile(id: number) {}
  getUserSettings(id: number) {}
}

Team naming convention example:

ActionConventionExample
Read (single)get + noungetUser, getOrder
Read (list)list + plural nounlistUsers, listOrders
Createcreate + nouncreateUser, createOrder
Updateupdate + nounupdateUser, updateOrder
Deletedelete + noundeleteUser, deleteOrder
Check existencehas/is + adjective/nounhasPermission, isActive
Convertto + target formtoJSON, toString

Rule 6: Name Length Should Match Scope

Short scopes get short names; wide scopes get descriptive names.

Bad:

// Long name for a loop variable
for (let arrayIndex = 0; arrayIndex < items.length; arrayIndex++) {
  process(items[arrayIndex]);
}

// Short name for a global constant
const T = 3000;

Good:

// Loop variables are short
for (let i = 0; i < items.length; i++) {
  process(items[i]);
}

// Global/module-level is descriptive
const API_REQUEST_TIMEOUT_MS = 3000;

Category 2: Functions — 6 Rules

Rule 7: Keep Functions Small

The ideal function length is under 20 lines. It should fit on one screen.

Bad:

function processOrder(order: Order): OrderResult {
  // Validation (20 lines)
  if (!order.items || order.items.length === 0) throw new Error('No items');
  if (!order.customer) throw new Error('No customer');
  // ... more validation

  // Price calculation (30 lines)
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  const shipping = total > 100 ? 0 : 10;
  // ... more calculations

  // Payment processing (20 lines)
  const paymentResult = chargeCard(order.customer.cardId, total + tax + shipping);
  // ... payment logic

  // Notification (15 lines)
  sendEmail(order.customer.email, 'Order confirmed');
  // ... notification logic

  return { orderId: generateId(), total, status: 'completed' };
}

Good:

function processOrder(order: Order): OrderResult {
  validateOrder(order);
  const pricing = calculatePricing(order.items);
  const paymentResult = processPayment(order.customer, pricing.total);
  notifyCustomer(order.customer, paymentResult);

  return createOrderResult(paymentResult, pricing);
}

function validateOrder(order: Order): void {
  if (!order.items?.length) throw new InvalidOrderError('No items');
  if (!order.customer) throw new InvalidOrderError('No customer');
}

function calculatePricing(items: OrderItem[]): Pricing {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const tax = subtotal * TAX_RATE;
  const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
  return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}

Rule 8: Do One Thing (Single Responsibility)

A function should do one thing, do it well, and do it only.

Bad:

function emailClients(clients: Client[]): void {
  for (const client of clients) {
    const record = database.lookup(client.id);
    if (record.isActive()) {
      email(client.email, 'Welcome!');
    }
  }
}

Good:

function emailActiveClients(clients: Client[]): void {
  const activeClients = getActiveClients(clients);
  activeClients.forEach(client => sendWelcomeEmail(client));
}

function getActiveClients(clients: Client[]): Client[] {
  return clients.filter(client => isClientActive(client.id));
}

function isClientActive(clientId: number): boolean {
  return database.lookup(clientId).isActive();
}

function sendWelcomeEmail(client: Client): void {
  email(client.email, 'Welcome!');
}

Rule 9: Keep Parameters Few (Ideally 2 or Fewer)

When you have 3 or more parameters, wrap them in an object.

Bad:

function createUser(
  name: string, email: string, age: number,
  role: string, department: string, isActive: boolean
): User { /* ... */ }

createUser('Alice', 'alice@example.com', 30, 'admin', 'engineering', true);

Good:

interface CreateUserParams {
  name: string;
  email: string;
  age: number;
  role: UserRole;
  department: string;
  isActive: boolean;
}

function createUser(params: CreateUserParams): User { /* ... */ }

createUser({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  role: UserRole.ADMIN,
  department: 'engineering',
  isActive: true,
});

Rule 10: Avoid Side Effects

Functions should not do unexpected things.

Bad:

let currentUser: User | null = null;

function checkPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  if (user && user.password === hash(password)) {
    // Side effect! A password check function initializes a session
    currentUser = user;
    initializeSession(user);
    return true;
  }
  return false;
}

Good:

function verifyPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  return user !== null && user.password === hash(password);
}

function login(userName: string, password: string): LoginResult {
  if (!verifyPassword(userName, password)) {
    return { success: false };
  }
  const user = database.findUser(userName);
  const session = initializeSession(user);
  return { success: true, user, session };
}

Rule 11: Command-Query Separation

A function should either "do something" or "return something," not both.

Bad:

function setAndCheckAttribute(name: string, value: string): boolean {
  const attribute = findAttribute(name);
  if (attribute) {
    attribute.value = value;
    return true;
  }
  return false;
}

// Confusing call site
if (setAndCheckAttribute('username', 'alice')) { /* ??? */ }

Good:

function attributeExists(name: string): boolean {
  return findAttribute(name) !== null;
}

function setAttribute(name: string, value: string): void {
  const attribute = findAttribute(name);
  if (!attribute) throw new AttributeNotFoundError(name);
  attribute.value = value;
}

// Clear call site
if (attributeExists('username')) {
  setAttribute('username', 'alice');
}

Rule 12: DRY (Don't Repeat Yourself)

Duplicate code is a breeding ground for bugs.

Bad:

function getAdminUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'admin')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

function getModeratorUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'moderator')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

Good:

function getUsersByRole(role: UserRole): User[] {
  const users = database.query('SELECT * FROM users WHERE role = ?', [role]);
  return users.map(addDisplayName).sort(byDisplayName);
}

function addDisplayName(user: User): User {
  return { ...user, displayName: `${user.firstName} ${user.lastName}` };
}

function byDisplayName(a: User, b: User): number {
  return a.displayName.localeCompare(b.displayName);
}

Category 3: Comments — 4 Rules

Rule 13: Explain "Why," Not "What"

Let the code express "what" and use comments to explain "why you did it this way."

Bad:

// Get the user name
const userName = user.getName();

// Iterate over the array
for (const item of items) {
  // Process the item
  processItem(item);
}

Good:

// External API limits batch requests to 100 items max (API docs: https://...)
const MAX_BATCH_SIZE = 100;

// Must encode as ISO-8859-1 for legacy system compatibility
// Will switch to UTF-8 after new system migration (JIRA-4521)
const encodedData = encode(data, 'ISO-8859-1');

// Lock must be acquired first due to concurrency issue
// Details: https://wiki.internal/concurrency-fix-2024
await acquireLock(resourceId);

Rule 14: Avoid Noise Comments

Comments that add no information make code harder to read.

Bad:

/**
 * Constructor
 */
constructor() {}

/**
 * Returns the id
 * @returns the id
 */
getId(): number {
  return this.id;
}

Good:

// Code so clear it needs no comments
class User {
  constructor(
    private readonly id: number,
    private readonly name: string,
  ) {}

  getId(): number {
    return this.id;
  }
}

Rule 15: TODOs Should Follow a Standard Format

TODOs are useful tools for tracking technical debt.

Bad:

// TODO fix later
// FIXME this doesn't work
// HACK temporary workaround

Good:

// TODO(team-auth): Add OAuth2 token refresh logic [JIRA-1234] @2025-Q2
// FIXME(alice): Race condition on concurrent requests. Need distributed lock [JIRA-5678]
// HACK: Safari 17.2 flexbox bug workaround. Remove after Safari 18 fix

Rule 16: Use JSDoc/Docstrings for Public APIs

TypeScript (JSDoc):

/**
 * Calculates user activity statistics for a given time period.
 *
 * @param userId - The user ID to query statistics for
 * @param startDate - Start date (inclusive)
 * @param endDate - End date (inclusive)
 * @returns Activity statistics object
 * @throws {UserNotFoundError} When the user does not exist
 * @throws {InvalidDateRangeError} When start date is after end date
 *
 * @example
 * const stats = await getUserActivityStats('user-123', '2025-01-01', '2025-03-31');
 * console.log(stats.totalLogins); // 45
 */
async function getUserActivityStats(
  userId: string,
  startDate: string,
  endDate: string,
): Promise<ActivityStats> {
  // ...
}

Python (Docstring):

def get_user_activity_stats(
    user_id: str,
    start_date: str,
    end_date: str,
) -> ActivityStats:
    """Calculate user activity statistics for a given time period.

    Args:
        user_id: The user ID to query statistics for
        start_date: Start date (inclusive, ISO 8601 format)
        end_date: End date (inclusive, ISO 8601 format)

    Returns:
        ActivityStats: Activity statistics object

    Raises:
        UserNotFoundError: When the user does not exist
        InvalidDateRangeError: When start date is after end date

    Example:
        >>> stats = get_user_activity_stats('user-123', '2025-01-01', '2025-03-31')
        >>> stats.total_logins
        45
    """
    ...

Category 4: Error Handling — 4 Rules

Rule 17: Use Exceptions Instead of Error Codes

Error codes force callers to handle errors immediately, making code convoluted.

Bad:

function withdraw(account: Account, amount: number): number {
  if (amount > account.balance) return -1;  // Insufficient funds
  if (amount < 0) return -2;                // Negative amount
  if (account.isFrozen) return -3;          // Frozen account

  account.balance -= amount;
  return 0;  // Success
}

const result = withdraw(myAccount, 100);
if (result === -1) { /* handle */ }
else if (result === -2) { /* handle */ }
else if (result === -3) { /* handle */ }

Good:

class InsufficientFundsError extends Error {
  constructor(public readonly balance: number, public readonly amount: number) {
    super(`Insufficient funds: balance ${balance}, requested ${amount}`);
    this.name = 'InsufficientFundsError';
  }
}

class AccountFrozenError extends Error {
  constructor(public readonly accountId: string) {
    super(`Account ${accountId} is frozen`);
    this.name = 'AccountFrozenError';
  }
}

function withdraw(account: Account, amount: number): void {
  if (amount < 0) throw new InvalidAmountError(amount);
  if (account.isFrozen) throw new AccountFrozenError(account.id);
  if (amount > account.balance) throw new InsufficientFundsError(account.balance, amount);

  account.balance -= amount;
}

Rule 18: Fail Fast

Detect invalid states as early as possible and report immediately.

Bad:

function processPayment(orderId: string, amount: number): PaymentResult {
  // ... 100 lines of logic ...

  // Validation only at the very end
  if (!orderId) return { success: false, error: 'Invalid order ID' };
  if (amount < 0) return { success: false, error: 'Invalid amount' };

  // ... actual payment processing ...
}

Good:

function processPayment(orderId: string, amount: number): PaymentResult {
  // Guard clauses: validate first
  if (!orderId) throw new InvalidArgumentError('orderId is required');
  if (amount <= 0) throw new InvalidArgumentError('amount must be positive');

  const order = orderRepository.findById(orderId);
  if (!order) throw new OrderNotFoundError(orderId);

  return paymentGateway.charge(order, amount);
}

Rule 19: Define Custom Errors

Build an error hierarchy that matches your business logic.

abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly isOperational: boolean;

  constructor(message: string, public readonly cause?: Error) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly isOperational = true;
}

class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly isOperational = true;

  constructor(message: string, public readonly field: string, public readonly value: unknown) {
    super(message);
  }
}

class DatabaseError extends AppError {
  readonly statusCode = 500;
  readonly isOperational = false;  // System error — alert ops
}

Rule 20: Never Catch Generic Exceptions

catch (error) or except Exception hides unexpected errors.

Bad:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  console.log('Something went wrong');  // Swallows everything
}

Good:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  if (error instanceof NetworkError) {
    logger.warn('Network error fetching user data', { userId, error });
    await retryWithBackoff(() => fetchUserData(userId));
  } else if (error instanceof ValidationError) {
    logger.error('Invalid user data', { userId, field: error.field });
    throw error;
  } else if (error instanceof DatabaseError) {
    logger.fatal('Database error', { error });
    alertOpsTeam(error);
    throw error;
  } else {
    throw error;  // Re-throw unexpected errors
  }
}

Category 5: Code Organization — 5 Rules

Rule 21: Respect Vertical Formatting

Related code stays close; unrelated code is separated by blank lines.

Bad:

import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';
const MAX_RETRIES = 3;
const logger = new Logger('UserService');
export class UserService {
  private db: Database;
  private repo: UserRepository;
  constructor(db: Database) {
    this.db = db;
    this.repo = new UserRepository(db);
  }
  async getUser(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  }
}

Good:

import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';

const MAX_RETRIES = 3;
const logger = new Logger('UserService');

export class UserService {
  private db: Database;
  private repo: UserRepository;

  constructor(db: Database) {
    this.db = db;
    this.repo = new UserRepository(db);
  }

  async getUser(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  }
}

Rule 22: The Newspaper Metaphor

When reading a file top to bottom, abstraction level should decrease progressively.

// 1. First: Public interface (headline)
export class OrderProcessor {
  async processOrder(orderId: string): Promise<OrderResult> {
    const order = await this.validateAndFetchOrder(orderId);
    const pricing = this.calculatePricing(order);
    return this.executePayment(order, pricing);
  }

  // 2. Middle: Business logic (body)
  private async validateAndFetchOrder(orderId: string): Promise<Order> {
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new OrderNotFoundError(orderId);
    this.validateOrderState(order);
    return order;
  }

  private calculatePricing(order: Order): Pricing {
    const subtotal = this.sumItems(order.items);
    const discount = this.applyDiscounts(subtotal, order.coupons);
    const tax = this.calculateTax(subtotal - discount);
    return { subtotal, discount, tax, total: subtotal - discount + tax };
  }

  // 3. Last: Implementation details
  private validateOrderState(order: Order): void { /* ... */ }
  private sumItems(items: OrderItem[]): number { /* ... */ }
  private applyDiscounts(amount: number, coupons: Coupon[]): number { /* ... */ }
  private calculateTax(amount: number): number { /* ... */ }
}

Rule 23: Organize Imports

Import statements should follow a consistent ordering convention.

// 1. Node.js built-in modules
import { readFile } from 'fs/promises';
import path from 'path';

// 2. External libraries (node_modules)
import express from 'express';
import { z } from 'zod';

// 3. Internal modules — absolute paths
import { AppConfig } from '@/config';
import { Logger } from '@/utils/logger';

// 4. Internal modules — relative paths
import { UserService } from './services/user';
import { OrderService } from './services/order';

// 5. Type imports (TypeScript)
import type { User, Order } from './types';

Rule 24: Limit File Size

MetricRecommendedWarningDanger
File linesUnder 200200-400Over 400
Function linesUnder 2020-50Over 50
Class methodsUnder 1010-20Over 20
Parameters2 or fewer3-45 or more
Nesting depth2 levels max3 levels4+ levels

Rule 25: Define Clear Module Boundaries

src/
├── modules/
│   ├── user/
│   │   ├── index.ts          # Only export public API
│   │   ├── user.service.ts
│   │   ├── user.repository.ts
│   │   ├── user.controller.ts
│   │   ├── user.types.ts
│   │   └── __tests__/
│   ├── order/
│   │   ├── index.ts
│   │   └── ...
│   └── payment/
│       ├── index.ts
│       └── ...
├── shared/
│   ├── errors/
│   ├── utils/
│   └── types/
└── infrastructure/
    ├── database/
    ├── cache/
    └── messaging/

Key principle: Modules expose only through index.ts. Internal implementation stays hidden.

// modules/user/index.ts — only the public API
export { UserService } from './user.service';
export type { User, CreateUserDTO } from './user.types';
// UserRepository is NOT exported — internal implementation

Category 6: Testing — 5 Rules

Rule 26: FIRST Principles

PrincipleDescription
FastTests run quickly (milliseconds)
IndependentNo dependencies between tests
RepeatableSame results in any environment
Self-validatingPass/Fail is determined automatically
TimelyWritten just before/after production code

Rule 27: AAA Pattern (Arrange-Act-Assert)

describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      // Arrange
      const userRepo = new InMemoryUserRepository();
      const service = new UserService(userRepo);
      const userData: CreateUserDTO = {
        name: 'Alice',
        email: 'alice@example.com',
      };

      // Act
      const result = await service.createUser(userData);

      // Assert
      expect(result.name).toBe('Alice');
      expect(result.email).toBe('alice@example.com');
      expect(result.id).toBeDefined();
    });
  });
});

Rule 28: One Assert Concept per Test

Bad:

it('should handle user lifecycle', async () => {
  const user = await service.createUser(data);
  expect(user.id).toBeDefined();
  const fetched = await service.getUser(user.id);
  expect(fetched.name).toBe(data.name);
  await service.updateUser(user.id, { name: 'Bob' });
  const updated = await service.getUser(user.id);
  expect(updated.name).toBe('Bob');
  await service.deleteUser(user.id);
  await expect(service.getUser(user.id)).rejects.toThrow();
});

Good:

describe('UserService', () => {
  it('should create user and return with generated id', async () => {
    const user = await service.createUser(validUserData);
    expect(user.id).toBeDefined();
    expect(user.name).toBe(validUserData.name);
  });

  it('should retrieve existing user by id', async () => {
    const created = await service.createUser(validUserData);
    const fetched = await service.getUser(created.id);
    expect(fetched).toEqual(created);
  });

  it('should update user name', async () => {
    const user = await service.createUser(validUserData);
    await service.updateUser(user.id, { name: 'Bob' });
    const updated = await service.getUser(user.id);
    expect(updated.name).toBe('Bob');
  });

  it('should throw NotFoundError when getting deleted user', async () => {
    const user = await service.createUser(validUserData);
    await service.deleteUser(user.id);
    await expect(service.getUser(user.id)).rejects.toThrow(NotFoundError);
  });
});

Rule 29: Write Readable Tests

Bad:

it('test1', async () => {
  const r = await s.calc({ a: 100, b: 2, c: 0.1, d: true });
  expect(r).toBe(180);
});

Good:

it('should apply 10% discount when premium member orders 2 items at 100 each', async () => {
  // Given
  const order = createOrder({
    items: [{ price: 100, quantity: 2 }],
    isPremiumMember: true,
    discountRate: 0.1,
  });

  // When
  const totalPrice = await pricingService.calculateTotal(order);

  // Then
  expect(totalPrice).toBe(180); // (100 * 2) - 10% discount = 180
});

Rule 30: Test Names Should Describe Scenarios

Naming pattern: should [expected result] when [condition]

describe('PasswordValidator', () => {
  it('should return valid when password has 8+ chars with mixed case and numbers', () => {});
  it('should return invalid when password is shorter than 8 characters', () => {});
  it('should return invalid when password lacks uppercase letters', () => {});
  it('should throw EmptyPasswordError when password is empty string', () => {});
});

Code Smells Catalog

Representative code smells from Martin Fowler's Refactoring and their solutions:

Code SmellSymptomSolution
Long MethodFunction over 50 linesExtract Method
Large ClassClass has 3+ responsibilitiesExtract Class
Primitive ObsessionDomain expressed only with primitivesValue Object
Long Parameter List4+ parametersParameter Object
Data ClumpsSame data group repeatedExtract Class
Feature EnvyExcessive use of another class's dataMove Method
Shotgun SurgeryOne change affects many classesMove Method, Inline Class
Divergent ChangeOne class changes for multiple reasonsExtract Class
CommentsExcessive code-explaining commentsRename, Extract Method
Dead CodeUnused codeDelete it
Speculative GeneralityOver-abstraction for the futureCollapse Hierarchy
Duplicated CodeSame code in 2+ placesExtract Method

Refactoring Techniques: Before/After

Technique 1: Guard Clauses to Remove Nesting

Before:

function getPaymentAmount(employee: Employee): number {
  let result: number;
  if (employee.isSeparated) {
    result = separatedAmount();
  } else {
    if (employee.isRetired) {
      result = retiredAmount();
    } else {
      result = normalPayAmount();
    }
  }
  return result;
}

After:

function getPaymentAmount(employee: Employee): number {
  if (employee.isSeparated) return separatedAmount();
  if (employee.isRetired) return retiredAmount();
  return normalPayAmount();
}

Technique 2: Strategy Pattern to Eliminate Conditionals

Before:

function calculateShippingCost(order: Order): number {
  if (order.shippingMethod === 'standard') {
    return order.weight * 1.5;
  } else if (order.shippingMethod === 'express') {
    return order.weight * 3.0 + 5;
  } else if (order.shippingMethod === 'overnight') {
    return order.weight * 5.0 + 15;
  } else if (order.shippingMethod === 'international') {
    return order.weight * 10.0 + 25;
  }
  throw new Error('Unknown shipping method');
}

After:

interface ShippingStrategy {
  calculate(weight: number): number;
}

const shippingStrategies: Record<string, ShippingStrategy> = {
  standard: { calculate: (w) => w * 1.5 },
  express: { calculate: (w) => w * 3.0 + 5 },
  overnight: { calculate: (w) => w * 5.0 + 15 },
  international: { calculate: (w) => w * 10.0 + 25 },
};

function calculateShippingCost(order: Order): number {
  const strategy = shippingStrategies[order.shippingMethod];
  if (!strategy) throw new UnknownShippingMethodError(order.shippingMethod);
  return strategy.calculate(order.weight);
}

Technique 3: Pipeline Refactoring from Loops

Before:

function getActiveAdminEmails(users: User[]): string[] {
  const result: string[] = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].isActive) {
      if (users[i].role === 'admin') {
        result.push(users[i].email.toLowerCase());
      }
    }
  }
  result.sort();
  return result;
}

After:

function getActiveAdminEmails(users: User[]): string[] {
  return users
    .filter(user => user.isActive)
    .filter(user => user.role === 'admin')
    .map(user => user.email.toLowerCase())
    .sort();
}

Cognitive Complexity

Cognitive complexity, as measured by SonarQube, quantifies the mental effort required to understand code.

Factors That Increase Cognitive Complexity

FactorIncrementExample
if, else if, else+1Branching
switch case+1Branching
for, while, do-while+1Looping
catch+1Exception handling
break/continue to label+1Flow change
Nesting+1 per levelNested if/for
Logical operator chain change+1a && b || c
Recursive call+1Self-reference
LevelCognitive ComplexityAction
Good0-5Maintain
Warning6-10Review for refactoring
Danger11-15Must refactor
Critical16+Refactor immediately

SOLID Principles Quick Reference

S — Single Responsibility Principle

// Bad: One class, multiple responsibilities
class UserService {
  createUser(data: CreateUserDTO) {}
  sendEmail(to: string, body: string) {}
  generatePDF(user: User) {}
  logAction(action: string) {}
}

// Good: One class, one responsibility
class UserService { createUser(data: CreateUserDTO) {} }
class EmailService { send(to: string, body: string) {} }
class PDFGenerator { generate(user: User) {} }
class AuditLogger { log(action: string) {} }

O — Open/Closed Principle

interface PaymentProcessor {
  process(amount: number): Promise<PaymentResult>;
}

class StripeProcessor implements PaymentProcessor {
  async process(amount: number) { /* Stripe API */ }
}

class PayPalProcessor implements PaymentProcessor {
  async process(amount: number) { /* PayPal API */ }
}

// Adding a new payment method requires no changes to existing code
class CryptoProcessor implements PaymentProcessor {
  async process(amount: number) { /* Crypto API */ }
}

L — Liskov Substitution Principle

// Bad: Penguin cannot fly, so it shouldn't extend Bird
class Bird { fly(): void { /* flies */ } }
class Penguin extends Bird {
  fly(): void { throw new Error('Cannot fly!'); }
}

// Good: Separate interfaces
interface FlyingBird { fly(): void; }
interface SwimmingBird { swim(): void; }

class Eagle implements FlyingBird { fly() {} }
class Penguin implements SwimmingBird { swim() {} }

I — Interface Segregation Principle

// Bad: Fat interface
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
}

// Good: Segregated interfaces
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Meetable { attendMeeting(): void; }

class Developer implements Workable, Feedable, Meetable {
  work() {} eat() {} attendMeeting() {}
}

class Robot implements Workable {
  work() {}
  // No need for eat() or attendMeeting()
}

D — Dependency Inversion Principle

// Bad: High-level module depends on low-level module
class OrderService {
  private mysqlDb = new MySQLDatabase();
  save(order: Order) { this.mysqlDb.insert('orders', order); }
}

// Good: Depend on abstractions
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {}
  async save(order: Order) { await this.repo.save(order); }
}

class MySQLOrderRepository implements OrderRepository { /* ... */ }
class MongoOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* ... */ }

Code Review Checklist

## Naming
- [ ] Do names reveal intent?
- [ ] No abbreviations?
- [ ] Follows team naming conventions?

## Functions
- [ ] Does each function do one thing?
- [ ] 3 or fewer parameters?
- [ ] No side effects?

## Error Handling
- [ ] Uses appropriate custom errors?
- [ ] Errors are not swallowed?
- [ ] Guard clauses for fail-fast?

## Tests
- [ ] Tests exist for new features?
- [ ] Edge cases covered?
- [ ] Test names describe scenarios?

## Code Structure
- [ ] File under 400 lines?
- [ ] Nesting depth 3 or fewer?
- [ ] DRY principle followed?

## Security
- [ ] User input validated?
- [ ] No SQL injection or XSS risks?
- [ ] No sensitive info in logs?

Practical Quiz

Quiz 1: What are the problems with this code?
function proc(lst: any[], flg: boolean): any[] {
  let res: any[] = [];
  for (let i = 0; i < lst.length; i++) {
    if (flg) {
      if (lst[i].a > 0) {
        res.push(lst[i].a * 2);
      }
    } else {
      if (lst[i].b !== null) {
        res.push(lst[i].b);
      }
    }
  }
  return res;
}

Problems:

  1. Naming: proc, lst, flg, res, a, b are all meaningless
  2. Types: any overuse
  3. Responsibility: A flag parameter signals two functions are needed
  4. Nesting depth: 3 levels deep
  5. Imperative loop: Can be replaced with filter/map pipeline

Refactored:

function getDoubledPositiveAmounts(transactions: Transaction[]): number[] {
  return transactions
    .filter(t => t.amount > 0)
    .map(t => t.amount * 2);
}

function getNonNullBalances(accounts: Account[]): number[] {
  return accounts
    .filter(a => a.balance !== null)
    .map(a => a.balance);
}
Quiz 2: Improve this error handling code.
async function getData(id: string) {
  try {
    const res = await fetch(`/api/data/${id}`);
    const data = await res.json();
    return data;
  } catch (e) {
    console.log(e);
    return null;
  }
}

Problems:

  1. Swallows all errors
  2. Poor logging with console.log
  3. Does not check HTTP status codes
  4. Return type is ambiguous (nullable)

Refactored:

class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly endpoint: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchDataById(id: string): Promise<UserData> {
  const endpoint = `/api/data/${id}`;
  const response = await fetch(endpoint);

  if (!response.ok) {
    throw new ApiError(
      `Failed to fetch data: ${response.statusText}`,
      response.status,
      endpoint,
    );
  }

  return response.json() as Promise<UserData>;
}
Quiz 3: Find the SOLID violations in this class.
class ReportGenerator {
  generateReport(data: any[]): string {
    if (!data.length) throw new Error('No data');
    let html = '<table>';
    for (const row of data) {
      html += `<tr><td>${row.name}</td><td>${row.value}</td></tr>`;
    }
    html += '</table>';
    fs.writeFileSync('/tmp/report.html', html);
    sendEmail('admin@company.com', 'Report', html);
    return html;
  }
}

Violations:

  • SRP: Validation, HTML generation, file saving, email sending — 4 responsibilities
  • OCP: Adding PDF format requires class modification
  • DIP: Direct dependency on fs and sendEmail

Refactored:

interface ReportFormatter { format(data: ReportData[]): string; }
interface ReportExporter { export(content: string): Promise<void>; }

class HtmlFormatter implements ReportFormatter {
  format(data: ReportData[]): string { /* ... */ }
}

class ReportService {
  constructor(
    private formatter: ReportFormatter,
    private exporters: ReportExporter[],
  ) {}

  async generate(data: ReportData[]): Promise<string> {
    if (!data.length) throw new EmptyDataError();
    const content = this.formatter.format(data);
    await Promise.all(this.exporters.map(e => e.export(content)));
    return content;
  }
}
Quiz 4: Calculate the cognitive complexity of this function.
function calculatePrice(product: Product, user: User): number {
  let price = product.basePrice;

  if (user.isPremium) {                         // +1
    if (product.category === 'electronics') {   // +2 (nesting)
      price *= 0.8;
    } else {                                    // +1
      price *= 0.9;
    }
  } else {                                      // +1
    if (user.hasCoupon) {                       // +2 (nesting)
      price *= 0.95;
    }
  }

  if (product.isOnSale && user.isPremium) {     // +1
    price *= 0.9;
  }

  return price;
}

Cognitive Complexity: 8 (warning level, consider refactoring)

Refactored:

function calculatePrice(product: Product, user: User): number {
  const basePrice = product.basePrice;
  const memberDiscount = getMemberDiscount(user, product.category);
  const couponDiscount = getCouponDiscount(user);
  const saleDiscount = getSaleDiscount(product, user);

  return basePrice * memberDiscount * couponDiscount * saleDiscount;
}

function getMemberDiscount(user: User, category: string): number {
  if (!user.isPremium) return 1;
  return category === 'electronics' ? 0.8 : 0.9;
}

function getCouponDiscount(user: User): number {
  return !user.isPremium && user.hasCoupon ? 0.95 : 1;
}

function getSaleDiscount(product: Product, user: User): number {
  return product.isOnSale && user.isPremium ? 0.9 : 1;
}
Quiz 5: Improve this test code.
test('test1', () => {
  const s = new CalcService();
  expect(s.add(1, 2)).toBe(3);
  expect(s.add(-1, 1)).toBe(0);
  expect(s.subtract(5, 3)).toBe(2);
  expect(s.multiply(3, 4)).toBe(12);
  expect(s.divide(10, 2)).toBe(5);
  expect(() => s.divide(1, 0)).toThrow();
});

Refactored:

describe('CalcService', () => {
  const calculator = new CalcService();

  describe('add', () => {
    it('should return sum of two positive numbers', () => {
      expect(calculator.add(1, 2)).toBe(3);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-1, 1)).toBe(0);
    });
  });

  describe('divide', () => {
    it('should return quotient of two numbers', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw DivisionByZeroError when divisor is zero', () => {
      expect(() => calculator.divide(1, 0)).toThrow(DivisionByZeroError);
    });
  });
});

Tools and Automation

ESLint + Prettier Configuration

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "max-lines": ["warn", { "max": 400 }],
    "max-lines-per-function": ["warn", { "max": 50 }],
    "max-params": ["warn", { "max": 3 }],
    "max-depth": ["warn", { "max": 3 }],
    "complexity": ["warn", { "max": 10 }],
    "no-else-return": "error",
    "prefer-const": "error",
    "no-var": "error"
  }
}

SonarQube Quality Gate

sonar.qualitygate.conditions:
  - metric: cognitive_complexity
    operator: GT
    error: 15
  - metric: duplicated_lines_density
    operator: GT
    error: 3
  - metric: coverage
    operator: LT
    error: 80

References

  1. Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship" (2008)
  2. Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018, 2nd Edition)
  3. G. Ann Campbell, "Cognitive Complexity: A new way of measuring understandability" — SonarSource (2023)
  4. Google Engineering Practices — Code Review Guidelines
  5. Airbnb JavaScript Style Guide
  6. TypeScript Deep Dive — Style Guide
  7. PEP 8 — Python Style Guide
  8. "The Art of Readable Code" by Dustin Boswell and Trevor Foucher (2011)
  9. SonarQube Documentation — Cognitive Complexity
  10. ESLint Rules Reference
  11. Kent Beck, "Test Driven Development: By Example" (2002)
  12. Michael Feathers, "Working Effectively with Legacy Code" (2004)
  13. "A Philosophy of Software Design" by John Ousterhout (2018)
  14. Clean Code JavaScript — GitHub (ryanmcdermott/clean-code-javascript)