내일배움캠프 TIL

내일배움캠프 84일차 TIL "클래스와 구조체의 차이"

Jooglorystar 2025. 1. 16. 22:11

 

 

0. 서론

 

유니티에서 C#을 이용해 게임을 개발하면서 여러 클래스(Class)와 구조체(Struct)를 만들고, 이용했지만, 정작 그 차이에 대해 제대로 모르고 이용했었다.

 

앞으로 더 나은 개발이 가능하도록, 이러한 클래스와 구조체의 차이에 대해 정리를 하기로 결정했다.

 

우선 클래스와 구조체 둘 다 객체지향 프로그래밍에서 데이터를 캡슐화 하고 행동을 정의하는 역할을 한다. 

 

그러나 클래스와 구조체는 설계와 메모리 관리 방식에서 차이가 존재한다.

 

 

1. 클래스와 구조체의 차이점

 

기본적으로 클래스는 참조 타입으로, 힙 메모리에 저장되며 객체의 참조를 통해 접근한다.

반면 구조체는 값 타입으로 주로 스택 메모리에 저장되며, 값을 직접 복사 해 데이터를 전달한다.

 

1.1. 메모리 관리

 

클래스는 힙 메모리에 저장되며, 객체를 생성할 때 동적으로 메모리가 할당된다. 객체가 더 이상 사용되지 않으면 가비지 컬렉션에 의해 메모리가 자동으로 해제된다. 

이 가비지 컬렉션이 실행될 때 성능 저하가 발생할 수 있다.

 

구조체는 스택 메모리에 저장되며, 함수의 호출이 끝나거나 범위를 벗어나면 자동으로 메모리가 해제된다.

빠르게 메모리 할당과 해제가 가능하지만, 값 타입의 복사가 이루어질 때 메모리 사용량이 증가할 수 있다.

 

1.2. 상속 여부

 

클래스는 상속이 가능하며 다형성과 추상화를 구현하기에 적합하지만,

구조체는 상속이 불가능하며, 주로 단순한 데이터 묶음으로서 사용된다. (ex. Vector2, Vector3)

 

1.3. 성능 차이

 

클래스는 힙 메모리에서 동작하므로, 객체 생성과 해제에 시간이 더 소요된다. 특히, 힙 메모리의 단편화나 가비지 컬렉션으로 인해 추가적인 성능저하가 발생할 수 있다.

구조체는 스택메모리를 사용하기 때문에 생성과 해제가 클래스에 비해 매우 빠르다. 단순한 데이터를 다룰 경우 구조체가 성능적으로 더 유리할 수 있다.

 

1.4. 참조와 값

 

클래스는 참조 타입으로, 동일한 객체를 여러 변수로 참조할 수 있다. 한 변수에서 객체의 데이터를 수정하면, 그 변경이 모든 참조 변수에 반영된다.

반면, 구조체는 값 타입으로, 복사가 이루어질 때 독립적인 데이터가 생성된다. 따라서 한 변수에서 데이터를 변경해도 다른 변수에는 영향을 미치지 않는다.

아래는 이를 확인할 수 있는 예제다.

public class MyClass
{
    public int classValue;
}

public struct MyStruct
{
    public int structValue;
}

static void Main(string[] args)
{
    MyClass myClass = new MyClass { classValue = 5 };
    MyClass myClass2 = myClass;

    myClass2.classValue = 10;

    Console.WriteLine($"myClass = {myClass.classValue}");       // myClass = 10
    Console.WriteLine($"myClass2 = {myClass2.classValue}");     // myClass2 = 10

    MyStruct myStruct = new MyStruct { structValue = 20};
    MyStruct myStruct2 = myStruct;

    myStruct2.structValue = 40;

    Console.WriteLine($"myStruct = {myStruct.structValue}");    // myStruct = 20
    Console.WriteLine($"myStruct2 = {myStruct2.structValue}");  // myStruct2 = 40
}

 

myClass 객체의 classValue를 5로 초기화한 뒤, myClass2를 myClass로 초기화하면, 두 변수는 동일한 객체를 참조한다.
이후 myClass2.classValue를 10으로 변경하면, myClass.classValue도 10으로 변경된다.
이는 두 변수가 동일한 객체를 참조하기 때문이다.

myStruct의 structValue를 20으로 초기화한 뒤, myStruct2를 myStruct로 초기화하면, 두 변수는 독립적인 값을 가지게 된다.
이후 myStruct2.structValue를 40으로 변경해도, myStruct.structValue는 여전히 20으로 유지된다.
이는 구조체가 값을 복사하기 때문이다.

 

2. 구조체와 클래스의 실 사용 예시

 

나는 농사 시뮬레이터 게임에서 작물을 관리하는데 클래스와 구조체를 각각 적절히 활용했었다.

 

public class CropState
{
    public int growthStage;
    public int growingDate;
    public SeedSO seedSO;
    public SpriteResolver spriteResolver;
}

public struct CropSaveData
{
    public Vector3Int cropPosition;
    public int growthStage;
    public int growingDate;
    public int seedItemCode;

    public CropSaveData(Vector3Int p_cropPosition, CropState p_cropState)
    {
        cropPosition = p_cropPosition;
        growthStage = p_cropState.growthStage;
        growingDate = p_cropState.growingDate;
        seedItemCode = p_cropState.seedSO.itemCode;
    }
}

 

 

