Files
2nd/10_Wiki/Topics/Coding/Quality_Code_Smells.md
T
2026-05-09 22:47:42 +09:00

8.6 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
quality-code-smells Code Smells — 변경 어려움의 signal Coding draft B conceptual 2026-05-09 2026-05-09
quality
refactoring
vibe-coding
language applicable_to
TS / generic
Engineering
code smell
long method
god object
primitive obsession
feature envy
shotgun surgery

Code Smells

코드 가 "이상하다" — bug 아닌 변경 어려움 / 이해 어려움 의 signal. Long method, god class, primitive obsession, feature envy, shotgun surgery, dead code.

📖 핵심 개념

  • Smell ≠ bug — 동작 OK, 변경 비용 ↑.
  • 매 smell 가 reasonable 일 때 있음 — 문맥.
  • Refactoring 가 답 — 작은 step.
  • Linter 가 일부 자동 감지.

💻 코드 패턴

Long method

// ❌ 100 줄 함수
function processOrder(order) {
  // validate
  if (!order.userId) throw ...;
  if (!order.items) throw ...;
  // ...
  
  // calculate total
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.qty;
  }
  // tax, discount, ...
  
  // save
  // ...
  
  // notify
  // ...
}
// ✅ Extract method
function processOrder(order) {
  validateOrder(order);
  const total = calculateTotal(order);
  saveOrder(order, total);
  notifyUser(order);
}

→ 한 함수 1 책임 — 30 줄 이하 권장.

God class

// ❌ User 가 모든 거
class User {
  login() { ... }
  logout() { ... }
  sendEmail() { ... }     // 메일 책임?
  generateInvoice() { ... } // 인보이스?
  trackAnalytics() { ... }  // 분석?
}
// ✅ 분리
class User { login(); logout(); }
class Mailer { sendEmail(user); }
class InvoiceService { generate(user); }

→ Single Responsibility. 매 class 1 reason to change.

Primitive obsession

// ❌ string 만
function transfer(fromId: string, toId: string, amount: number) { ... }
// → fromId 가 user / account / order? amount 가 cents / dollars?

// ✅ Branded / value object
type UserId = string & { __brand: 'UserId' };
type AccountId = string & { __brand: 'AccountId' };

class Money {
  constructor(public cents: number, public currency: string) {}
  add(other: Money): Money { ... }
}

function transfer(from: AccountId, to: AccountId, amount: Money) { ... }

Feature envy

// ❌ Order 가 user 의 data 만 사용
class OrderService {
  total(order: Order, user: User) {
    let t = order.total;
    if (user.isVip) t *= 0.9;
    if (user.country === 'US') t += 0.08 * t;
    return t;
  }
}
// ✅ User method
class User {
  applyDiscount(amount: number): number {
    let r = amount;
    if (this.isVip) r *= 0.9;
    if (this.country === 'US') r += 0.08 * r;
    return r;
  }
}

class OrderService {
  total(order: Order, user: User) {
    return user.applyDiscount(order.total);
  }
}

Shotgun surgery

1 변경 = N 파일 수정.

예: "User 의 status 가 'banned' 추가"
- User type
- 모든 query (deleted=false → status='active')
- 모든 UI badge
- ...

→ Single responsibility 위반. Encapsulate.
// ✅ Status 가 1 곳에
class UserStatus {
  static active(): UserStatus { ... }
  static banned(): UserStatus { ... }
  isVisible(): boolean { ... }
  badge(): { color: string; text: string } { ... }
}

Long parameter list

// ❌
function createUser(
  name: string, email: string, password: string,
  birthdate: Date, country: string, city: string,
  newsletter: boolean, ...
) { ... }
// ✅ Parameter object
interface CreateUserInput {
  name: string;
  email: string;
  // ...
}

function createUser(input: CreateUserInput) { ... }

Data clump

// ❌ 같은 3 param 자주
function shipping(street, city, zip) { ... }
function billing(street, city, zip) { ... }

// ✅ Address class
class Address { ... }
function shipping(addr: Address) { ... }

Comments (smell)

// ❌ 코드 가 이해 안 됨 → comment 로 보충
// Calculate total with tax and discount
function fn(o, u) {
  let t = o.t;
  // VIP discount
  if (u.v) t *= 0.9;
  // ...
}

// ✅ 변수 / 함수 이름
function calculateTotalWithTaxAndDiscount(order, user) {
  let total = order.subtotal;
  total = applyVipDiscount(total, user);
  // ...
}

→ Comment = 코드 가 부족 의 signal. Naming 으로 fix.

Dead code

