CS/객체지향 프로그래밍

객체지향 설계의 5가지 기본원칙 S.O.L.I.D

devrabbit22 2026. 4. 9. 01:30

SOLID 원칙

SOLID 원칙은 객체지향 설계에서 지켜야 할 5가지 소프트웨어 설계 원칙을 의미한다.
각 원칙의 앞 글자를 따서 SOLID라고 부르며 다음과 같은 원칙으로 구성된다.

SRP(Single Responsibility Principle)
OCP(Open Closed Principle)
LSP(Liskov Substitution Principle)
ISP(Interface Segregation Principle)
DIP(Dependency Inversion Principle)

이 원칙들은 코드의 유지보수성, 재사용성, 확장성을 높이고 결합도를 낮추고 응집도를 높이는 방향으로 설계를 유도한다.

SRP (Single Responsibility Principle)

단일 책임 원칙

하나의 클래스는 하나의 책임만 가져야 한다.

클래스는 하나의 역할에만 집중해야 하며, 하나의 클래스가 여러 가지 책임을 가지게 되면 코드의 변경이 발생했을 때 영향을 받는 범위가 커질 수 있다.

클래스가 하나의 책임만 가지도록 설계하면 코드의 역할이 명확해지고 이해와 테스트가 쉬워진다.
반대로 하나의 클래스가 여러 책임을 가지게 되면 응집도가 낮아지고 코드 수정과 유지보수가 어려워진다.

즉, 클래스가 변경되는 이유는 단 하나여야 한다.

예시 코드

단일 책임 원칙 위반 예시 (하나의 클래스가 여러 책임)

using System;

class Character
{
    public string Name;
    public int Hp;
    public int Damage;

    public Character(string name, int hp, int damage)
    {
        Name = name;
        Hp = hp;
        Damage = damage;
    }

    // 공격
    public void Attack(Character target)
    {
        target.Hp -= Damage;
        Console.WriteLine($"{Name}이(가) {target.Name}에게 {Damage}의 피해를 입혔다.");
    }

    // 체력 출력
    public void ShowStatus()
    {
        Console.WriteLine($"{Name}의 체력 : {Hp}");
    }

    // 데이터 저장
    public void SaveData()
    {
        Console.WriteLine($"{Name}의 데이터를 저장했습니다.");
    }
}

SRP를 적용한 코드 예시

// SRP 적용 예시
// Character 클래스는 캐릭터의 데이터 관리만 담당한다.
class Character
{
    public string Name { get; private set; }
    public int Hp { get; private set; }
    public int Damage { get; private set; }

    public Character(string name, int hp, int damage)
    {
        Name = name;
        Hp = hp;
        Damage = damage;
    }

    public void TakeDamage(int damage)
    {
        Hp -= damage;
    }
}

// 전투 로직을 담당하는 클래스
class CombatSystem
{
    public void Attack(Character attacker, Character target)
    {
        target.TakeDamage(attacker.Damage);
        Console.WriteLine($"{attacker.Name}이(가) {target.Name}에게 공격했다.");
    }
}

// 캐릭터 상태를 화면에 출력하는 UI 클래스
class CharacterUI
{
    public void ShowStatus(Character character)
    {
        Console.WriteLine($"{character.Name}의 체력 : {character.Hp}");
    }
}

기존 코드에서는 Character 클래스가 전투 처리, 데이터 관리, UI 출력 등의 여러 책임을 동시에 가지고 있었다.
이는 하나의 클래스가 여러 변경 이유를 가지게 되는 구조이며 단일 책임 원칙(SRP)을 위반한다.
이를 해결하기 위해 전투 처리, UI 출력 등의 기능을 별도의 클래스로 분리하면 각 클래스는 하나의 책임만 가지게 되고 코드의 유지보수성과 확장성이 향상된다.

OCP (Open / Closed Principle)

개방-폐쇄 원칙

소프트웨어 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다.

기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있도록 설계해야 한다.

일반적으로 상속이나 인터페이스를 통해 기능 확장을 구현한다.