2.1 CropState 클래스

CropState의 경우 실제 게임에서 작물의 상태를 관리할 때 사용하는 클래스이다.

클래스로 사용하고자한 이유는 다음과 같다.

1. 작물의 성장 단계와 자란 날짜는 게임 진행중 동적으로 변경되는데, 이를 위해 참조타입이어야 했다.

2. 또한 SeedSO나 SpriteResolver와 같이 다른 객체를 참조로 관리해야하는 경우도 있었다.

 

아래는 CropState가 수정되는 대표적인 메서드이다.

public void UpdateGrowth()
{
    foreach (var (tilePosition, tileData) in GameManager.Instance.TilemapM.tileDataMap)
    {
        if (cropStateDictionary.TryGetValue(tilePosition, out CropState cropState))
        {
            if (tileData.IsWatered)
            {
                cropState.growingDate++;

                if (cropState.growingDate >= cropState.seedSO.growTime && cropState.growthStage < cropState.seedSO.maxGrowthStage)
                {
                    cropState.growthStage++;
                    cropState.growingDate = 0;
                }
            }
            UpdateCropSprite(tilePosition);
        }
    }
}

 

UpdateGrowth에서는 작물이 물을 받았는지 확인하고, 자란 날짜에 따라 성장단계를 갱신하는 역할을 한다.

이 과정에서 cropState 객체는 참조를 통해 직접 수정된다.

 

2.2 구조체 CropSaveData

반면 CropSaveData는 CropState를 그대로 저장하는데 불편함이 있어 만들어진 구조체이다.

CropSaveData를 구조체로 한 이유는 다음과 같다.

1. CropSaveData는 세이브와 로드에만 사용되는 구조체로, 변경될 일이 없다.

2. 값타입이 생성될 때는 복사가 이뤄지는데, 이는 저장과 로드시에 CropState가 변형되는 것을 방지해준다.

 

또한 CropSaveData가 되는 과정에서 CropState의 객체에는 영향을 주지 않게 된다.

public List<CropSaveData> SaveCropSaveData()
{
    List<CropSaveData> cropSaveDatas = new List<CropSaveData>();

    foreach (var (cropPosition, cropState) in cropStateDictionary)
    {
        CropSaveData cropSaveData = new CropSaveData(cropPosition, cropState);

        cropSaveDatas.Add(cropSaveData);
    }

    return cropSaveDatas;
}

public void LoadFromCropSaveData(List<CropSaveData> p_cropSaveDatas)
{
    foreach (CropSaveData cropSaveData in p_cropSaveDatas)
    {
        CropState cropState = new CropState();

        cropState.seedSO = (SeedSO)ItemCodeMapper.GetItemSo(cropSaveData.seedItemCode);
        cropState.growthStage = cropSaveData.growthStage;
        cropState.growingDate = cropSaveData.growingDate;

        GetCropObject(cropSaveData.cropPosition, cropState);
    }
}

 

 

SaveCropSaveData는 현재 게임 상태의 모든 작물을 CropSaveData로 변환하여 저장한다. 변환 과정에서 CropState 객체는 그대로 유지되며, 데이터만 복사된다.

LoadFromCropSaveData는 저장된 CropSaveData를 불러와, 새로운 CropState 객체를 생성하고 게임 상태를 복구한다.

 

 

클래스와 구조체는 각각의 역할에 따라 적절히 사용되었으며, 게임에서의 데이터를 효율적으로 관리할 수 있도록 설계되었다.

 

 

3. 결론

 

클래스와 구조체는 C#에서 데이터 관리와 객체지향 설계의 핵심적인 도구로, 각각의 특징이 명확하기에 목적에 맞게 적절히 사용해야한다.

 

요약하면 다음과 같다.

1. 클래스는 상속이나 복잡한 상태관리가 필요할 경우 적합하다.

2. 구조체는 단순한 데이터 묶음으로, 불변성과 독립성이 필요한 경우 적합하며, 값 타입이기에 성능적으로 유리할 수 있다.

 

이를 CropState와 CropSaveData를 비교하면서 클래스와 구조체에 대해 더 이해할 수 있었다.

 

 

클래스와 구조체는 C#에서 데이터 관리와 객체지향 설계의 핵심 도구로, 각각의 특성과 목적에 따라 적절히 사용해야 한다.

 

이를 요약하면 다음과 같다.

 

1. 클래스는 상속, 다형성, 복잡한 상태 관리가 필요하거나 참조를 통해 데이터를 공유해야 하는 경우에 적합하다.

2. 구조체는 단순한 데이터 묶음을 표현하며, 불변성과 독립성이 요구될 때 적합하다. 값 타입으로 동작하기 때문에 성능적으로 유리할 수 있다.

 

이번에 CropState(클래스)와 CropSaveData(구조체)를 비교하며, 클래스와 구조체가 서로 다른 요구사항을 충족하기 위해 설계되었음을 이해할 수 있었다. 이를 바탕으로, 앞으로도 데이터의 특성과 목적에 따라 적절한 선택을 내릴 수 있도록 노력할 것이다.