1. SOLID 원칙

더보기

1. SOLID

 

SOLID  Eng. Kor.
SRP Single Responsibility Principle 단일 책임 원
OCP Open-Closed Principle 개방-폐쇄 원칙 
LSP Liskov Substitution Principle 리스코프 치환 원칙 
ISP Interface Segregation Principle 인터페이스 분리 원칙
DIP Dependency Inversion Principle 의존성 역전 원칙


객체지향 개념
은 추상화 → 캡슐화 → 다형성 → 상속/인터페이스 → SOLID 원칙 → 디자인 패턴  → 아치텍처 패턴 등이 서로 연결되어 있습니다.


 

2. 개방-폐쇄 원칙 (OCP, Open-Closed Principle)

더보기

A. 개념

 

"Software entities ... should be open for extension, but closed for modification."

 

-『Object-Oriented Software Construction』 (1988)   Bertrand Meyer


새로운 기능을 추가할 때는 코드 확장으로 해결해야 한다.

“추상화와 다형성”을 활용한 설계(예: 전략 패턴, 인터페이스 기반 프로그래밍)로 기존 코드를 직접 수정하지 않는다.

 

기존 코드를 수정하면 버그 발생 위험이 증가하고, 기능 간의 결합도가 강해져 구현이 깨지기 쉬워 유지보수 측면에서도 재앙같은 존재입니다.

 

 

 

 

B. 용어

 

확장에는 열려(Open) 있고, 수정에는 닫혀(Close) 있는 상태 >> 개방-폐쇄 원칙 (OCP, Open-Closed Principle)

 

 

 

 

C. 고려사항

 

OCR 예제들은 DIP 예제와 유사하며,

이전에 학습한 SRP과 연관하여 문제점과 해결책을 살펴봅니다.


 

3. 도형(Shape) 클래스 

더보기

A. 가정

 

  • 프로그램에 버튼을 누르거나, 이동 하거나, 사용자가 특정 동작을 진행하면 "도형"이 표현된다고 가정합니다.
    (마치 게임에서 새로운 오브젝트(나무, 돌, 벽, 몬스터 등)가 등장하듯이)
  • 도형의 위치, 색, 크기 등의 속성 기능을 구현한다면, 어떤 구조로 구현해야 할까요? 

 

 

 

 

B. OCP를 위반한 코드

using System;

namespace OCP_Example1_1
{
    #region Shape Classes (도형 클래스들)
    public class Circle { public void Draw() { Console.WriteLine("Drawing a circle"); } }
    public class Rectangle { public void Draw() { Console.WriteLine("Drawing a rectangle"); } }
    // 🔴 새로운 도형을 추가한다 가정하고, 문제점을 파악한다.
    public class Triangle { public void Draw() { Console.WriteLine("Drawing a triangle"); } }
    #endregion

    #region ShapeManager (OCP 위반)
    // 🔴 OCP 위반 클래스
    public class ShapeManager
    {
        public void Render(object shape)
        {
            if (shape is Circle) { ((Circle)shape).Draw(); }
            else if (shape is Rectangle) { ((Rectangle)shape).Draw(); }
            else if (shape is Triangle) { ((Triangle)shape).Draw(); } // 🔴 새로운 도형이 추가될 때마다, Render 구현체의 수정이 필요
            else { throw new Exception("Unsupported shape"); }
        }
    }
    #endregion

    #region Program Entry
    class Program
    {
        static void Main(string[] args)
        {
            ShapeManager manager = new ShapeManager();
            manager.Render(new Circle());
            manager.Render(new Rectangle());
            manager.Render(new Triangle());  // 🔴 새로운 도형을 추가한다 가정한다.
        }
    }
    #endregion
}

 

  • OCP를 위반한 코드 문제점 파악
    • 새로운 도형을 추가할 때마다 ShapeManager 클래스의 코드를 직접 수정해야 합니다. >> OCP 위반
    • if-else 문이나 switch 문을 사용 >> 새로운 도형이 추가될 때마다 코드를 직접 수정해야합니다
    • 여러분이 게임 개발자이고, 새로운 지형지물이 추가될때마다, 코드를 직접 수정한다면???
      클래스를 수정하는 사람이 여럿이라면???

      모든 구성원이 각자 모든 코드를 수정한다면???

 

 

 

 

