내일배움캠프 6주차 3일차 TIL - 객체 지향의 중요성

백흰범·2024년 5월 22일
0
post-custom-banner

오늘 한 일

  • 챌린지반 과제 풀이 (제너릭 싱글턴 구현하기)
  • 팀 프로젝트 진행 (파워업 추가(보호막), 프로젝트 마무리, 발표 준비)
  • 게임 프로젝트 개발하면서 아쉬웠던 점 회고하기

...작성 중...
오늘은 객체 지향의 4가지 특징에 대해서 회고하고, 객체 지향 프로그래밍의 특징을 활용하여 프로젝트에서 구현한 기능 중 하나인 떨어지는 오브젝트들을 리팩토링해보기로 했다.

기초적인 부분들은 어느 정도 넘어갈 수도 있으니 주의하자


객체 지향 프로그래밍

개념

객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모음으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다. 위키백과




4가지 특징

추상화

  • 구체적인 여러 물건들의 공통적인 특징이나 기능을 추출하는 것
  • 불필요한 세부 사항들을 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현하는 것

객체 지향 프로그래밍 중 하나인 추상화는 여러 객체의 공통적인 부분을 묶어서 동일한 데이터와 기능들을 클래스로 묶어서 상속해주기 위해 사용된다. (또는 인스턴스를 생성할 때 인스턴스마다의 공통된 특징을 묶어서 클래스 하나만으로도 다양한 종류의 인스턴스를 만드는 코드를 기획할 때 쓰인다. [클래스를 붕어빵틀이라고 설명하는 이유]


추상화 예시

  • 위 이미지를 보면 마인크래프트의 도구들을 하나로 묶어주는건데, 마인크래프트를 해보면 알겠지만 각 도구들에게는 특정 블럭을 캐면 다른 걸 들고있는 것보다 빨리 캐는 특성을 가지고 있다. (DigSpeed) 추상화를 이용한다면 공통된 특징을 찾아내고 이것을 통해서 객체들이 상속받을 부모 클래스를 설계할 수 있다.

조금 더 추상화해보자

  • 위와 같이 도구, 음식, 블럭으로 추상화해봤고 이러한 모든 애들이 플레이어의 인벤토리에 들어가기 때문에 Item이라는 추상화까지 해낼 수 있다. 마인크래프트에서 아이템들은 전부 인벤토리 칸에 들어갈 수 있다. (Pick())

이렇게 게임 속 객체들에 대한 추론 뿐만 아니라 실제 사물에서도 이러한 추론을 통해 공통된 특징을 유추해볼 수 있다. (자동차, 오토바이 -> 이동수단) 이러한 추상화를 잘한다면 구체화된 객체들에게 상속해줄 부모 클래스의 설계를 잘해낼 수 있다.
그렇기 때문에 게임 기획에 있어서 추상화라는 개념은 매우 중요한 요소이다.



상속

  • 특정 클래스의 데이터와 기능들을 하위 클래스에게 물려주는 것

상속을 통해서 각 객체들마다 중복되는 코드를 계속해서 써내는 것보다는 공통된 특징들을 부모 클래스를 하나 만들어내고 그러한 특징을 가진 객체들에게 상속시켜주자. 앞서했던 추상화를 통해 오브젝트끼리의 공통된 데이터와 기능을 추출하고 그걸 바탕으로 상속된 부모 클래스를 만들어주면 더욱더 효율적으로 상속시켜줄 수 있다.


기본적인 상속

  • 위 이미지를 보면 Tool 클래스는 Sword의 부모 클래스로 Sword 클래스에게 필드와 기능들을 전달해주고 있다. 이 상속을 이용한다면 공통된 특징들은 부모 클래스로 통해서 전해주고 자신의 구체적인 특징들은 자식 클래스에서 구현해주면 된다.

상속 관계

  • 상속의 길이에는 제한이 없다 (위에서부터 상속된 클래스의 모든 멤버들을 가져갈 수 있는 것이다.) 하지만 *클래스의 다중 상속은 불가능하니 그 점을 유의하자 (*하나의 클래스가 두 개의 클래스를 상속 받는 것)

인터페이스

  • 객체 지향 프로그래밍의 기능 중 하나로 기능의 틀을 만드는 역할을 한다. 인터페이스는 클래스처럼 상속 받을 수 있지만 인터페이스 내에 있는 메서드들을 반드시 표현해줘야한다.

인터페이스 사용

  • 필드 구현이 불가능하다. (상수는 가능하다.)
  • 인터페이스를 상속 받는다면 해당 인터페이스의 멤버들을 모두 구현해줘야한다.
  • 클래스와 달리 다중 상속이 가능하다.

인테페이스 예시

  • 인터페이스는 주로 자주 사용되는 기능에 대한 부분을 구현해줄 때 사용된다. (분류로 나누기에는 애매할 때)

상속은 공통된 데이터와 기능을 부모 클래스를 통해 상속하여 코드의 중복을 줄이기 위해 사용된다.
좀 더 디테일하게 들어가주면 클래스는 주로 객체들의 명사적인 부분, 정체성에 대해 설정할 때 기획해주는 것이 좋고, 인터페이스는 주로 객체들의 동사적, 기능적인 부분에 대해 설정할 때 기획해주는 것이 올바른 방향이다.
class -> A is ~ ||| Interface -> A can ~



다형성

  • 어떤 객체가 상황에 따라서 다양한 기능이나 속성을 가질 수 있는 것을 말한다.

예를 들어서 검이라는 무기는 적을 공격할 수 있지만, 거미줄을 쉽게 깨는 역할도 있고, 아이템 액자에 걸어줄 수 있는 등 단순히 한 가지의 특징이나 기능 뿐만 아니라 다양한 것들을 수행해낼 수 있다는 것을 말한다. 다형성의 특징을 가진 기능은 주로 overridingoverloading이나 상위 클래스의 변수를 참조하는 기능들이 있다.


상위 클래스/인터페이스 참조

사실 다형성의 가장 큰 부분을 생각한다면 아무래도 상위 존재 참조에 대한 부분이 있을 것이다.
우리가 특정 기능에 대한 액세스를 시도하기 위해서는 해당 클래스 타입 인스턴스를 받아오고 그 클래스 내에 있는 메서드를 실행시켜줘야한다.

코드 예시
// 인스턴스를 받아와서 일을 수행하는 코드
public class Something
{
	public void Method1(Class1 instance)
    {
    	instance.method1;
    }
}

만약에 개발을 수행하면서 비슷한 기능을 가진 클래스가 추가적으로 생기게 된다면 그 많은 클래스들에 대한 추가적인 처리도 해줘야한다.

코드 예시
// 추가적인 클래스들의 액세스를 위해서 추가적으로 overloading을 해줘야한다.
	public void Method1(Class1 instance)
    {
    	instance.method1;
    }
    public void Method1(Class2 instance)
    {
    	instance.method1;
    }
    public void Method1(Class3 instance)
    {
    	instance.method1;
    }
    public void Method1(Class4 instance)
    {
    	instance.method1;
    }
// 이렇게 되면 쓸데 없이 코드가 길어지고 난잡해진다.

이 현상이 계속 반복된다면 그만큼 일이 가중될 것이니 전혀 효율적인 방법이 아니다.
하지만 그 클래스들의 상위 클래스/인터페이스를 참조한다면 메서드의 액세스가 필요한 클래스가 계속 추가된다하더라도 추가적인 일이 발생되지는 않게 되어 확장성을 용이하게 만들어준다.

// 상위 클래스 타입으로 가져와주자
public void Method1(ParentClass instance)
{
	instance.method1; 
}

이와 같이 다형성을 잘 이용한다면 적은 코드만으르도 다양한 걸 표현해낼 수 있고 액세스의 편의성을 가져다준다.


캡슐화

  • 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것

사실 보호하는 측면도 있지만, 일처리적인 부분으로 보면 굳이 밖에서 내부 필드들을 건들지 않아도 독립적으로 처리할 수 있게 만들어주는 측면도 존재한다. 자신의 필드를 독자적으로 해결해낼 수 있다면 클래스 간의 결합도를 효과적으로 줄여줄 수 있다.

캡슐화에 밀접하는 기능들은 주로 접근 제한자, 프로퍼티에 해당한다.






리팩토링 해보기

  • OOP의 특징들을 토대로 내가 프로젝트에서 구현했던 떨어지는 오브젝트들을 리팩토링해볼 생각이다.

리팩토링 전

Food.cs

public class Food : MonoBehaviour
{
    [SerializeField] private LayerMask playerCollisionLayer;
    [SerializeField] private LayerMask shieldCollisionLayer;

    private FoodAnimator animator;
    private void Awake()
    {
        animator = GetComponent<FoodAnimator>();
    }

    private int scorePoint = 100; 

    void Update()
    {
        transform.position += Vector3.down * Time.deltaTime * 2 * SpawnManager.instance.speedScaling;
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if(IsLayerMatched(shieldCollisionLayer, other.gameObject.layer)) { return; }

        if (IsLayerMatched(playerCollisionLayer.value, other.gameObject.layer))
        { 
            GameManager.instance.ScoreEarn(scorePoint);
            SoundManager.instance.PlayClickSound();
            animator.IsHit(true);
        }
        Invoke("Disabled", 3f);     
    }

    private bool IsLayerMatched(int layerMask, int objectLayer)
    {
        return layerMask == (layerMask | (1 << objectLayer));
    }

    private void Disabled()
    {
        gameObject.SetActive(false);
        animator.IsHit(false);
    }
}

Avoid.cs

public class AvoidFood : MonoBehaviour
{
    public IObjectPool<GameObject> pool { get; set; }

    [SerializeField] private LayerMask playerCollisionLayer;

    void Update()
    {
        transform.position += Vector3.down * Time.deltaTime * 2 * SpawnManager.instance.speedScaling; // 속도 스케일링
    }
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (IsLayerMatched(playerCollisionLayer.value, other.gameObject.layer))
        { 
            PlayerController controller = other.gameObject.GetComponent<PlayerController>();
            controller.CallDieEvent();

            Invoke("GameOverEvent",0.5f);            
        }        
        gameObject.SetActive(false);
    }

    private bool IsLayerMatched(int layerMask, int objectLayer)
    {
        return layerMask == (layerMask | (1 << objectLayer));
    }

    private void GameOverEvent()
    {
        SystemManager.instance.CallGameOver();
    }
}

