Clean Code · Ch.03

Clean Functions

Switch문의 함정 → Factory 패턴

switch문이 내포하는 SRP/OCP 위반과 Abstract Class + Factory로 해결하는 방법

❌ BEFORE 문제 코드
public Money CalculatePay(Employee e)
{
  switch (e.Type)
  {
    case EmployeeType.Commissioned:
      return CalculateCommissionedPay(e);
    case EmployeeType.Hourly:
      return CalculateHourlyPay(e);
    case EmployeeType.Salaried:
      return CalculateSalariedPay(e);
    default:
      throw new InvalidEmployeeTypeException(e.Type);
  }
}
SRP 위반 OCP 위반 추상화 수준 혼재
✅ AFTER 해결: 추상 클래스
public abstract class Employee
{
  public abstract bool IsPayday();
  public abstract Money CalculatePay();
  public abstract void DeliverPay(Money pay);
}

public class CommissionedEmployee : Employee
{
  public override Money CalculatePay() { /* ... */ }
}

// ProcessPayroll은 절대 변하지 않음 ✓
public void ProcessPayroll(Employee e)
{
  var pay = e.CalculatePay();
}
OCP 준수 단일 책임 다형성 활용
Factory Pattern 객체 생성 책임 위임
public interface IEmployeeFactory
{
  Employee MakeEmployee(EmployeeRecord record);
}

public class EmployeeFactory : IEmployeeFactory
{
  public Employee MakeEmployee(EmployeeRecord record)
  {
    return record.Type switch
    {
      EmployeeType.Commissioned => new CommissionedEmployee(record),
      EmployeeType.Hourly       => new HourlyEmployee(record),
      EmployeeType.Salaried     => new SalariedEmployee(record),
      _                          => throw new InvalidEmployeeTypeException(record.Type)
    };
  }
}
UML — Abstract + Factory 구조
«interface» IEmployeeFactory
MakeEmployee(record)
«abstract» Employee
IsPayday()
CalculatePay()
DeliverPay(pay)
CommissionedEmployee
override CalculatePay()
HourlyEmployee
override CalculatePay()
SalariedEmployee
override CalculatePay()
핵심 인사이트 — switch는 단 한 곳(Factory)에만 존재해야 한다. 이후 새 타입을 추가할 때 기존 로직은 수정하지 않고 Employee 구현체 추가 + Factory 한 줄 추가만으로 끝난다. 이것이 OCP.

플래그 인수는 추하다

boolean 파라미터가 있다는 건, 함수가 두 가지 일을 한다는 신호다.

❌ BEFORE
public void SaveFile(string path, bool overwrite)
{
  if (overwrite)
    OverwriteFile(path);
  else
    CreateNewFile(path);
}
🔍

호출부에서 SaveFile(path, true)를 보는 순간, 독자는 true가 뭘 의미하는지 함수 내부로 들어가 확인해야 한다.

✅ AFTER 두 함수로 분리
public void OverwriteFile(string path)
{
  // 덮어쓰기 전용 로직
}

public void CreateNewFile(string path)
{
  // 신규 생성 전용 로직
}

함수 이름만 봐도 의도를 즉시 파악할 수 있다. 내부로 들어갈 필요 없음.

왜 추한가? — 두 가지 이유
① SRP 위반
플래그 = 내부에 if (flag) 분기 = 두 가지 행동. 하나의 함수가 하나의 책임을 가져야 한다는 원칙에 어긋난다.
② 추상화 수준 혼재
SaveFile은 고수준 이름인데, 내부는 overwrite냐 아니냐는 저수준 분기가 공존. 내려가기 규칙 위반.
SaveFile(path, true)
🚫 →
OverwriteFile(path)
+
CreateNewFile(path)
Rule — 함수에 bool/enum 파라미터가 보이면 즉시 의심하라. 두 함수로 쪼개야 할 신호일 가능성이 높다.

Side Effects — 숨겨진 결합

함수 이름이 약속하지 않은 일을 몰래 하는 것. Temporal Coupling의 원인.

❌ 문제 코드 CheckPassword에 숨겨진 Side Effect
public bool CheckPassword(string userName, string password)
{
  var user = UserGateway.FindByName(userName);
  if (user != User.Null)
  {
    string codedPhrase = user.GetPhraseEncodedByPassword();
    string phrase = _cryptographer.Decrypt(codedPhrase, password);

    if (phrase == "Valid Password")
    {
      Session.Initialize();  // ← 💣 Side Effect!
      return true;
    }
  }
  return false;
}
문제 분석
🎭