C. OCP 원칙을 준수한 코드

using System;

namespace OCP_Example1_2
{
    #region Shape Abstraction (OCP 준수 추상화 계층)
    // 🟢 추상 클래스 정의
    public interface IShape
    {
        void Draw();
    }
    #endregion


    #region Shape Classes (도형 클래스들)
    public class Circle : IShape { public void Draw() { Console.WriteLine("Drawing a circle"); } }
    public class Rectangle : IShape { public void Draw() { Console.WriteLine("Drawing a rectangle"); } }
    // 🟢 새로운 도형 추가 시, ShapeManager 수정 필요 없음
    public class Triangle : IShape { public void Draw() { Console.WriteLine("Drawing a triangle"); } }
    #endregion


    #region ShapeManager (OCP 준수)
    public class ShapeManager
    {
        public void Render(IShape shape) { shape.Draw(); } // 다형성 활용 → 새로운 도형 추가 시 Render 수정 필요 없음
    }
    #endregion


    #region Program Entry
    class Program
    {
        static void Main(string[] args)
        {
            ShapeManager manager = new ShapeManager();
            manager.Render(new Circle());
            manager.Render(new Rectangle());
            manager.Render(new Triangle()); // 🟢 기존 코드 수정 없이 새로운 도형 추가 가능
        }
    }
    #endregion
}

 

 

 


5. OCP를 준수한 코드에서 개선된 부분 파악

 

  • 새로운 도형이 추가될 때 ShapeManager 클래스 수정이 필요 없음 >> OCP 준수
  • 다형성을 사용해 새로운 도형을 쉽게 추가 가능 >> 기존 코드 수정하지 않음
  • if-else 문 없이 인터페이스추상 클래스 기반 설계 가능

 

 

4. 계산기(Calculator) 클래스 

더보기

OCP: 원본 코드를 수정하지 않고, 새로운 동작을 추가합니다.

 

 

 

 

A. 가정

 

  • 도형의 너비를 구하는 함수를 구현한다고 가정합니다.
  • 기존 원 너비, 사각형 너비를 구하는 로직 이외에
    오각형, 육각형의 너비를 구하는 기능을 구현한다면, 어떤 구조로 구현해야 할까요? 

 

 

 

 

B. OCP를 위반한 코드

using System;

namespace OCP_Example2_1
{
    #region Shape Models (구체 도형 타입들)
    public class Rectangle
    {
        public float width;
        public float height;
        public Rectangle(float w, float h) { width = w; height = h; }
    }

    public class Circle
    {
        public float radius;
        public Circle(float r) { radius = r; }
    }

    // 🔴(추가 가정) 나중에 또 생기는 도형…
    public class Pentagon
    {
        public float side; // 정오각형 한 변 길이
        public Pentagon(float s) { side = s; }
    }
    #endregion



    #region Calculator (🔴 OCP 위반: 도형 추가 시 메서드 계속 추가/수정)
    public class Calculator
    {
        public float GetRectangleArea(Rectangle r) { return r.width * r.height; }

        public float GetCircleArea(Circle c) { return c.radius * c.radius * (float)Math.PI; } // Unity라면 Mathf.PI

        public float GetPentagonArea(Pentagon p) { return (5f * p.side * p.side) / (4f * (float)Math.Tan(Math.PI / 5.0)); }

        // 🔴 문제: 새로운 도형이 추가될 때마다 위 클래스에
        //         GetXxxArea 메서드를 “반드시” 추가/수정해야 함 → OCP 위반
    }
    #endregion



    #region Program
    class Program
    {
        static void Main()
        {
            var calc = new Calculator();

            Console.WriteLine(calc.GetRectangleArea(new Rectangle(10, 5)));  // 50
            Console.WriteLine(calc.GetCircleArea(new Circle(7)));            // 153.938...
            Console.WriteLine(calc.GetPentagonArea(new Pentagon(6)));        // 추가된 정오각형 예시

            // 🔴 도형이 늘어날수록 Calculator를 계속 수정해야만 함 → 유지보수 비용↑
        }
    }
    #endregion
}

 

 

 

 

C. OCP 원칙을 준수한 코드

 

C.1 원, 삼각형, 사각형, 오각형 등의 도형이 추상화된 인터페이스(추상클래스) IShape 를 구현합니다.