PowerUp.cs

public class PowerUp : MonoBehaviour
{
    [SerializeField] private LayerMask playerCollisionLayer;
    [SerializeField] private LayerMask shieldCollisionLayer;

    private FoodAnimator animator;

    private void Awake()
    {
        animator = GetComponent<FoodAnimator>();
    }

    void Update()
    {
        transform.position += Vector3.down * Time.deltaTime * 2.5f * SpawnManager.instance.speedScaling; // 속도 스케일링
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (IsLayerMatched(shieldCollisionLayer, other.gameObject.layer)) { return; }

        if (IsLayerMatched(playerCollisionLayer.value, other.gameObject.layer))
        {
            SoundManager.instance.PlayItemSound();
            animator.IsHit(true);
            Movement playerCoroutine = other.gameObject.GetComponent<Movement>();
            playerCoroutine.StartSpeedUP();
        }
        Invoke("Disabled", 3f);
    }

    private bool IsLayerMatched(int layerMask, int objectLayer)
    {
        return layerMask == (layerMask | (1 << objectLayer));
    }

    private void Disabled()
    {
        gameObject.SetActive(false);
        animator.IsHit(false);
    }
}

심지어 파워업은 사이즈 증가, 사이즈 감소, 보호막까지 3개 더 있다



문제점 짚기

  • 중복되는 코드들이 많다.
    • Animator, LayerMask
    • 아래로 이동
    • 트리거 감지, 레이어 검사
    • 비활성화
  • 다 따로 나눠져 있어서 코드도 완전 제각각이다.


