스터디 기록

내 코드가 그렇게 이상한가요? - 6장 : 조건 분기 (C# 정리)

informsec 2026. 9. 7. 02:58

6장에서 나오는 악마는 다음과 같다.

  1. 조건 분기의 중첩으로 인해 낮아지는 가독성
  2. switch 조건문 중복
  3. 조건 분기의 중복과 중첩
  4. 자료형 확인에 조건 분기 사용하지 않기
  5. 플래그 매개변수

악마를 줄이는 방법을 하나씩 알아보자.

 

책에는 Java로 정리되어 있기에, 예시 코드를 C#으로 바꿔서 정리해보았다. 

 

1. 중첩되는 조건 분기 제거하기

 

문제 상황

조건문(if)이 여러 겹 중첩되면 코드의 가독성이 크게 떨어집니다. 각 조건문의 범위를 파악하기 힘들고, 중첩 구조 사이사이에 코드가 섞여 있으면 특정 조건에서 어떤 로직이 처리되는지 파악하기 어려워 버그가 발생하기 쉽습니다.

 

// 살아 있는가
if (0 < member.HitPoint) 
{
    // 움직일 수 있는가
    if (member.CanAct()) 
    {
        // 매직 포인트가 남아 있는가
        if (magic.CostMagicPoint <= member.MagicPoint) 
        {
            member.ConsumeMagicPoint(magic.CostMagicPoint); // 매직 포인트 소비
            member.Chant(magic); // 마법 발동
        }
    }
}

 

해결 방법

조기 리턴(early return)을 사용하여 조건 분기의 중첩을 제거합니다. 조건을 반전시켜, 만족하지 않을 때 미리 처리를 종료(return)하는 방식입니다.

 

// 살아 있지 않은 경우 처리를 종료
if (member.HitPoint <= 0) return;

// 움직일 수 없다면 처리를 종료
if (!member.CanAct()) return;

// 매직 포인트가 부족하다면 처리를 종료
if (member.MagicPoint < magic.CostMagicPoint) return;

member.ConsumeMagicPoint(magic.CostMagicPoint);
member.Chant(magic);

 

조기 리턴을 적용하면 코드가 평탄해져 가독성이 높아집니다. '조건을 확인하는 로직'을 상단에 모으고, '실제 실행하는 로직'을 하단으로 분리할 수 있어 게임 개발 시 새로운 마법이나 상태 이상 조건을 추가할 때 매우 유연하게 대처할 수 있습니다. else가 남발된 복잡한 분기 역시 동일한 방식으로 개선 가능합니다.

 

// 개선 후 (조기 리턴)
if (hitPointRate == 0) 
	return HealthCondition.Dead;
if (hitPointRate < 0.3) 
	return HealthCondition.Danger;
if (hitPointRate < 0.5) 
	return HealthCondition.Caution;
    
return HealthCondition.Fine;

 

2. switch 조건문 중복 제거하기

 

문제 상황

값의 종류에 따라 처리를 다르게 할 때 switch문을 주로 사용하지만, 요구사항이 추가될 때 치명적인 버그를 유발하기 쉽습니다.

 

enum MagicType 
{
    Fire, // 불 계열 마법
    Lightning // 번개 계열 마법
}

// 마법의 이름
string GetName(MagicType magicType) 
{
    string name = "";
    switch (magicType) 
    {
        case MagicType.Fire: name = "파이어"; break;
        case MagicType.Lightning: name = "라이트닝"; break;
    }
    return name;
}

// 매직포인트 소비량
int CostMagicPoint(MagicType magicType, Member member) 
{
    int magicPoint = 0;
    switch (magicType) 
    {
        case MagicType.Fire: magicPoint = 2; break;
        case MagicType.Lightning: magicPoint = 5 + (int)(member.Level * 0.2); break;
    }
    return magicPoint;
}

 

새로운 마법이 추가되어야 할 때 관련된 모든 메서드의 switch문에 case를 일일이 추가해야 하며, 단 한 곳이라도 수정이 누락되면 바로 버그로 직결됩니다.

 

해결 방법

인터페이스(Interface)를 활용한 전략 패턴(Strategy Pattern)으로 전환합니다.

💡 인터페이스(Interface)란?
클래스가 반드시 구현해야 하는 '행동의 규약'입니다. 내부에 구체적인 로직은 없고 메서드의 이름, 매개변수, 반환 타입만 정의해 둡니다. 서로 다른 클래스(Fire, Lightning)라도 같은 인터페이스(IMagic)를 구현하기만 하면, 호출하는 입장에서는 내부가 어떻게 생겼는지 몰라도 동일한 방식(IMagic.Name())으로 사용할 수 있게 해주는 강력한 도구입니다. C#에서는 이름 앞에 보통 I를 붙여 명명합니다.

 

공통으로 필요한 기능(이름, 매직포인트 소비량, 공격력)을 인터페이스의 메서드로 정의하고, 각 마법 타입이 이를 구현하도록 분리합니다.

 

interface IMagic 
{
    string Name();
    int CostMagicPoint();
    int AttackPower();
}

class Fire : IMagic 
{
    private readonly Member _member;
    public Fire(Member member) { _member = member; }
    
    public string Name() => "파이어";
    public int CostMagicPoint() => 2;
    public int AttackPower() => 20 + (int)(_member.Level * 0.5);
}

// Lightning, HellFire 클래스 등도 위와 동일하게 IMagic 구현

 

분기 처리는 Dictionary를 활용해 객체를 매핑하는 방식으로 대체합니다. (책에서는 Java의 Map을 사용했지만, C#에서는 Dictionary를 사용합니다. Dictionary는 키(Key)와 값(Value)을 한 쌍으로 저장하는 자료구조로, 특정 키를 넣으면 그에 매핑된 값을 즉시 꺼내올 수 있어 조건 분기를 완벽하게 대체할 수 있습니다.)

 

readonly Dictionary<MagicType, IMagic> _magics = new Dictionary<MagicType, IMagic>();

public void InitMagics(Member member)
{
    _magics.Add(MagicType.Fire, new Fire(member));
    _magics.Add(MagicType.Lightning, new Lightning(member));
}

// 마법 공격 실행하기
void MagicAttack(MagicType magicType) 
{
    IMagic usingMagic = _magics[magicType]; // 매핑된 인스턴스 추출
    ShowMagicName(usingMagic);
    ConsumeMagicPoint(usingMagic);
    MagicDamage(usingMagic);
}

 

새로운 타입을 추가할 때 인터페이스에 정의된 메서드 구현을 하나라도 빠뜨리면 컴파일 오류가 발생하므로, 개발자의 실수(누락)를 컴파일 단계에서 차단할 수 있습니다.

 

3. 자료형 확인에 조건 분기 사용하지 않기

문제 상황

도형의 넓이를 구하는 것처럼 동일한 작업을 수행하지만 각기 다른 클래스를 사용할 때, 객체의 실제 자료형을 확인하기 위해 is (Java의 instanceof) 키워드로 분기 처리를 하면 코드가 매우 번거로워집니다.

 

double GetArea(object shape) 
{
    if (shape is Rectangle rect) 
    {
        return rect.Area();
    }
    if (shape is Circle circle) 
    {
        return circle.Area();
    }
    return 0;
}

 

해결 방법

마찬가지로 공통 인터페이스를 추출하여, 서로 다른 자료형을 같은 자료형처럼 다룰 수 있게 구성합니다.

 

interface IShape 
{
    double Area();
}

class Rectangle : IShape 
{
    private readonly double _width;
    private readonly double _height;
    public Rectangle(double width, double height) { _width = width; _height = height; }
    
    public double Area() => _width * _height;
}

class Circle : IShape 
{
    private readonly double _radius;
    public Circle(double radius) { _radius = radius; }
    
    public double Area() => _radius * _radius * Math.PI;
}

 

이렇게 구성하면 호출부에서는 자료형을 판정하는 조건 분기 없이, 인터페이스 타입 하나로 깔끔하게 처리할 수 있습니다.

 

double GetArea(IShape shape) 
{
    return shape.Area();
}

 

4. 플래그 매개변수 피하기

 

문제 상황

메서드 내부의 기능을 전환하기 위해 bool 형태의 매개변수(플래그 매개변수)를 전달하는 것은 '하나의 메서드는 하나의 기능만 해야 한다'는 원칙을 훼손하며 가독성을 떨어뜨립니다.

 

void ApplyDamage(bool isPhysical, int damageAmount) 
{
    if (isPhysical) 
    {
        // 물리 대미지 처리
    } 
    else 
    {
        // 마법 대미지 처리
    }
}

 

해결 방법

이 역시 전략 패턴을 적용하여, 분기되는 각각의 처리를 인터페이스의 구현체로 쪼개고 Dictionary로 묶어냅니다.

 

// 대미지를 나타내는 인터페이스
interface IDamage 
{
    void Execute(int damageAmount);
}

// 물리 대미지
class HitPointDamage : IDamage 
{
    public void Execute(int damageAmount) { /* 처리 */ }
}

// 마법 대미지
class MagicPointDamage : IDamage 
{
    public void Execute(int damageAmount) { /* 처리 */ }
}

 

사용할 때는 enum과 Dictionary를 조합해 분기문 없이 원하는 로직을 실행시킵니다.

 

enum DamageType { HitPoint, MagicPoint }
private readonly Dictionary<DamageType, IDamage> _damages = new Dictionary<DamageType, IDamage>();

void ApplyDamage(DamageType damageType, int damageAmount) 
{
    IDamage damage = _damages[damageType];
    damage.Execute(damageAmount);
}

 

5. 조건 분기의 중복과 중첩 (정책 패턴)

 

문제 상황

고객 등급을 판정하는 로직처럼, 세부 조건 기준만 다를 뿐 뼈대가 되는 다중 중첩 분기가 반복적으로 나타나는 경우입니다.

 

// 골드 회원 판정
bool IsGoldCustomer(PurchaseHistory history) 
{
    // 구매 금액이 100만원 이상
    if (1000000 <= history.TotalAmount) 
    {
        // 한 달 구매 횟수 10회 이상
        if (10 <= history.PurchaseFrequencyPerMonth) 
        {
            // 반품률이 0.1% 이하
            if (history.ReturnRate <= 0.001) 
            {
                return true;
            }
        }
    }
    return false;
}

 

해결 방법

복잡하고 반복되는 조건을 독립적인 부품처럼 분리한 뒤, 이를 조립해서 사용하는 정책 패턴(Policy Pattern)을 적용합니다.

먼저 판정 조건 하나하나를 나타내는 규칙(Rule) 인터페이스를 만들고 각각 구현합니다.

 

interface IExcellentCustomerRule 
{
    bool Ok(PurchaseHistory history);
}

// 부품 1: 골드 회원 구매 금액 조건
class GoldCustomerPurchaseAmountRule : IExcellentCustomerRule 
{
    public bool Ok(PurchaseHistory history) => 1000000 <= history.TotalAmount;
}

// 부품 2: 공통 반품률 조건
class ReturnRateRule : IExcellentCustomerRule 
{
    public bool Ok(PurchaseHistory history) => history.ReturnRate <= 0.001;
}

 

그 다음, 규칙 부품들을 모아 종합적으로 판정해 주는 정책(Policy) 클래스를 만듭니다. C#에서는 Java의 Set 대신 HashSet을 주로 사용합니다.

 

class ExcellentCustomerPolicy 
{
    private readonly HashSet<IExcellentCustomerRule> _rules = new HashSet<IExcellentCustomerRule>();
    
    // 정책에 사용할 규칙 추가
    public void Add(IExcellentCustomerRule rule) 
    {
        _rules.Add(rule);
    }
    
    // 규칙을 모두 만족하는지 확인
    public bool ComplyWithAll(PurchaseHistory history) 
    {
        foreach (var rule in _rules) 
        {
            if (!rule.Ok(history)) return false;
        }
        return true;
    }
}

 

이제 각 회원 등급 클래스에서는 필요한 조건(부품)만 자유롭게 조립하여 유연하게 등급 정책을 구성할 수 있습니다.

 

class GoldCustomerPolicy 
{
    private readonly ExcellentCustomerPolicy _policy;
    
    public GoldCustomerPolicy() 
    {
        _policy = new ExcellentCustomerPolicy();
        _policy.Add(new GoldCustomerPurchaseAmountRule());
        _policy.Add(new GoldPurchaseFrequencyRule());
        _policy.Add(new ReturnRateRule());
        // 원하는 조건 자유롭게 추가 및 제거 가능
    }
    
    public bool ComplyWithAll(PurchaseHistory history) 
    {
        return _policy.ComplyWithAll(history);
    }
}