public interface IShape
{
    float GetArea(); // 각 도형이 스스로 넓이를 계산
}

 

 

C.2 IShape 를 상속받아 구체화된 클래스를 구현합니다.

: 도형 클래스 IShape

public class Rectangle : IShape
{
    public float width;
    public float height;
    public Rectangle(float w, float h) { width = w; height = h; }

    public float GetArea() { return width * height; }
}

public class Circle : IShape
{
    public float radius;
    public Circle(float r) { radius = r; }

    public float GetArea() { return radius * radius * (float)Math.PI; }
}

 

 

C.3 Calculator 클래스는 각각의 도형을 직접 사용하는 것이 아니라, 도형이 추상화된 IShape 를 사용합니다.

 : IShape Calculator  클래스 

// (🟢 OCP 준수: 다형성만 사용)
public class Calculator
{
    public float ComputeArea(IShape shape) { return shape.GetArea(); }
}

 

 

C.4 실제 사용되는 곳에서도, 수정 불필요

static void Main()
{
    var calc = new Calculator();

    Console.WriteLine(calc.ComputeArea(new Rectangle(10, 5)));  // 50
    Console.WriteLine(calc.ComputeArea(new Circle(7)));         // 153.938...
    Console.WriteLine(calc.ComputeArea(new Pentagon(6)));       // 정오각형 예시

    //🟢 새로운 도형을 추가해도 Calculator 수정 불필요
}

 

 

전체코드

using System;

namespace OCP_Example2_1_Compliance
{
    #region Shape Abstraction
    public interface IShape
    {
        float GetArea();
    }
    #endregion

    #region Shape Models
    public class Rectangle : IShape
    {
        public float width;
        public float height;
        public Rectangle(float w, float h) { width = w; height = h; }

        public float GetArea() { return width * height; }
    }

    public class Circle : IShape
    {
        public float radius;
        public Circle(float r) { radius = r; }

        public float GetArea() { return radius * radius * (float)Math.PI; }
    }

    public class Pentagon : IShape
    {
        public float side;
        public Pentagon(float s) { side = s; }

        public float GetArea() { return (5f * side * side) / (4f * (float)Math.Tan(Math.PI / 5.0)); }
    }
    #endregion

    #region Calculator (OCP 준수)
    public class Calculator
    {
        public float ComputeArea(IShape shape) { return shape.GetArea(); }
    }
    #endregion

    #region Program
    class Program
    {
        static void Main()
        {
            var calc = new Calculator();

            Console.WriteLine(calc.ComputeArea(new Rectangle(10, 5)));  // 50
            Console.WriteLine(calc.ComputeArea(new Circle(7)));         // 153.938...
            Console.WriteLine(calc.ComputeArea(new Pentagon(6)));       // 정오각형 예시

            // 🟢 새로운 도형을 추가해도 Calculator 수정 불필요
        }
    }
    #endregion
}

 

5. 예제 04 - 인간 → 전사, 기사, 도적 추가

더보기
using System;

namespace OCP_HumanFamily
{
    // ===== 공통 추상 타입 =====
    abstract class Human
    {
        public string Name { get; protected set; }
        public int Level   { get; protected set; }
        public int Hp      { get; protected set; }
        public int AttackPower { get; protected set; }

        protected Human(string name, int level, int hp, int ap)
        {
            Name = name; Level = level; Hp = hp; AttackPower = ap;
        }

        // (확장 훅1) 직업별 추가 정보
        public virtual string GetExtraInfo() { return ""; }

        // (확장 훅2) 스킬 보너스: 직업이 필요하면 오버라이드
        public virtual int GetAttackBonusFor(string skillName) { return 0; }
    }

    // ===== 직업들 =====
    class Warrior : Human
    {
        public Warrior(string name, int level, int hp, int ap)
            : base(name, level, hp, ap) { }
    }

    class Knight : Warrior
    {
        public int Defense { get; private set; }

        public Knight(string name, int level, int hp, int ap, int defense)
            : base(name, level, hp, ap)
        {
            Defense = defense;
        }

        public override string GetExtraInfo()
        {
            return ",Defense=" + Defense;
        }

        public override int GetAttackBonusFor(string skillName)
        {
            if (skillName == "KnightShieldAttack") return Defense / 2;
            return 0;
        }
    }

    class Rogue : Human
    {
        public int Agility { get; private set; }

