[C#] Design Pattern

Lingtea_luv·2025년 4월 4일
0

C#

목록 보기
22/37
post-thumbnail

디자인 패턴


프로그래밍 디자인 패턴은 개발에서 자주 발생하는 문제를 해결하기 위한 재사용 가능한 설계 솔루션이다. 지금까지 수많은 개발자들이 저마다의 프로그래밍을 하며 나타난 비슷한 설계 구조들이 있는데, 이를 이름 붙여서 설계 원칙으로 만든 것이라고 보면 된다.

개념

디자인 패턴은 구체적인 코드를 제공하지 않고, 일반적인 구조와 상호작용을 설명하는 템플릿을 가진다. 예를 들어 "수많은 객체를 동시에 인스턴스화 시켜 한 곳에 담아주고 사용이 끝난 후 메모리를 해제" 방식이 구현의 차이만 있을 뿐 여러 코드에서 발견되었다고 가정해보자. 만약 해당 방식을 A패턴이라고 부르기 시작해서 널리 퍼지게 된다면 추후 다른 개발자가 비슷한 내용을 구현해야하는 상황에서 A패턴이라는 이름으로 정보를 얻을 수 있게 되는 것이다.

이점

  1. 패턴은 세대를 거치며 보완이 되었기에 하나의 가이드라인으로 삼을 수 있다.
  2. 다양한 패턴 학습을 통해 개발자 간의 소통 효율이 증대된다.
  3. 패턴의 적용으로 인해 유지보수 및 확장성을 보장할 수 있다.

분류

주의사항

  1. 이미 쉬운 방법으로 구현할 수 있는데 복잡한 디자인 패턴을 적용시킬 필요가 있을까? 라는 고민을 항상 할 필요가 있다. => 오버 엔지니어링

  2. 협업자가 디자인 패턴에 대해 잘 모를 수 있고, 이 경우 프로그램의 설계가 더 복잡하게 여겨질 수 있다.

  3. 디자인 패턴은 게임 프로그래밍에서만 사용되는 개념이 아니기에, 게임의 성능과는 동떨어진 패턴도 다수 존재한다. 따라서 상황에 맞는 패턴을 적용시킬 수 있도록 많은 공부가 필요할 것이다.

  4. 디자인 패턴을 쓰기 위한 프로그래밍이 아닌, 프로그래밍을 효율적으로 하기 위한 디자인 패턴의 활용이 요구되어야한다.

디자인 패턴의 종류


수많은 디자인 패턴들이 존재하는데 일부는 확장성에, 일부는 최적화 및 재활용 성에 초점을 맞추는 등 그 종류가 다양하다. 게임의 경우 퍼포먼스가 중요하기에 최적와와 직결죄는 디자인 패턴의 선호가 높다.

싱글톤 패턴

프로그램에서 유일한 인스턴스를 보장하고 전역적인 접근을 제공하는 개념을 싱글톤 패턴이라 한다. 게임 개발의 경우 게임 매니저, 파일 매니저, 자원 관리자 등에 활용될 수 있다.

상태 패턴

게임에서 오브젝트의 동작이 상태에 따라 달라지는 경우 사용된다. 각 상태를 클래스로 구현하고 상태에 따라 다른 동작을 수행하는 식으로 동작하며, 캐릭터의 이동 상태, 공격 상태 등을 다루는데 유용하다. 클래스로 구현하는 경우 추후 새로운 상태가 추가 되어도 유연하게 확장이 가능하다.

오브젝트 풀 패턴

게임에서 반복적으로 생성되고 소멸되는 객체들을 효율적으로 관리하기 위한 패턴이다. 미리 생성된 객체들을 풀에 저장해두고 필요할 때마다 객체를 재사용하여 생성 및 소멸의 오버헤드를 줄인다. 미리 큰 메모리 공간을 확보하고 객체의 사용이 끝나더라도 삭제하는 것이 아닌 비활성화 이후 재활용할 경우 성능 상 큰 이득을 볼 수 있다.

컴포넌트 패턴

게임 오브젝트의 동작과 기능을 구현하는데 사용하는 패턴이다. 게임 오브젝트는 각각 여러 개의 컴포넌트로 구성되며, 컴포넌트는 독립적으로 동작하고 상호작용한다. 자동차를 예로 들면, 엔진, 휠, 브레이크 등 세분화된 객체 들을 조립하여 하나의 자동차 객체를 구성해나가는 패턴이다.

옵저버 패턴

이벤트와 같이 한 객체의 상태 변화에 따라 다른 객체들이 자동으로 업데이트 되도록 하는 것이 목적인 패턴이다. 주제와 옵저버라는 2가지 요소로 구성되는데, 주제는 상태를 가지고 상태가 변경될 때 옵저버에게 알리며, 옵저버는 주제를 구독하고 주제의 상태가 변경되면 업데이트를 수신하여 필요한 동작을 수행한다.

생성 디자인 패턴


Factory Method

Factory Method Pattern은 생성 패턴 중 하나로 객체를 생성할 때 어떤 클래스의 인스턴스 생성을 서브 클래스에 위임하는 것을 의미한다.

	public class MonsterFactory
    {
        public int uplevel;
        
        public Monster Create(string name)
        {
            switch (name)
            {
                case "슬라임":
                    return new Monster("슬라임", 1 + uplevel);
                case "레드 슬라임":
                    return new Monster("레드 슬라임", 1 + uplevel); 
                case "고블린":
                    return new Monster("고블린", 3 + uplevel);
                case "오크 족장":
                    return new Monster("오크 족장", 10 + uplevel);
            }

            Console.WriteLine("[error]")
            return null;
        }

        public Monster Create(string name, int level)
        {
            Monster monster = Create(name);
            monster.Level = level;
            return monster;
        }
    }
    
    public class Monster
    {
        public string Name { get; set; }
        public int Level { get; set; }

        public Monster(string name, int level)
        {
            Name = name;
            Level = level;
        }
    }

위의 예시를 보면 Monster 인스턴스를 생성할 때 new 키워드와 Monster의 호출로 생성할 수도 있지만, MonsterFactory의 Create 메서드를 사용하여 생성할 수도 있다. 이처럼 Monster의 인스턴스 생성을 MonsterFactory의 메서드에게 위임한다고 해서 팩토리 메서드 패턴이라고 한다.

장점

  1. 개방 폐쇄 원칙 - 기존의 코드 수정없이 Factory Method에 추가하는 것으로 확장이 가능
  2. 단일 책임 원칙 - 인스턴스 생성 관련해서 책임을 갖기에, 문제가 생긴다면 팩토리만 참고해도 된다.

단점

  1. 인스턴스 생성을 위해 클래스를 추가로 정의해야하기에 코드가 길어진다.

Builder

Builder Pattern은 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴으로 체인 메서드를 활용하여 객체의 다양한 유형들과 표현을 구현할 수 있다.

	public class MonsterBuilder
    {
        public string Name;
        public string Weapon;
        public string Armor;

        public MonsterBuilder()
        {
            Name = "몬스터";
            Weapon = "기본 무기";
            Armor = "기본 갑옷";
        }

        public Monster Build()
        {
            Monster monster = new();
            monster.Name = Name;
            monster.Weapon = Weapon;
            monster.Armor = Armor;
            return monster;
        }

        public MonsterBuilder SetName(string name)
        {
            this.Name = name;
            return this;
        }
        public MonsterBuilder SetWeapon(string weapon)
        {
            this.Weapon = weapon;
            return this;
        }
        public MonsterBuilder SetArmor(string armor)
        {
            this.Armor = armor;
            return this;
        }
    }
    
    public class Monster
    {
        public string Name;
        public string Weapon;
        public string Armor;
    }
	MonsterBuilder orcArcherBuilder = new();
    
    orcArcherBuilder
        .SetName("오크 궁수")
        .SetWeapon("나무 활")
        .SetArmor("가죽 갑옷");
    
    Monster monster1 = orcArcherBuilder.Build();        

위의 코드를 보면 빌더 패턴의 인스턴스 생성은 메인 메서드에서 단계 별로 진행되는 것을 볼 수 있는데, 이처럼 빌더 패턴은 인스턴스의 정보를 노출하고 있다는 특징을 가지고 있다. 또한 빌더 패턴은 팩토리 메서드와는 다른 장점을 가지고 있다.

장점

  1. 인스턴스 생성 코드의 가독성 향상
    생성 과정에서 처리해야할 매개변수가 많을 때 위임받은 클래스에서 단계적으로 처리함으로써 복잡한 과정을 일련의 과정으로 정리할 수 있다.

  2. 인스턴스 유효성 검사
    단계적으로 처리한다는 특징으로 필요한 매개변수가 빠지거나, 유효하지 않은 값이 사용되는 것을 방지할 수 있어 오류를 사전에 차단할 수 있다.

  3. 불변(Immutable) 객체 생성
    생성을 마치고 나면 해당 인스턴스는 더 이상 상태가 변경되지 않는 불변 객체가 되어 안전하게 공유될 수 있다.

  4. 인스턴스 생성의 유연성
    생성 과정에서 매개변수의 일부만 전달하여도 나머지는 기본값으로 처리할 수 있으며, 일부 속성을 변경하여 다른 유형의 객체를 생성하는 것 또한 쉽다.

  5. 생성자 오버로드 감소
    파라미터를 한 번에 전달하는 경우 예외 처리를 위해 생성자 오버로드가 쌓일 수록 코드의 가독성이 떨어지게 된다. 하지만 빌더 패턴의 경우 단계적으로 처리하여 이를 방지할 수 있다.

단점

  1. 팩토리 메서드와 마찬가지로 생성을 위해 새로운 클래스를 추가해야한다.
  2. 인스턴스 생성을 위해 여러 메서드를 호출하여 일부 성능 저하가 발생할 수 있다.
  3. 인스턴스 생성에 필요한 요소가 많아질수록 오히려 가독성이 떨어질 수 있다.

Builder vs Factory Method

빌더 패턴과 팩토리 메서드 패턴 모두 클래스의 인스턴스 생성을 다른 클래스에 위임한다는 점에서 동일하지만, 차이점도 분명하다.

  • 빌더 패턴 : 인스턴스의 필드값을 구현하는 메서드를 필드 종류 별로 가지고 있다.
  • 팩토리 메서드 패턴 : 하나의 메서드가 인스턴스의 구현을 담당하기에 parameter를 해당 메서드에 모두 넣어주어야한다.

따라서 빌더 패턴은 메인 메서드에서 parameter를 하나씩 받아 단계적으로 인스턴스를 생성하는 과정을 보이며, 팩토리 메서드는 내부 정보를 바탕으로 단계를 거치지 않고 한 번에 인스턴스가 생성되는 것이다. 이를 쉽게 비유하자면 팩토리 메서드는 오마카세, 빌더는 서브웨이 주문이라고 생각하면 된다.

추가 기능


Chain Method

반환형을 자기 자신으로 지정하는 경우 메서드를 연결해서 사용하는 것이 가능하다.

	public class GameObject
    {
        public int X { get; set; }
        public int Y { get; set; }
        public int Z { get; set; }

        public GameObject SetX(int x)
        {
            X = x;
            return this; 
        }
        
        public GameObject SetY(int y)
        {
            Y = y;
            return this; 
        }
        
        public GameObject SetZ(int z) 
        {
            Z = z;
            return this; 
        }
        public GameObject(int x, int y, int z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }

이렇게 멤버 함수로 자기 자신을 반환하도록(return this) 설정하면, 해당 메서드를 연속으로 사용할 수 있게된다.

	GameObject gameObject = new(10,10,10);
        
    gameObject.SetX(20).SetY(20).SetZ(20); 
    
    gameObject
        .SetX(30)
        .SetY(30)
        .SetZ(30); 

체인 메서드의 경우 별 쓰임이 없어보이지만, 가독성이 좋고 클래스 내에서 혹은 메인 메서드에서 객체의 다양한 필드 값을 바꿀 때 유용하게 쓰인다.

profile
뚠뚠뚠뚠

0개의 댓글