// ❌ 안 쓰이는 함수
function oldMethod() { ... }  // 아무도 호출 X

// ✅ 삭제
// Git history 가 보존

→ "혹시" 안 둠. Lint 가 감지 (no-unused-vars).

Duplicate code

// ❌ 같은 logic 두 곳
function validateUserA(user) {
  if (!user.email) throw ...;
  if (!user.email.includes('@')) throw ...;
}
function validateUserB(user) {
  if (!user.email) throw ...;
  if (!user.email.includes('@')) throw ...;
}

// ✅ Extract
function validateEmail(email: string) {
  if (!email) throw ...;
  if (!email.includes('@')) throw ...;
}

→ Rule of three: 3 번 = abstract. 2 번 = OK.

Switch / if cascade

// ❌ Type 별 분기 폭발
switch (animal.type) {
  case 'dog': return bark(animal);
  case 'cat': return meow(animal);
  case 'cow': return moo(animal);
  // 새 type → 모든 switch 수정 (shotgun surgery)
}
// ✅ Polymorphism
interface Animal { sound(): string; }
class Dog implements Animal { sound() { return 'bark'; } }
class Cat implements Animal { sound() { return 'meow'; } }

animal.sound();

Magic number / string

// ❌
if (user.role === 3) ...
setTimeout(fn, 86400000);

// ✅
const ROLE_ADMIN = 3;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;

Lazy class (작은 class)

// ❌ 의미 없는 wrapper
class UserName { 
  constructor(public name: string) {} 
}

// ✅ string 직접 — 쓰기 nothing 추가 시.
// 단, validation / brand 가 있으면 유지.

Speculative generality

// ❌ "혹시 나중" 추상
abstract class BaseHandler { abstract process(input: any): any; }
class SingleConcreteHandler extends BaseHandler { ... }
// → 1 구현 만 — abstract 의미 없음

// ✅ YAGNI. Concrete first, abstract when 2nd 구현.

Refused bequest

// ❌ 상속 거부
class Bird { fly() {} }
class Penguin extends Bird {
  fly() { throw new Error('penguins cant fly'); }  // 상속 거부
}

// ✅ 다른 추상
interface Animal { ... }
class Bird implements Animal { fly() {} }
class Penguin implements Animal { swim() {} }

Inappropriate intimacy

// ❌ 다른 class 의 internal 접근
class Order {
  total(user: User) {
    return this.amount * user._private.discount;  // private 접근
  }
}

// ✅ Public method
class User { discount(): number { return this._private.discount; } }

Train wreck / Law of Demeter

// ❌ 깊은 chain
const city = order.user.address.city.name;

// ✅ Tell, don't ask
const city = order.userCity();

→ "한 dot rule". 내부 구조 노출 X.

Temporal coupling

// ❌ 순서 dependency
const x = new X();
x.init();   // 잊으면 깨짐
x.process();

// ✅ Constructor 가 init
const x = new X();  // 자동 init
x.process();

Linter / 자동 감지

# eslint-plugin-sonarjs (smell)
yarn add -D eslint-plugin-sonarjs

# rules: cognitive-complexity, no-duplicate-string, no-identical-functions, etc.

Sonar / CodeClimate

- Cyclomatic complexity > 10
- Cognitive complexity > 15
- Duplication > 5%
- File LOC > 500
- Method LOC > 30

→ 자동 보고.

Refactor 시점

"Smell 발견 → 즉시 fix" 가 ideal.
실제: feature 작업 중 발견 → 별 PR (boy scout rule).

큰 refactor = 따로 sprint.

🤔 의사결정 기준

Smell Refactor
Long method Extract method
God class Extract class / SRP
Primitive obsession Value object / branded
Feature envy Move method
Shotgun surgery Encapsulate
Long params Parameter object
Data clump Class
Duplicate Extract method (3+)
Switch on type Polymorphism
Magic number Constant
Train wreck Tell don't ask

안티패턴

  • 모든 smell 즉시 fix: 큰 refactor 가 PR 막힘.
  • 무시: 누적 = 변경 어려움.
  • Smell 가 패턴 으로 위장: e.g. "이건 strategy 인데" — 1 구현.
  • Comment 가 변수명 대체: code 가 분명해야.
  • YAGNI 무시 (speculative): 안 쓰이는 추상.
  • DRY 강박 (premature abstract): 2번 = OK, 3번 = extract.

🤖 LLM 활용 힌트

  • LLM 가 smell 찾기 강함 (PR review).
  • "Boy scout rule" — 작은 fix 매번.
  • Linter (sonarjs) 자동 감지.
  • Refactor = 작은 commit + test 보존.

🔗 관련 문서