CheckPassword라는 이름은 "비밀번호 확인"만 약속한다. 그런데 내부에서 Session을 초기화한다 → 함수 이름이 거짓말을 하고 있음.

Temporal Coupling: 이 함수는 세션 초기화가 허용된 시점에만 호출해야 한다. 세션이 이미 있는 상태에서 호출하면 세션이 날아간다. 호출 순서가 강제된다.

해결 방향
// 방법 1: 이름에 명시
public bool CheckPasswordAndInitSession(
  string user, string pw)

// 방법 2: 책임 분리 (권장)
public bool CheckPassword(string user, string pw)
{
  // 순수하게 검증만
}

public void InitializeSession()
{
  Session.Initialize();
}
CQS 원칙 Command / Query 분리
CQS (Command-Query Separation) — 상태를 변경하는 함수(Command)와 값을 반환하는 함수(Query)를 분리하라. CheckPassword는 Query인데 Command를 숨기고 있다.

Try-Catch 블록 뽑아내기

에러 처리와 비즈니스 로직은 서로 다른 레벨의 관심사다. 섞지 마라.

✅ 분리된 구조
// 에러 처리 전담 — "어떻게 실패를 다룰까?"
public void delete(Page page)
{
  try
  {
    deletePageAndAllReferences(page);
  }
  catch (Exception e)
  {
    logError(e);
  }
}

// 실제 로직 전담 — "무엇을 할까?"
private void deletePageAndAllReferences(Page page) throws Exception
{
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}

// 로그 처리 전담
private void logError(Exception e)
{
  logger.log(e.getMessage());
}
각 함수의 단일 책임
🔴

delete()에러 처리 조율. try-catch만 존재. 실제 동작 모름.

🟢

deletePageAndAllReferences()비즈니스 로직. 예외를 throw만 함. 처리 방법 모름.

🔵

logError()로그 처리. logger 교체/변경 시 여기만 수정.

왜 이렇게 나눠야 하나?
try-catch 자체가 정상 흐름과 예외 흐름을 섞는 구조다. 이 안에 비즈니스 로직까지 넣으면 함수가 두 가지 이상을 책임지게 된다.
추상화 레벨 통일 —
delete()는 "삭제하고 에러 처리"
deletePageAndAllReferences()는 "삭제 상세"
각 함수는 같은 레벨의 이야기만 한다.
delete(page)
→ 위임
deletePageAndAll
References(page)
→ 실패 시
logError(e)

Ch.03 핵심 원칙 요약

함수를 작고, 명확하고, 단일 책임으로 만드는 네 가지 규칙

🔀

Switch → Factory

switch는 단 한 곳, Factory에만. 추상 클래스 + 다형성으로 OCP를 지킨다. 새 타입 추가 = 기존 코드 무수정.

🚩

Flag 인수 제거

bool 파라미터 = 함수가 두 가지 일을 한다는 신호. 두 개의 명확한 함수로 분리하라.

👻

Side Effect 제거

함수 이름이 약속하지 않은 일을 하면 Temporal Coupling이 생긴다. Command/Query를 분리하라 (CQS).

🧱

Try-Catch 분리

에러 처리와 비즈니스 로직은 다른 관심사. try-catch를 갖는 함수는 그것만 해야 한다.


핵심 원칙 연결 지도
SRP (Single Responsibility)
플래그 인수, Switch 남용, Side Effect 모두 SRP를 위반한다. 함수가 하나의 이유로만 변경되어야 한다.
OCP (Open/Closed Principle)
Switch를 Factory로 감싸면, 새 타입이 추가돼도 기존 로직은 닫혀(수정 불가) 있고 확장만 열려 있다.
추상화 수준 통일
하나의 함수 안에서 고수준 이름(SaveFile, CheckPassword)과 저수준 분기(if overwrite, Session.Initialize)가 공존하면 읽기 어렵다.
함수 = 한 가지 일
함수의 크기와 역할을 줄이면 테스트, 재사용, 독해 모두 쉬워진다. 이름이 모든 것을 설명해야 한다.
빠른 체크리스트