        public Rogue(string name, int level, int hp, int ap, int agility)
            : base(name, level, hp, ap)
        {
            Agility = agility;
        }

        public override string GetExtraInfo()
        {
            return ",Agility=" + Agility;
        }

        public override int GetAttackBonusFor(string skillName)
        {
            if (skillName == "RogueBackstab") return Agility;
            return 0;
        }
    }

    // ===== 전투 전략(OCP 포인트) =====
    interface IAttackStrategy
    {
        int CalcDamage(Human h);
        string SkillName();
    }

    // 공통 물리 공격
    class BasicAttack : IAttackStrategy
    {
        public int CalcDamage(Human h)
        {
            return h.AttackPower + (h.Level * 2);
        }
        public string SkillName() { return "BasicAttack"; }
    }

    class KnightShieldAttack : IAttackStrategy
    {
        public int CalcDamage(Human h)
        {
            int dmg = h.AttackPower + (h.Level * 2);
            dmg += h.GetAttackBonusFor(SkillName());   // 하위 타입 훅
            return dmg;
        }
        public string SkillName() { return "KnightShieldAttack"; }
    }

    class RogueBackstab : IAttackStrategy
    {
        public int CalcDamage(Human h)
        {
            int dmg = h.AttackPower + (h.Level * 2);
            dmg += h.GetAttackBonusFor(SkillName());   // 하위 타입 훅
            return dmg;
        }
        public string SkillName() { return "RogueBackstab"; }
    }

    // ===== UI/HUD (공통 타입만 의존) =====
    interface IHudRenderer { void Render(Human h); }

    class ConsoleHudRenderer : IHudRenderer
    {
        public void Render(Human h)
        {
            Console.WriteLine(
                "[HUD] {0,-10} | Type={1,-6} | Lv {2,2} | HP {3,4} | AP {4,3}{5}",
                h.Name, h.GetType().Name, h.Level, h.Hp, h.AttackPower, h.GetExtraInfo()
            );
        }
    }

    // ===== 오케스트레이션: 전투 흐름 (전략 주입) =====
    class BattleService
    {
        private readonly IAttackStrategy _attack;

        public BattleService(IAttackStrategy attack)
        {
            _attack = attack;
        }

        public void Attack(Human h, string target)
        {
            int damage = _attack.CalcDamage(h);
            Console.WriteLine("[{0}] {1} -> {2} : {3} dmg",
                _attack.SkillName(), h.Name, target, damage);
        }
    }

    // ===== 데모 =====
    class Program
    {
        static void Main()
        {
            IHudRenderer hud = new ConsoleHudRenderer();

            Human w   = new Warrior("평범한전사", 4, 100, 15);
            Human k   = new Knight ("튼튼한기사", 6, 140, 18, defense: 10); // Warrior 상속
            Human r   = new Rogue  ("민첩한도적", 7, 110, 16, agility: 12);

            hud.Render(w);
            hud.Render(k);
            hud.Render(r);
            Console.WriteLine();

            // 기존 코드를 수정하지 않고 전략(스킬)만 교체/추가하여 확장(OCP)
            BattleService svcBasic  = new BattleService(new BasicAttack());
            BattleService svcShield = new BattleService(new KnightShieldAttack());
            BattleService svcBack   = new BattleService(new RogueBackstab());

            svcBasic.Attack(w, "슬라임");
            svcBasic.Attack(k, "슬라임");
            svcBasic.Attack(r, "슬라임");

            svcShield.Attack(k, "오우거");   // Knight 전용 보너스 적용
            svcBack.Attack(r, "오우거");     // Rogue 전용 보너스 적용

            // ★ 새 직업이나 새 스킬을 추가하려면?
            // -> Human을 상속한 클래스를 추가 OR IAttackStrategy 구현 클래스를 추가
            // 기존 클래스(BattleService/HUD/기존 직업) 수정 없이 "추가"만으로 확장 가능 = OCP 준수
        }
    }
}

 

6.  몬스터 클래스

더보기