예를 들어 게임에서 적 캐릭터들은 서로 다른 공격 방식을 가질 수 있다.

이때 모든 공격 로직을 하나의 클래스에서 처리하게 되면 새로운 적이 추가될 때마다 기존 코드를 수정해야 한다.

하지만 부모 클래스에서 공격 메서드를 추상 메서드로 정의하고, 각 적 캐릭터가 이를 상속받아 override하여 구현하도록 하면 새로운 적이 추가되더라도 기존 코드를 수정할 필요가 없다.

즉, 기존 구조는 그대로 유지하면서 새로운 기능을 확장할 수 있게 된다.

 

예시 코드

 //OCP 원칙 예시
 //기존 코드를 변경하지 않고 기능을 확장한다.
 public abstract class Enemy
 {
     public abstract void Attack();
 }

 public class Goblin : Enemy
 {
     public override void Attack()
     {
         Console.WriteLine("고블린이 단검으로 공격한다.");
     }
 }

 public class Orc : Enemy
 {
     public override void Attack()
     {
         Console.WriteLine("오크가 도끼로 공격한다.");
     }
 }
 public class EnemyAct
 {
     //performAttack(Enemy enemy) 메서드는 Enemy 타입에 의존하도록 설계되어 있기 때문에
     //새로운 Enemy 클래스를 추가하더라도 기존 코드를 수정할 필요 없이 기능을 확장할 수 있다.
     public void EnemyAttack(Enemy enemy)
     {
         enemy.Attack();
     }
 }

LSP (Liskov Substitution Principle)

리스코프 치환 원칙

자식 클래스는 부모 클래스의 자리를 대체할 수 있어야 한다.

즉, 부모 클래스 타입으로 사용되는 곳에 자식 클래스를 사용하더라도
프로그램의 동작이 올바르게 유지되어야 한다.

이를 통해 객체지향 프로그래밍에서 다형성을 안정적으로 사용할 수 있으며 코드의 일관성을 유지할 수 있다.

이 원칙은 상속 관계를 설계할 때 자식 클래스가 부모 클래스의 기능을 올바르게 확장하고, 기존 동작을 깨뜨리지 않도록 설계해야 한다는 점을 강조한다.

 

예시 코드

// LSP 만족 예시
public abstract class Character
{
    public abstract void Attack(Character target);
}
public class Warrior : Character
{
    public override void Attack(Character target)
    {
        Console.WriteLine("전사가 검으로 공격한다.");
    }
}
public class Archer : Character
{
    public override void Attack(Character target)
    {
        Console.WriteLine("궁수가 화살을 쏜다.");
    }
}
static void Main(string[] args)
{
    Character player = new Archer();
    Character enemy = new Warrior();

    player.Attack(enemy);
}

Warrior와 Archer 클래스는 Character 클래스를 상속받아 Attack() 메서드를 각각의 방식으로 구현하고 있다.
두 클래스 모두 Character 타입으로 사용될 수 있으며, 어떤 객체를 사용하더라도 프로그램의 동작이 정상적으로 유지된다.
이처럼 부모 타입을 사용하는 코드에서 자식 객체로 대체되더라도 프로그램의 동작이 깨지지 않는 구조를 리스코프 치환 원칙(LSP)을 만족한다고 한다.

ISP (Interface Segregation Principle)

인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다.

하나의 큰 인터페이스보다 여러 개의 작은 인터페이스로 분리하여 클라이언트가 필요한 기능만 의존하도록 설계해야 한다.

여기서 알 수 있는 점은 인터페이스를 클라이언트에 특화된 작은 단위로 분리하여 의존성을 낮추고, 시스템을 더욱 유연하게 만든다.

인터페이스(Interface)란?

인터페이스는 클래스가 구현해야 할 메서드의 규약을 정의하는 구조이다.
메서드의 시그니처(이름, 매개변수, 반환 타입)만 정의하며, 일반적으로 실제 구현은 포함하지 않는다.

인터페이스를 구현하는 클래스는 인터페이스에 정의된 모든 메서드를 반드시 구현해야 한다.