리팩토링 이후

어떻게 리팩토링을 할까?

  • 코드를 하나로 통일시키고 생성하는 과정에서만 다르게 하자! (SO의 정보 -> 오브젝트의 정보를 담는 스크립트)

    • 플레이어와 부딪혔을 때 발생하는 상호작용을 다른 부분으로 갈아끼우는 방식으로 들어가면 좋을 것 같다.
      (상호작용 관련 enum과 enum을 받아서 switch로 상호작용을 처리하는 클래스를 만들자)

    • 혹여나 플레이어 레이어 뿐만 아니라 다른 레이어에 부딪혔을 때도 상호작용 부분도 만들어 주는 것도 좋다.
      (Layer를 단일로 받지 않고 List로 바꾸고 이걸 SO로 전해받는 식으로 해주면 될 것 같다)

      일단 꾸미는 부분은 잠시 예외시키고 상호작용 부분에만 신경써보자.


리팩토링 코드

FallingObject.cs

public class FallingObject : MonoBehaviour
{
    public FallingObjectSO objectSO; // SO를 넣어주는 것 (또는 스폰 매니저에서 줘도 된다.)

    void Update()
    {
        transform.position += Vector3.down * Time.deltaTime * 2;
    }

    private void OnTriggerEnter2D(Collider2D collider)
    {
        foreach (LayerMask layer in objectSO.InteractionWhenEnter) // foreach를 통해 모두 검사 (||와 비슷)
        {
            if (IsLayerMatched(layer, collider.gameObject.layer))
            {
                ItemInteractionManager.instance.InteractionActive(objectSO.type, objectSO.Score);
  				break;
            }
        }

        foreach (LayerMask layer in objectSO.DestroyWhenEnter)
        {
            if (IsLayerMatched(layer, collider.gameObject.layer))
            {
                Disabled();
  				break;
            }
        }
    }

