switch문이 내포하는 SRP/OCP 위반과 Abstract Class + Factory로 해결하는 방법
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); } }
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(); }
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) }; } }
boolean 파라미터가 있다는 건, 함수가 두 가지 일을 한다는 신호다.
public void SaveFile(string path, bool overwrite) { if (overwrite) OverwriteFile(path); else CreateNewFile(path); }
호출부에서
SaveFile(path, true)를
보는 순간, 독자는
true가 뭘 의미하는지 함수 내부로
들어가 확인해야 한다.
public void OverwriteFile(string path) { // 덮어쓰기 전용 로직 } public void CreateNewFile(string path) { // 신규 생성 전용 로직 }
함수 이름만 봐도 의도를 즉시 파악할 수 있다. 내부로 들어갈 필요 없음.
함수 이름이 약속하지 않은 일을 몰래 하는 것. Temporal Coupling의 원인.
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(); }
에러 처리와 비즈니스 로직은 서로 다른 레벨의 관심사다. 섞지 마라.
// 에러 처리 전담 — "어떻게 실패를 다룰까?" 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 교체/변경 시
여기만 수정.
함수를 작고, 명확하고, 단일 책임으로 만드는 네 가지 규칙
switch는 단 한 곳, Factory에만. 추상 클래스 + 다형성으로 OCP를 지킨다. 새 타입 추가 = 기존 코드 무수정.
bool 파라미터 = 함수가 두 가지 일을 한다는 신호. 두 개의 명확한 함수로 분리하라.
함수 이름이 약속하지 않은 일을 하면 Temporal Coupling이 생긴다. Command/Query를 분리하라 (CQS).
에러 처리와 비즈니스 로직은 다른 관심사. try-catch를 갖는 함수는 그것만 해야 한다.