A. OCP를 지키지 않은 간단한 예제 

 using System;

 namespace GameExampleWithoutOCP
 {
     public class MonsterHandler
     {
         public void HandleMonster(string type)
         {
             if (type == "좀비")
             {
                 Console.WriteLine("좀비 몬스터 스폰");
             }
             else if (type == "흡혈귀")
             {
                 Console.WriteLine("흡혈귀 몬스터 스폰");
             }
             else if (type == "드래곤") // 새로운 몬스터 추가 시 MonsterHandler 구현부 소스 코드 수정 필요
             {
                 Console.WriteLine("드래곤 몬스터 스폰");
             }
         }
     }

     class Program
     {
         static void Main(string[] args)
         {
             MonsterHandler handler = new MonsterHandler();
             handler.HandleMonster("좀비");
             handler.HandleMonster("흡혈귀");
             handler.HandleMonster("드래곤"); // 새로운 몬스터를 추가하고 싶다면, MonsterHandler 코드 수정 필요
         }
     }
 }

 

기능을 확장할 때 기존 코드를 수정해야 한다.

  • 새로운 몬스터(dragon)가 추가될 때마다
    MonsterHandler 클래스의 HandleMonster() 메서드 구현부를 수정해야 합니다.
  • if-else 또는 switch-case가 점점 커지면서
    결합도(coupling)가 높아지고
    코드가 복잡해져
    >> 유지보수 및 확장성이 떨어집니다.

 

 

 

 

B. OCP를 지키지 않은 예제 

using System;

namespace GameExampleWithoutOCP
{
    // 부모 클래스 정의
    public class Monster
    {
        public string Type { get; set; }
    }

    // 하위 클래스 정의
    public class Zombie : Monster
    {
        public Zombie()
        {
            Type = "좀비";
        }
    }

    public class Vampire : Monster
    {
        public Vampire()
        {
            Type = "흡혈귀";
        }
    }

    public class Dragon : Monster // 새로운 몬스터를 추가한다고 가정합니다.
    {
        public Dragon()
        {
            Type = "드래곤";
        }
    }

    // 개방-폐쇄 원칙 위반 클래스
    public class MonsterHandler
    {
        public void HandleMonster(Monster monster)
        {
            if (monster is Zombie)
            {
                Console.WriteLine("몬스터가 좀비로 등장한다.");
            }
            else if (monster is Vampire)
            {
                Console.WriteLine("몬스터가 흡혈귀로 등장한다.");
            }
            else if (monster is Dragon) // 새로운 몬스터 추가 시 handler 구현부 수정 필요
            {
                Console.WriteLine("몬스터가 드래곤으로 등장한다.");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MonsterHandler handler = new MonsterHandler();
            handler.HandleMonster(new Zombie());
            handler.HandleMonster(new Vampire());
            handler.HandleMonster(new Dragon()); // 새로운 몬스터를 추가하고 싶다면, MonsterHandler 코드 수정 필요
        }
    }
}

 

 


C. OCP를 준수한 예제
 

using System;

namespace GameExampleWithOCP
{
    // 부모 클래스 정의 (추상 클래스)
    public abstract class Monster
    {
        // 모든 몬스터가 공격 기능을 가지도록 정의
        public abstract void Handle();
        public abstract void Attack();
    }

    // 하위 클래스 정의
    public class Zombie : Monster
    {
        public override void Handle()
        {
            Console.WriteLine("Handling a zombie");
        }

        public override void Attack()
        {
            Console.WriteLine("Zombie bites the player!");
        }
    }

    public class Vampire : Monster
    {
        public override void Handle()
        {
            Console.WriteLine("Handling a vampire");
        }

        public override void Attack()
        {
            Console.WriteLine("Vampire drains the player's health!");
        }
    }

    public class Dragon : Monster
    {
        public override void Handle()
        {
            Console.WriteLine("Handling a dragon");
        }

        public override void Attack()
        {
            Console.WriteLine("Dragon breathes fire at the player!");
        }
    }

    // 개방-폐쇄 원칙을 준수한 클래스 구현체
    public class MonsterHandler
    {
        // 몬스터 핸들링과 공격 처리
        public void HandleMonster(Monster monster)
        {
            monster.Handle(); // 몬스터의 기본 동작 처리
            monster.Attack(); // 몬스터의 공격 처리
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            MonsterHandler handler = new MonsterHandler();
            handler.HandleMonster(new Zombie());
            handler.HandleMonster(new Vampire());
            handler.HandleMonster(new Dragon()); // 새로운 몬스터 추가에도 handler 구현체 수정 필요 없음
        }
    }
}