    private bool IsLayerMatched(int layerMask, int objectLayer)
    {
        return layerMask == (layerMask | (1 << objectLayer));
    }

    private void Disalbed()
    {
        gameObject.SetActive(false);
    }
}

FallingObjectSO.cs

[CreateAssetMenu(fileName = "FallingObjectSO", menuName = "FallingObject", order = 0)]
public class FallingObjectSO : ScriptableObject
{
    public List<LayerMask> InteractionWhenEnter;	// 이 레이어에 부딪히면 상호작용을 일으킨다
    public List<LayerMask> DestroyWhenEnter;		// 이 레이어에 부딪히면 사라지게 한다.

    public int Score;				// 점수 ( 점수 획득 상호작용에 쓰임 )
    public InteractionType type;	// 상호작용 타입
}	

ItemInteractionManager.cs

public enum InteractionType
{
    Score,
    Big,
    Small,
    Shield,
    GameOver = 100
}

public class ItemInteractionManager
{
    public static ItemInteractionManager instance = new ItemInteractionManager();
    public void InteractionActive(InteractionType type, int score)
    {
        switch((int)type)
        {
            case 0:
                // 점수 증가 - GameManager.instance.EarnScore(score);
                break;
            case 1:
                // 크기 증가 - SpawnManager.instance.SizeChange = 2.0f;
                break;
            case 2:
                // 크기 감소 - SpawnManager.instance.SizeChange = 0.5f;
                break;
            case 3:
                // 쉴드 활성화 - GameManager.instance.player.ShieldActive();
                break;
            case 100:
                // 게임 오버 - SystemManager.instance.
                break;
        }
    }
}

위와 같이 객체 지향의 특징인 추상화와 다형성을 활용해서 코드를 좀 더 간결하고 깔끔하게 리팩토링하고 새로운 것이 추가되어도 코드의 통일성을 해치지 않게 되어 서로 다르게 생성되는 경우가 발생하지 않는다.



참고 자료

사진 - 마인크래프트

동영상

1. 객체지향 프로그래밍이 뭔가요? - 얄팍한 코딩 사전
2. 객체지향 프로그래밍? 문과도 이해쌉가능. 10분컷 - 노마드 코더

위키 백과

  1. 객체 지향 프로그래밍 - 위키백과

블로그

1. 객체 지향 프로그래밍의 4가지 원칙 - 나태웅




TIL을 쓰면서 느낀점

객체 지향이라는 것을 뭔가 정확히 알기보다는 겉으로만 알고 있는 느낌이 강했지만, 다시 한번 복습하고 알아보니 어떤 원리를 작용하게 되는지 자세히 알게 되었다. 그리고 코드를 짤 때는 이상하게도 뭔가 뚜렷하게 코드를 떠올리는 것보다는 추상적인 것에서부터 시작해서 마치 그림을 그리듯이 구도를 짠 다음에 자기가 알고 있는 코드를 동원하게 되는 느낌이다. 디자인 패턴이에 대해서 배울 때 그게 강하게 느껴진다. 아무래도 코드를 잘 짜려면 다양한 디자인 패턴을 겪어봐야할 필요도 느껴졌다.

profile
게임 개발 꿈나무
post-custom-banner

0개의 댓글