
만약 C#에서 다루려는 데이터가 2D 좌표(x, y)나 색상 값(R, G, B)처럼
아주 작고 단순하다면 어떨까요? 이런 간단한 데이터를 위해
우리는 항상 class라는 거대한 도구를 사용하는 것이 최선일까요? 이럴 때를 위해
C#은 구조체(struct)라는 가볍고 빠른 '값 형식(Value Type)'의 컨테이너를 제공합니다.
구조체는 클래스와 정말 비슷하게 생겼지만, 내부적으로 동작하는 방식이
완전히 달라서 성능에 영향을 줄 수 있는 중요한 개념이에요.
구조체를 이해하려면 '값 형식'과 '참조 형식'의 차이를 알고 있어야 합니다.
C#에서 데이터를 다루는 형식은 크게 두 가지로 나뉩니다.
class, interface, array 등struct, int, bool, enum 등구조체와 클래스의 가장 큰 차이는 "값을 직접 들고 있느냐, 값이 있는 곳을 가리키느냐"입니다.
클래스(참조 형식)는 '보물 지도'와 같습니다.
변수에는 실제 데이터가 있는 위치(주소)만 적혀있는 '지도'가 들어있습니다.
실제 데이터(보물)는 힙(Heap)이라는 크고 넓은 메모리 공간 어딘가에 저장됩니다.
지도를 복사해서 친구에게 줘도, 두 지도는 여전히 하나의 똑같은 보물을 가리킵니다.
그래서 친구가 보물을 바꿔치기하면, 내 지도로 찾아가도 바뀐 보물을 보게 됩니다.
구조체(값 형식)는 '작은 보석 상자' 그 자체와 같습니다.
변수 안에 실제 데이터인 '보석'이 들어있습니다. 친구에게 보석 상자를 복사해서 주면,
친구는 완전히 똑같은 내용물을 가진 새로운 상자를 받게 됩니다.
그래서 친구가 자기 상자의 보석을 바꿔도, 내 상자에는 아무런 영향이 없습니다.
구조체는 struct키워드로 선언하며, 주로 작고 간단한 데이터 값들을
캡슐화하기 위해 사용되는 값 형식(Value Type)입니다.
[문법]
public struct PointStruct
{
// 1. 구조체는 필드와 속성을 가질 수 있습니다.
public int X;
public int Y;
// 2. 구조체는 생성자와 메서드를 가질 수 있습니다.
public PointStruct(int x, int y)
{
X = x;
Y = y;
}
public void Display()
{
Console.WriteLine($"({X}, {Y})");
}
}
클래스(보물 지도)와 구조체(보석 상자)의 차이를 코드로 보면 더욱 명확해집니다.
[코드]
using System;
// 참조 형식 (보물 지도)
public class PointClass
{
public int X;
public int Y;
public PointClass(int x, int y) { X = x; Y = y; }
public void Display() { Console.WriteLine($"({X}, {Y})"); }
}
// 값 형식 (보석 상자)
public struct PointStruct
{
public int X;
public int Y;
public PointStruct(int x, int y) { X = x; Y = y; }
public void Display() { Console.WriteLine($"({X}, {Y})"); }
}
class Program
{
static void Main()
{
// 1. 클래스 (참조 형식)
Console.WriteLine("--- 클래스(참조 형식) 테스트 ---");
PointClass classPoint1 = new PointClass(10, 20);
PointClass classPoint2 = classPoint1; // '보물 지도'를 복사
Console.Write("변경 전 classPoint1: ");
classPoint1.Display();
// 복사본(classPoint2)의 값을 변경합니다.
classPoint2.X = 99;
Console.WriteLine("classPoint2.X를 99로 변경 후...");
Console.Write("변경 후 classPoint1: ");
classPoint1.Display(); // 원본의 값도 바뀌었습니다!
// 2. 구조체 (값 형식)
Console.WriteLine("\n--- 구조체(값 형식) 테스트 ---");
PointStruct structPoint1 = new PointStruct(10, 20);
PointStruct structPoint2 = structPoint1; // '보석 상자'를 통째로 복사
Console.Write("변경 전 structPoint1: ");
structPoint1.Display();
// 복사본(structPoint2)의 값을 변경합니다.
structPoint2.X = 99;
Console.WriteLine("structPoint2.X를 99로 변경 후...");
Console.Write("변경 후 structPoint1: ");
structPoint1.Display(); // 원본의 값은 그대로입니다!
}
}
[실행 결과]
--- 클래스(참조 형식) 테스트 ---
변경 전 classPoint1: (10, 20)
classPoint2.X를 99로 변경 후...
변경 후 classPoint1: (99, 20)
--- 구조체(값 형식) 테스트 ---
변경 전 structPoint1: (10, 20)
structPoint2.X를 99로 변경 후...
변경 후 structPoint1: (10, 20)
결과가 정말 명확하죠?
구조체는 데이터가 복사되고 클래스는 주소만 복사된다는 점이 핵심적인 차이입니다.
구조체는 클래스와 비슷하게 속성, 메서드, 생성자 등을 가질 수 있지만
몇 가지 중요한 특징이 있습니다.
상속 불가: 다른 클래스나 구조체를 상속받을 수 없습니다.
인터페이스 구현: 클래스처럼 인터페이스를 구현하여 특정 규약을 따를 수 있습니다.
성능 이점: 작고 수명이 짧은 데이터를 다룰 때, 클래스보다 빠를 수 있습니다.
C#에서 구조체는 클래스와 유사해 보이지만, 메모리 관리 방식과
동작에서 큰 차이가 있습니다. 이 차이점 때문에 구조체를 잘 사용하다면
성능 최적화의 도구가 되지만, 무분별하게 사용하면 성능이 저하될 수 있습니다.
다음의 조건을 만족할 때 구조체 사용을 고려해 보세요.
1. 논리적으로 '하나의 값'처럼 취급될 때
구조체는 개념적으로는 단일 값을 표현하는 데 가장 적합합니다.
예를 들어, Point는 X와 Y 좌표를 갖지만 '하나의 좌표'로 표현 됩니다.
2. 인스턴스의 크기가 16바이트 이하로 작을 때
일반적으로 구조체의 데이터 크기는 16바이트를 넘지 않는 것이 좋습니다.
인스턴스 크기가 크면 복사 비용이 참조(8바이트)를 전달하는 것보다 비싸집니다.
차라리 8바이트짜리 주소만 전달하는 클래스를 쓰는 게 성능에 더 유리합니다.
(16바이트 기준: long 2개, int 4개, short 8개, bool 16개)
3. 불변(Immutable)일 때
구조체는 복사 시 원본과 다른 객체가 되므로 생성된 후에
값이 변하지 않는 불변(Immutable)으로 만드는 것이 가장 이상적입니다.
구조체의 값 형식(Value Type)에서 발생할 수 있는 버그를 막습니다.
4. 박싱/언박싱이 자주 일어나지 않을 때
구조체는 값 형식입니다. 만약 object형식이나 인터페이스 형식의 변수에 담으면,
힙(Heap)에 객체를 새로 할당하고 값을 복사하는 박싱(Boxing)이 일어납니다.
박싱은 가비지 컬렉션(GC)의 부담을 늘려 성능 저하의 원인이 됩니다.
readonly: 구조체 내부의 모든 필드를 읽기 전용으로 만듭니다.
public readonly struct ImmutablePoint
{
public int X { get; }
public int Y { get; }
public ImmutablePoint(int x, int y)
{
X = x;
Y = y;
}
}
record struct: C# 10.0 이상에서는 구조체에 레코드의 기능을 사용할 수 있어요.
public readonly record struct ImmutablePoint(int X, int Y);
구조체는 스스로의 의지로 힙에 가지 않아요. 힙에 사는 객체(클래스, 배열)의
'운명 공동체'가 되면 어쩔 수 없이 같이 힙(Heap)에 살게 되는 것입니다.
상황 1: 지갑만 따로 있을 때 (일반적인 구조체)
public struct MyWallet { public int Money; }
class Program
{
static void Main()
{
void GoToConvenienceStore()
{
MyWallet myWallet; // '지갑'을 책상 위에 꺼내 놓음 (스택)
myWallet.Money = 10000;
// 편의점에서 물건 사고 메서드 종료
} // 메서드가 끝나면 책상 위 '지갑'은 바로 사라짐
}
}
여기서 MyWallet구조체는 메서드의 작업 공간(스택)에 잠시 만들어졌다가
메서드가 끝나면 사라집니다. 이게 구조제의 기본 규칙이죠.
상황 2: 가방 안에 지갑이 있을 때 (클래스의 필드인 구조체)
public struct MyWallet { public int Money; }
public class MyBag { public MyWallet Wallet; } // '가방'은 '지갑'을 가질 수 있다
class Program
{
static void Main()
{
void GoToTrip()
{
MyBag myBag = new MyBag(); // '가방'을 힙 메모리에서 만듦
myBag.Wallet.Money = 50000;
// ... 여행을 다님 ...
}
}
}
지갑이 혼자 있었다면 책상(스택)에 있었겠지만, 가방의 '내용물'이
되었기 때문에 운명 공동체가 되어 함께 힙(Heap)에 있게 된 것입니다.
상황 3: 상자 안에 지갑이 여러 개가 있을 때 (배열에 포함된 구조체)
public struct MyWallet { public int Money; }
class Program
{
static void Main()
{
void OrganizeWallets()
{
// '지갑' 3개를 담을 수 있는 '상자'(배열)를 힙 메모리에서 만듦
MyWallet[] walletBox = new MyWallet[3];
walletBox[0].Money = 10000;
walletBox[1].Money = 20000;
walletBox[2].Money = 30000;
}
}
}
walletBox라는 배열(상자) 자체가 힙 메모리에서 큰 공간을 할당받습니다.
그리고 그 공간 안에 MyWallet구조체 3개가 내용물로서 직접 저장됩니다.
따라서 이 구조체들도 힙 메모리에 위치하게 됩니다.
C#은 객체 지향 프로그래밍 언어로써, 기본 선택지는 클래스입니다.
성능 최적화가 필요한 특정 상황에서 구조체 사용을 고려해야 합니다.
처음에는 클래스 사용에 익숙해지는 것에 집중하시고,
나중에 성능 최적화가 필요한 상황에서 구조체를 사용해 보세요.
| 구분 | 구조체(Struct) | 클래스(Class) |
|---|---|---|
| 형식 | 값 형식(Value Type) | 참조 형식(Reference Type) |
| 저장 방식 | 변수가 실제 값을 직접 보관 | 변수가 힙 객체를 가리키는 참조(주소)를 보관 |
| 전달/할당 방식 | 데이터 전체가 값으로 복사 (Pass by Value) | 참조(주소)가 복사 (Pass by Value of Reference) |
| 상속 | 불가능 (인터페이스는 구현 가능) | 가능 |
| 핵심 역할 | 작고 불변인 '값(Value)'의 캡슐화 | 독립적인 정체성과 행위를 가진 '객체(Object)' |