본문 바로가기
개발/Unity

[Unity] SOLID 원칙 (2) OCP : 개방 폐쇄 원칙

by 김뜬뜬 2025. 11. 25.

안녕하세요 주인장입니다.

저번 글에 이어서 SOLID원칙에 대해서 알아보겠습니다.

 

SOLID란 무엇인가

SOLID는 다음 다섯 가지 원칙으로 이루어져 있습니다.

S - Single Responsibility Principle, 단일 책임 원칙
O - Open/Closed Principle, 개방-폐쇄 원칙
L - Liskov Substitution Principle, 리스코프 치환 원칙
I - Interface Segregation Principle, 인터페이스 분리 원칙
D - Dependency Inversion Principle, 의존성 역전 원칙

 

OCP(개방 폐쇄 원칙) - 확장에는 열려있어야 하고, 변경에는 닫혀 있어야 한다

개발하다 보면 기능이 계속 추가되거나 바뀌는 상황은 피할 수 없습니다.
문제는 기능을 추가할 때 기존 코드를 계속 뜯어고친다면, 그럴수록 버그는 늘어나고, 코드의 안정성은 점점 떨어집니다.
그래서 등장한 원칙이 개방-폐쇄 원칙입니다. 한 줄로 요약하면:

새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 기존 구조를 확장하는 방식으로 해결해야 한다.

즉, 코드를 안정적으로 유지하면서, 필요할 때는 유연하게 확장할 수 있어야 한다는 의미입니다.
이 원칙이 지켜지지 않으면 매우 간단한 기능 추가도 크고 작은 수정 작업을 계속 발생시켜 시스템 전체가 불안정해질 수 있습니다.

 

OCP를 지키지 않으면 생기는 문제들

1. 기능 추가할 때 기존 코드가 망가지는 Side Effect 발생

새로운 기능을 넣을 때마다 기존 코드를 직접 수정해야 합니다.

예를 들어:

  •   새로운 상태를 추가하려면 기존 상태 처리 로직을 바꿔야합니다
  •   새로운 장비 타입을 추가하려면 기존 장비 처리 로직을 손대야 합니다
  •   새로운 스킬을 만들려면 기존 스킬 시스템 내부를 뜯어고쳐야 합니다

이 구조는 기능 하나 추가할 때마다 다른 기능이 영향을 받을 가능성이 커집니다.
확장 → 수정 → 기존 기능의 잠재적 파손의 패턴이 반복되기 때문에
컨텐츠를 늘려나가거나 구체화가 진행될수록 Side Effect가 폭발적으로 증가하게 됩니다.

 

2. 기능이 늘어날수록 코드가 비대해지고 복잡해짐

확장 기반이 아닌 "수정 기반" 구조에서는 기능이 늘어날 때마다 클래스가 계속 커집니다.

  •   새로운 기능을 추가할 때마다 기존 클래스가 점점 커진다.
  •   switch문 혹은 if-else 블록이 계속 추가된다.
  •   하나의 클래스가 모든 기능을 다 알고 있어야 한다.

특히 다음과 같은 구조에서 문제가 심화됩니다.

  •   장비 타입이 점점 다양해지는 경우
  •   캐릭터 행동(걷기, 달리기, 공격, 점프)이 계속 추가되는 경우
  •   AI의 상태 패턴이 점점 늘어나는 경우

기능이 많아질수록 기존 클래스는 기능을 담기 위해 비대해지고,
결과적으로 확장이 반복될수록 유지보수가 어려운 구조가 됩니다.
이 부분은 앞서 말한 SRP랑 일맥상통하는 부분이 있습니다.

 

3. 유지보수 비용증가 + 신규 인력의 이해도 하락

OCP 적용이 안 된 프로젝트는 시간이 지날수록 다음과 같은 문제가 나타납니다.

  •   새로운 기능을 넣을 때 기존 코드의 흐름부터 전부 파악해야 한다.
  •   기존 Stable 코드를 건드리는 부분이 많아 Side Effect 위험이 크다
  •   서로 의존성이 꼬여있어 구조 파악 자체가 어렵다.
  •   신규 인력은 진입 장벽이 매우 높아 생산성이 떨어진다.

