struct 는 값을 담고 class 는 주소를 담는다
Esc

    struct 는 값을 담고 class 는 주소를 담는다

    –C#메모리

    앞에서 값 타입과 참조 타입을 갈랐습니다. int 는 값을 직접 담고, Dog 는 주소를 담았습니다. 그때 값 타입 목록은 int, double, bool, char 처럼 이미 있는 것들이었습니다.

    그런데 그 목록에 우리가 만든 것을 추가할 수 있습니다.

    class 대신 struct 라고 쓰면 값 타입이 됩니다. 힙에 안 가고, 대입하면 통째로 복사됩니다.

    struct 는 이렇게 생겼다

    class 자리에 struct 라고 쓰는 것이 전부입니다.

    struct 점
    {
        public int X;
        public int Y;
    }

    쓰는 법도 똑같이 생겼습니다.

    점 p = new 점();
    p.X = 3;
    p.Y = 4;

    new 를 쓰는 모양까지 같습니다. 그런데 이 new 는 힙에 자리를 잡지 않습니다. struct 에서 new 는 “자리를 만들어라” 가 아니라 “생성자를 불러 채워라” 에 가깝습니다. 자리는 이미 p 라는 변수 그 자체입니다.

    대입하면 통째로 복사된다

    이게 유일하게 중요한 차이입니다. 나머지는 전부 여기서 나옵니다.

    struct 로 바꾸면 힙이 아예 등장하지 않습니다.

    같은 코드에서 결과가 갈립니다.

    b.X = 99;
    Console.WriteLine(a.X);

    점 이 class 면 99, struct 면 3 입니다. 코드는 한 글자도 안 달라졌는데 답이 다릅니다.

    메서드에 넘길 때도 같다

    메서드 편에서 “넘어가는 것은 값의 사본이다” 라고 하고, 왜 결과가 다른지는 미뤄 뒀습니다. 그 답이 이것입니다.

    넘어가는 것이 사본이라는 규칙은 언제나 같습니다. 다만 무엇의 사본인지가 다릅니다.

    무엇이 복사되나 안에서 필드를 고치면
    struct 값 전체 바깥에 반영 안 됨
    class 주소 바깥에 반영됨

    class 를 넘기면 주소가 복사됩니다. 주소는 복사됐지만 그 주소가 가리키는 상자는 하나입니다. 그래서 안에서 dog.Age = 5 를 하면 바깥에서도 5 입니다.

    struct 를 넘기면 값 전체가 복사됩니다. 안에서 p.X = 5 를 해도 바깥의 p 는 그대로입니다. 바깥까지 바꾸고 싶으면 ref 를 붙여야 합니다. ref 가 있는 이유가 여기 있습니다.

    왜 나눠 놨나

    int 하나를 쓸 때마다 힙에 자리를 잡고 주소를 들고 다닌다고 생각해 보면 답이 나옵니다.

    4바이트 값 하나를 위해 힙에 상자를 만들고, 주소 8바이트를 스택에 담고, 나중에 GC 가 그 상자를 치웁니다. 값 자체보다 값을 관리하는 비용이 더 큽니다.

    작고 개수가 많은 것은 그냥 자리에 담는 편이 낫습니다. 그래서 값 타입이 있습니다.

    Unity 를 보면 이 판단이 그대로 보입니다.

    Vector3 위치 = transform.position;

    Vector3 는 float 셋입니다. 게임 한 프레임에 이런 것이 수만 개씩 오갑니다. 이게 class 였으면 프레임마다 힙에 수만 개가 생기고 GC 가 계속 돌았을 겁니다. 그래서 Vector3, Quaternion, Color, Rect 는 전부 struct 입니다.

    함정 하나: 값을 꺼내면 사본이다

    이걸 모르면 반드시 한 번은 막힙니다.

    transform.position.x = 5f;   // 컴파일 에러

    transform.position 은 필드가 아니라 프로퍼티입니다. 프로퍼티는 값을 돌려줍니다. Vector3 는 struct 라서 돌려받는 것이 사본입니다.

    즉 이 코드는 “사본을 하나 만들고, 그 사본의 x 를 고치고, 버려라” 가 됩니다. 아무 일도 안 일어나는 코드라서 컴파일러가 미리 막습니다.

    제대로 하려면 통째로 다시 넣어야 합니다.

    Vector3 p = transform.position;
    p.x = 5f;
    transform.position = p;

    같은 이유로 List<점> 안의 원소도 list[0].X = 5 로 못 고칩니다. list[0] 이 사본이기 때문입니다. 배열은 됩니다. 배열의 인덱서는 사본이 아니라 그 자리를 직접 가리키기 때문입니다. 같아 보이는 둘이 다르게 동작하는 자리라 헷갈리기 쉽습니다.

    그래서 언제 struct 로 만드나

    세 가지가 다 맞을 때만 값 타입으로 만드는 편이 안전합니다.

    • 작다. 대략 16바이트 안쪽. 크면 복사 비용이 주소 복사보다 비싸집니다
    • 만든 뒤에 안 바꾼다. 좌표, 색, 시간처럼 통째로 갈아 끼우는 것
    • 개수가 많다. 하나 만들고 마는 것이면 굳이 struct 일 이유가 없습니다

    셋 중 하나라도 어긋나면 class 가 낫습니다. 특히 바뀌는 값을 struct 에 담으면 위에서 본 사본 함정이 계속 발목을 잡습니다.

    정리

    • class 대신 struct 라고 쓰면 값 타입이 됩니다
    • 대입하면 주소가 아니라 값 전체가 복사됩니다. 그래서 서로 영향을 안 줍니다
    • 힙에 안 갑니다. 작고 많은 것을 담을 때 관리 비용이 사라집니다
    • Vector3 가 struct 라서 transform.position.x = 5 가 컴파일 에러입니다
    • 작고, 안 바뀌고, 개수가 많을 때만 씁니다