이를 통해 서로 다른 클래스들이 동일한 방식으로 동작하도록 규약을 제공할 수 있다.

메서드(Method)란?

메서드는 클래스 내부에 정의된 함수로, 객체가 수행할 동작을 구현하는 코드이다.

메서드는 객체의 상태(데이터)를 변경하거나 특정 기능을 수행하는 역할을 한다.

예를 들어 Attack(), Move(), TakeDamage() 같은 기능이 메서드로 구현될 수 있다.

예시 코드

// ISP 적용 예시
public interface IAttackSkill
{
    void AttackSkill();
}
public interface IHealSkill
{
    void HealSkill();
}

public class Priest : IAttackSkill, IHealSkill
{
    public void AttackSkill()
    {
        Console.WriteLine("빛의 마법으로 적을 공격합니다.");
    }

    public void HealSkill()
    {
        Console.WriteLine("아군에게 치유 마법을 사용합니다.");
    }
}

public class Warrior : IAttackSkill
{
    public void AttackSkill()
    {
        Console.WriteLine("강력한 일격으로 적을 공격합니다.");
    }
}

DIP (Dependency Inversion Principle)

의존 역전 원칙

고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.

또한 추상화는 구체적인 구현에 의존하면 안 되고, 구현이 추상화에 의존해야 한다.

즉, 클래스가 직접 다른 클래스에 의존하기보다는 인터페이스나 추상 클래스를 통해 의존하도록 설계해야 한다.

이로 인해 변화에 더 유연하게 대응할 수 있고, 의존성을 줄여 모듈 간의 결합도를 낮출 수 있다.

예시 코드

// DIP 원칙 적용
// 고수준 모듈(Player)이 저수준 모듈(포션)에 직접 의존하지 않고 추상화(IItem)에 의존한다.

public interface IItem
{
    void Use();
}

public class HealingPotion : IItem
{
    public void Use()
    {
        Console.WriteLine("회복 포션을 사용합니다.");
    }
}

public class ManaPotion : IItem
{
    public void Use()
    {
        Console.WriteLine("마나 포션을 사용합니다.");
    }
}

public class Player
{
    private readonly IItem _item;

    // 생성자를 통해 의존성 주입
    public Player(IItem item)
    {
        _item = item;
    }

    public void UseItem()
    {
        _item.Use();
    }
}

의존성 역전 원칙(DIP)은 고수준 모듈이 저수준 모듈에 직접 의존하지 않고, 추상화에 의존하도록 설계하는 원칙이다.
위 예제에서 Player 클래스는 HealingPotion이나 ManaPotion과 같은 구체 클래스에 의존하지 않고 IItem 인터페이스에 의존한다.
따라서 새로운 아이템이 추가되더라도 Player 클래스를 수정할 필요 없이 IItem을 구현한 새로운 클래스를 추가하기만 하면 된다.


SOLID 원칙의 중요성

SRP는 클래스가 하나의 책임만 가지도록 하여 코드의 명확성을 높이고 수정 및 테스트를 용이하게 만든다.
OCP는 기존 코드를 변경하지 않고 기능을 확장할 수 있도록 하여 코드의 안정성과 확장성을 향상시킨다.
LSP는 부모 타입을 사용하는 코드에서 자식 객체를 자유롭게 사용할 수 있도록 하여 다형성을 안정적으로 활용할 수 있게 한다.
ISP는 인터페이스를 작은 단위로 분리하여 불필요한 의존성을 줄이고 클래스 간의 결합도를 낮춘다.
DIP는 구체적인 구현이 아닌 추상화에 의존하도록 설계하여 시스템의 유연성과 확장성을 높인다.

이러한 다섯 가지 원칙은 단순한 이론적인 개념이 아니라 실제 소프트웨어 설계에서 매우 중요한 역할을 한다.
SOLID 원칙을 준수하면 코드의 재사용성과 유지보수성이 향상되며, 확장 가능하고 안정적인 소프트웨어를 개발할 수 있다.