기록하며 노세

OOP의 마지막 단계, 제네릭(Generic)

늙어서노세 2024. 2. 13. 15:19

 아마 OOP 객체 지향 프로그래밍 지식의 마지막 단계가 아닐까 싶다. 제네릭(Generic) 용법에 대해서 알아보자. 그 전에, 간단한 자료구조 몇 가지를 익혀야 할 필요성이 있다.

 오늘 알아볼 자료구조는 스택, 큐라는 자료구조를 알아볼 것이다. 자료구조의 간단한 정리는 아래에 있다.

 

 

 스택은 데이터로 탑을 쌓고, 데이터를 꼭대기에서부터 꺼내는 방식이라고 보면 되고, 큐는 원형 도넛같은 공간에 데이터를 넣고, 먼저 들어간 데이터부터 꺼내는 자료구조 방식이라고 생각하면 아마도 편할 수도 있다. 아래에는 간단히 스택 자료구조를 구현한 C# 스크립트이다. 스택 자료구조에서는 데이터 입력을 Push, 데이터 출력을 Pop이라고 대중적으로 사용한다. 대중적으로 사용하기 때문에 필수적이지는 않다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEditor.Progress;

// int 값을 배열로 저장하고 스택 자료구조로 관리하는 클래스
public class IntStack
{
    private int[] stack;    // 스택 배열 메모리
    private int top;        // 소진될 스택의 배열 인덱스 키
    private int maxSize;    // 스택의 최대 크기 (배열 사이즈)

    // 스택 생성 초기화
    public IntStack(int size = 5)
    {
        // 저장한 사이즈로 스택 배열 메모리를 생성함
        stack=new int[size];
        top = -1; // 스택에서 소진될 데이터 인덱스 초기화(아직 데이터가 없으므로 -1로 설정)
        // 스택 최대 크기 설정
        maxSize=size;

        Debug.Log("크기가 " + size + "인 아이템 스택을 생성하였습니다.");
    }

    // 아이템 획득 (스택에 아이템 추가)
    public void Push(int item)
    {
        // 현재 스택이 모두 찼으므로 스택 추가 실패
        if(top>=maxSize-1)
        {
            Debug.Log("더이상 아이템을 스택에 넣을 수 없습니다.");
            return;
        }

        stack[++top] = item;
        Debug.Log("아이템 스택에 [" + item + "] 아이템이 저장되었습니다.");
    }

    // 아이템 소진 (스택에 아이템 소진)
    public void Pop()
    {
        if(top==-1)
        {
            Debug.Log("더이상 소진할 아이템이 스택에 없습니다.");
            return;
        }

        Debug.Log("아이템 스택에 [" + stack[top--] + "] 아이템이 소진되었습니다.");
    }
}

public class NoneGenericStackComponent : MonoBehaviour
{
    void Start()
    {
        // 5개짜리 아이템(아이템 번호로 관리) 스택을 생성함
        IntStack iItemStack=new IntStack();

        // 유저가 아이템을 6개 획득함
        iItemStack.Push(1001);
        iItemStack.Push(1002);
        iItemStack.Push(1003);
        iItemStack.Push(1004);
        iItemStack.Push(1005);
        iItemStack.Push(1006);

        // 유저가 아이템을 3개의 아이템을 사용(소진)함
        iItemStack.Pop();
        iItemStack.Pop();
        iItemStack.Pop();
    }
}

 위처럼 간단하게 구현하였다. 이해하기는 아마 쉬울 것이다. 하지만 우리는 여기서 생각해야 한다. 스택을 현재는 int값으로 저장하게 해 놓았지만, 변경점이 생겨 아이템의 이름으로 스택을 저장하게 됐다면 어떤 문제점이 있을까?

 