결국 기능이 늘어날수록 코드 품질은 떨어지고 유지보수 비용은 기하급수적으로 증가합니다.

 

Unity에서 OCP를 적용한 간단한 예시

OCP 위반 예시 - 확장 가능성이 높은 Enemy State 로직에 switch-case 문 사용

public class EnemyAI : MonoBehaviour
{
    public enum EnemyState { Patrol, Chase, Attack }
    public EnemyState state;

    void Update()
    {
        switch (state)
        {
            case EnemyState.Patrol:
                Patrol();
                break;
            case EnemyState.Chase:
                Chase();
                break;
            case EnemyState.Attack:
                Attack();
                break;
        }
    }

    void Patrol() { /*...*/ }
    void Chase() { /*...*/ }
    void Attack() { /*...*/ }
}

이 방식의 문제점:

  • 행동을 추가할 때마다 swtich문 직접 수정 필요
  • State 함수중 하나만 바뀌어도 기존 EnemyAI 스크립트 수정
  • State 종류가 늘어날수록 switch-case 블록을 계속 추가해줘야 함 

State의 필요성이 dev/prod 로 구성된 개발/운영 서버접속 분기로직이라면 큰 문제가 없겠지만

Enemy State 로직 특성상 확장 가능성이 매우 높기에 switch-case 문은 적절하지 않습니다.

만약 50개의 행동패턴이 있으면 50개의 switch-case 블록과 State 함수가 필요합니다.

 

OCP 준수 예시 - 행동을 인터페이스로 분리해 확장 가능 구조로 변경

행동 인터페이스 IEnemyState.cs

public interface IEnemyState
{
    void Execute(EnemyAI enemy);
}

 

각 상태는 별도의 스크립트로 구성

public class PatrolState : IEnemyState
{
    public void Execute(EnemyAI enemy) { /*...*/ }
}

public class ChaseState : IEnemyState
{
    public void Execute(EnemyAI enemy) { /*...*/ }
}

public class AttackState : IEnemyState
{
    public void Execute(EnemyAI enemy) { /*...*/ }
}

 

EnemyAI.cs는 상태를 바꾸기만 함

public class EnemyAI : MonoBehaviour
{
    private IEnemyState currentState;

    public void SetState(IEnemyState newState)
    {
        currentState = newState;
    }

    void Update()
    {
        currentState?.Execute(this);
    }
}

 

만약 새로운 상태인 도망(Flee)을 추가하려면 더 이상 switch-case를 건드릴 필요가 없습니다

public class FleeState : IEnemyState
{
    public void Execute(EnemyAI enemy)
    {
        // 도망 로직
    }
}

 

FleeState 클래스를 추가해주고, EnemyAI는 변경 없이 SetState(new FleeState())만 호출하면 됩니다.

 

개방 폐쇄 원칙 체크리스트

1.새로운 기능을 추가하려 할 때 기존 코드를 수정하고 있는가?

→ 기존 enum에 값 추가, switch/if 문에 case 추가 후 기존 클래스 내부 로직을 뜯어 고쳐야함. OCP 위반

 

2. 지나치게 많은 조건문(switch/if)으로 기능을 구분하고 있는가?

→ 클래스가 계속해서 커지고, 확장할 때 필연적으로 해당 조건문을 수정해야함. OCP 위반 

 

3. 하나의 클래스가 모든 기능을 알고 있어야만 동작하는가?

→ 클래스에 하나에 모든 종류의 분기가 집중적으로 나열되어있다. OCP 위반

 

4. 기능 확장을 '클래스 추가'만으로 해결 가능한가?

→ 기능 확장을 할 때마다 기존 Stable로직 수정 및 검증 필요. OCP 위반

 

마치며

개방-폐쇄 원칙은 처음에는 다소 추상적으로 느껴지지만, 핵심은 매우 명확합니다.

 

"기존에 잘 돌아가는 코드는 그대로 두고, 새로운 기능은 새로 만든 코드에서 해결하자"

 

기능이 여러 번 추가되고, 업데이트와 밸런싱 작업이 반복되는 프로젝트에서는 특히 중요한 원칙입니다.

 

 

오늘의 기록은 여기까지, 주인장은 이만 로그아웃 합니다. 모두 평안한 밤 보내세요!