Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

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

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:**

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:**

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 내장 모듈

// 2. 외부 라이브러리 (node_modules)

// 3. 내부 모듈 — 절대 경로

// 4. 내부 모듈 — 상대 경로

// 5. 타입 Import (TypeScript)

**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 # 퍼블릭 API만 export

│ │ ├── 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 원칙

| 원칙 | 설명 |

|------|------|

| **F**ast | 테스트는 빠르게 실행되어야 한다 (ms 단위) |

| **I**ndependent | 테스트 간 의존성이 없어야 한다 |

| **R**epeatable | 어떤 환경에서든 동일한 결과 |

| **S**elf-validating | Pass/Fail이 자동으로 판단되어야 한다 |

| **T**imely | 프로덕션 코드 직전/직후에 작성 |

규칙 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 |

| 논리 연산자 체인 변경 | +1 | `a && 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 { /* ... */ } // 테스트용

실전 퀴즈

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);

}

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>;

}

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;

}

}

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;

}

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)

현재 단락 (1/1250)

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

작성 글자: 0원문 글자: 33,111작성 단락: 0/1250