1. 클래스 이름 변경이 필요할 것이다.
2. 이름을 저장할 수 있는 string 스택 배열 메모리가 있어야 할 것이다.
3. Push/Pop 기능도 string 값을 취급하는 메소드가 추가 되어야 한다.
4. 해당 스택 객체를 정수값으로만 관리하는 상태에서도 string 배열 및 관련 메소드들이 쓸데없이 존재하게 될 것이다.

 

 이에 새롭게 StringStack 클래스를 생성해서 새로운 객체로 생성하는 방법도 가능은 하다. 아니면 MixStack이라는 새로운 클래스를 만들어 두 스택을 모두 생성한 후 Push는 overload, Pop은 메소드를 두 개를 만들어 관리하는 복잡한 방법도 있긴 하다. 모두 구현은 가능하지만, 자료형이 계속해서 늘어날 때는 방식에 대한 고민을 해야 한다.

 

 이럴 경우에 사용하는 유용한 용법이 Generic이다. Generic은 하나의 동일한 목적성을 가진 클래스에서 사용하는 데이터의 자료형만 다를 경우, 클래스를 Generic 자료형으로 작성하고 클래스를 호출할 때 자료형만 넘겨주면 어떤 자료형이든 사용이 가능하게 하는 유용한 기법이다. 아래의 스크립트를 보면 이해할 수 있을 것이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GenericStack<T>
{
    // Generic 타입 스택 배열
    private T[] stack;
    private int top;
    private int maxSize;

    // 스택 생성 초기화
    public GenericStack(int size = 5)
    {
        // 저장한 사이즈로 스택 배열 메모리를 생성함
        stack = new T[size];
        top = -1; // 스택에서 소진될 데이터 인덱스 초기화(아직 데이터가 없으므로 -1로 설정)
        // 스택 최대 크기 설정
        maxSize = size;

        Debug.Log("크기가 " + size + "인 아이템 스택을 생성하였습니다.");
    }

    // 아이템 획득 (스택에 아이템 추가)
    public void Push(T item)
    {
        // 현재 스택이 모두 찼으므로 스택 추가 실패
        if (top >= maxSize - 1)
        {
            Debug.Log("더이상 아이템을 스택에 넣을 수 없습니다.");
            return;
        }

        stack[++top] = item;
        Debug.Log("아이템 스택에 [" + item + "] 아이템이 저장되었습니다.");
    }

    // 아이템 소진 (스택에 아이템 소진)
    public void Pop()
    {
        if (top == -1)
        {
            Debug.Log("더이상 소진할 아이템이 스택에 없습니다.");
            return;
        }

        Debug.Log("아이템 스택에 [" + stack[top--] + "] 아이템이 소진되었습니다.");
    }
}

public class GenericComponent : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // 5개짜리 아이템(아이템 번호로 관리) 스택을 생성함
        GenericStack<int> iItemStack = new GenericStack<int>();

        // 유저가 아이템을 6개 획득함
        iItemStack.Push(Random.Range(0, 1000));
        iItemStack.Push(Random.Range(0, 1000));
        iItemStack.Push(Random.Range(0, 1000));
        iItemStack.Push(Random.Range(0, 1000));
        iItemStack.Push(Random.Range(0, 1000));
        iItemStack.Push(Random.Range(0, 1000));

        // 유저가 아이템을 3개의 아이템을 사용(소진)함
        iItemStack.Pop();
        iItemStack.Pop();
        iItemStack.Pop();


        // 5개짜리 아이템(아이템 이름으로 관리) 스택을 생성함
        GenericStack<string> sItemStack = new GenericStack<string>();

        // 유저가 아이템을 3개 획득함
        sItemStack.Push("물풍선");
        sItemStack.Push("번개");
        sItemStack.Push("자석");

        // 유저가 아이템을 3개의 아이템을 사용(소진)함
        sItemStack.Pop();
        sItemStack.Pop();
        sItemStack.Pop();
    }
}

 분명 두 가지 int와 string 자료형을 사용하고, 하나의 Generic 클래스를 사용하는데 두 자료형 모두 문제없이 작동되는 것을 확인할 수 있을 것이다. 이렇게 구현하면, 자료형이 늘어나도 Generic 클래스의 목적성이 바뀌지 않는 이상 수정할 필요가 없는 것이다. 꺽쇠 안에 들어가 있는 가변 타입인 T라는 타입은 C++ 개발자들이 부르는 C++에서의 generic같은 용법인 Templete에서 따온 것으로, 원하는 아무 이름이나 작성해도 문제가 없다.

 또한 이 generic 용법은 클래스가 아닌 메소드도 사용이 가능하다. 아래처럼 스크립트를 작성해 보았다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DebugUtil
{
    // ... 많은 디버그용 기능 메소드들이 있다고 상상

    // 제네릭 메소드
    public static void Log<T>(T logdata)
    {
        Debug.Log("출력하고자 하는 디버깅 데이터 : " + logdata.ToString());
    }
}

public class GenericMethodComponent : MonoBehaviour
{
    void Start()
    {
        DebugUtil.Log<int>(10);
        DebugUtil.Log<float>(10.1f);
        DebugUtil.Log<string>("문자열데이터");
    }
}

 간단하게 generic 메소드를 사용했는데, 정상적으로 모두 출력되는 것을 확인할 수 있다. 스택으로는 이정도 구현해 보았다. 큐 자료구조는 스택과 비슷한 용법이기 때문에, 위 스크립트들을 참고하여 직접 구현해보는 것은 어렵지 않을 것이다.

 

 이렇게 generic을 마지막으로 OOP에 대해 알아보았는데, C#은 본인이 기존에 지겹게 다루었던 C언어에 비해 유용한 기능들이 참 많은 것 같다. 이에 유니티와 함께하여 더 구현할 수 있는 유용한 기능들을 더 파헤쳐보고 공부해보고 싶은 의욕이 가득한 학습이 되었던 것 같다.