C# 언어 핵심 정리

C#은 Microsoft가 개발한 범용 프로그래밍 언어로, .NET 플랫폼을 기반으로 실행된다.

Java와 비슷하게 가비지 컬렉션을 사용하는 관리형 언어이면서도 값 타입과 참조 타입을 구분하고, ref, out, unsafe, 포인터 같은 기능을 제공해 비교적 낮은 수준의 제어도 가능하다.

현재는 웹 서버, 데스크톱 애플리케이션, 게임, 클라우드 서비스, 자동화 도구 등 다양한 분야에서 사용된다.

특히 Unity를 이용한 게임 개발에서는 C#이 주요 스크립팅 언어로 사용되기 때문에 게임 개발에서도 자주 접하게 되는 언어다.


C#의 기본 구조

간단한 C# 프로그램은 다음과 같이 작성할 수 있다.

using System;

class Program
{
    static void Main()
    {
        int value = 10;

        Console.WriteLine(value);
    }
}

C# 코드는 일반적으로 .NET 런타임 위에서 실행된다.

전통적인 .NET 환경에서는 C# 소스 코드가 먼저 IL(Intermediate Language)로 컴파일되고 실행 시 CLR(Common Language Runtime)이 이를 실행한다.

현대 .NET에서는 JIT 컴파일뿐만 아니라 Native AOT 같은 방식도 존재하기 때문에 모든 C# 프로그램이 동일한 실행 경로를 갖는 것은 아니다.

일반적인 구조를 단순화하면 다음과 같다.

C# Source -> C# Compiler -> IL + Metadata -> .NET Runtime -> Machine Code

이 구조 때문에 C#은 C++처럼 플랫폼별 기계어 코드를 직접 작성하는 방식과는 다른 개발 모델을 가진다.


변수와 타입

C#은 정적 타입 언어다.

int health = 100;
float speed = 3.5f;
double distance = 100.0;
bool alive = true;
char grade = 'A';
string name = "Player";

변수를 선언할 때 타입을 명시할 수 있다.

또한 var를 사용할 수도 있다.

var health = 100;
var name = "Player";

여기서 var는 동적 타입이 아니다.

컴파일러가 오른쪽의 값을 보고 실제 타입을 결정한다.

var health = 100;
// health는 int

따라서 다음 코드는 불가능하다.

var health = 100;

// health = "100"; // 오류

var는 타입을 없애는 기능이 아니라 타입을 컴파일러가 추론하도록 하는 문법이다.


값 타입과 참조 타입

C#을 이해할 때 중요한 개념 중 하나다.

대표적인 값 타입은 다음과 같다.

int
float
double
bool
char
struct
enum

참조 타입에는 다음과 같은 것들이 있다.

class
string
array
delegate
interface

값 타입 변수는 값 자체를 가지고 있고, 참조 타입 변수는 객체에 대한 참조를 가진다.

int a = 10;
int b = a;

b = 20;

이 경우 a는 여전히 10이다.

반면 클래스 객체는 참조를 복사한다.

Player a = new Player();
Player b = a;

b.Health = 0;

ab가 같은 객체를 가리키고 있다면 a.Health 역시 0이 된다.

따라서 핵심은 다음과 같이 볼 수 있다.

값 타입 -> 값 자체를 전달
참조 타입 -> 객체를 가리키는 참조를 전달

이 차이는 객체를 전달하고 복사할 때 계속 등장한다.


클래스

클래스는 객체를 정의하는 기본적인 방법이다.

class Player
{
    public int Health;

    public void Attack()
    {
        Console.WriteLine("Attack");
    }
}

객체는 new를 사용해서 생성한다.

Player player = new Player();

player.Health = 100;
player.Attack();

클래스는 데이터와 동작을 하나의 단위로 묶을 수 있다.

게임이라면 Player, Monster, Weapon, Inventory 같은 객체를 클래스 형태로 표현할 수 있다.

Class -> Object -> State + Behavior

생성자

객체가 생성될 때 실행되는 것이 생성자다.

class Player
{
    public int Health;

    public Player(int health)
    {
        Health = health;
    }
}
Player player = new Player(100);

생성자를 사용하면 객체가 생성된 직후 필요한 상태를 보장할 수 있다.

필드 초기화와 함께 사용하는 경우도 많다.

class Player
{
    private int health = 100;
}

객체 생성 흐름은 다음과 같이 볼 수 있다.

new Player(100) -> 생성자 호출 -> 객체 초기화 -> 사용

프로퍼티

C#에서는 필드를 직접 노출하기보다 프로퍼티를 사용하는 경우가 많다.

class Player
{
    public int Health { get; set; }
}

사용하는 방식은 필드와 비슷하다.

player.Health = 100;

int hp = player.Health;

하지만 프로퍼티는 단순한 변수보다 접근을 제어할 수 있다.

class Player
{
    public int Health
    {
        get;
        private set;
    }
}

이렇게 하면 외부에서는 Health를 읽을 수 있지만 값을 변경하는 것은 클래스 내부로 제한할 수 있다.

값을 설정할 때 로직을 넣는 것도 가능하다.

public int Health
{
    get => health;

    set
    {
        health = Math.Max(0, value);
    }
}

private int health;

프로퍼티는 C#에서 캡슐화를 구현하는 대표적인 문법이다.

외부 접근 -> Property -> 내부 상태

접근 제한자

클래스나 멤버의 접근 범위를 제한할 수 있다.

대표적으로 다음이 있다.

public
private
protected
internal

예를 들어:

class Player
{
    public void Attack()
    {
    }

    private int health;
}

외부에서는 Attack()을 호출할 수 있지만 health에는 직접 접근할 수 없다.

외부 코드 -> public Member -> 객체
외부 코드 -X-> private Member

이런 식으로 객체 내부 구현과 외부 인터페이스를 분리한다.


상속

C#에서는 클래스를 상속할 수 있다.

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog");
    }
}
Animal animal = new Dog();

animal.Speak();

virtualoverride를 사용하면 런타임 다형성을 구현할 수 있다.

Animal -> Dog
Animal Reference -> Dog Object -> Dog.Speak()

C#의 클래스 상속은 하나의 클래스만 상속할 수 있다.

여러 타입의 기능을 조합해야 한다면 인터페이스를 사용하는 경우가 많다.


인터페이스

인터페이스는 특정 기능을 제공한다는 계약을 정의하는 데 사용한다.

interface IDamageable
{
    void TakeDamage(int damage);
}

여러 클래스가 같은 인터페이스를 구현할 수 있다.

class Player : IDamageable
{
    public void TakeDamage(int damage)
    {
    }
}

class Monster : IDamageable
{
    public void TakeDamage(int damage)
    {
    }
}

이제 PlayerMonster의 구체적인 타입을 몰라도 IDamageable을 통해 공통 기능을 사용할 수 있다.

void ApplyDamage(IDamageable target)
{
    target.TakeDamage(10);
}

구조를 단순화하면 다음과 같다.

Player -> IDamageable
Monster -> IDamageable
IDamageable -> TakeDamage()

인터페이스는 객체 간 결합도를 낮추고 교체 가능한 구조를 만들 때 유용하다.


struct

struct는 값 타입을 정의한다.

struct Vector2
{
    public float X;
    public float Y;
}

클래스와 달리 구조체 변수는 기본적으로 값 자체를 가진다.

Vector2 a = new Vector2();
Vector2 b = a;

b.X = 10;

이 경우 a.X는 변경되지 않는다.

게임 개발에서는 위치, 방향, 색상, 좌표처럼 작은 데이터를 표현할 때 구조체가 자주 사용된다.

다만 struct가 무조건 스택에 저장된다고 생각하면 안 된다.

값 타입이라는 것과 메모리가 반드시 스택에 존재한다는 것은 다른 문제다.

struct -> Value Type
Value Type -X-> 무조건 Stack

실제 메모리 배치는 변수의 위치와 객체 구성, JIT 최적화, 실행 환경 등에 따라 달라질 수 있다.


enum

열거형은 특정 값들을 이름으로 표현할 때 사용한다.

enum PlayerState
{
    Idle,
    Move,
    Attack,
    Dead
}
PlayerState state = PlayerState.Attack;

숫자로 상태를 관리하는 것보다 코드의 의미가 명확하다.

게임 상태, 아이템 종류, 방향, 팀 구분 같은 값을 표현할 때 자주 사용한다.

PlayerState -> Idle / Move / Attack / Dead

배열과 컬렉션

배열은 같은 타입의 데이터를 저장한다.

int[] scores = new int[10];

크기가 고정되어 있기 때문에 데이터 개수가 변하는 경우에는 List<T>를 사용하는 것이 일반적이다.

List<int> scores = new List<int>();

scores.Add(100);
scores.Add(200);

C#의 컬렉션은 대부분 제네릭을 사용한다.

대표적인 자료구조는 다음과 같다.

List<T>
Dictionary<TKey, TValue>
HashSet<T>
Queue<T>
Stack<T>
LinkedList<T>

자료의 접근 방식과 삽입, 삭제, 검색 특성에 따라 적절한 자료구조를 선택해야 한다.


제네릭

제네릭은 타입을 매개변수처럼 사용하는 기능이다.

class Container<T>
{
    public T Value;
}

사용할 때 타입을 지정한다.

Container<int> a = new Container<int>();
Container<string> b = new Container<string>();

컬렉션에서도 제네릭이 사용된다.

List<int>
List<string>
Dictionary<int, Player>

제네릭을 사용하면 여러 타입에 대응하는 코드를 만들면서도 컴파일 타임 타입 검사를 유지할 수 있다.

Generic<T> -> 실제 타입 지정 -> 타입이 결정된 코드

메서드

C#에서는 클래스 내부의 함수를 메서드라고 부른다.

class Player
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

정적 메서드도 사용할 수 있다.

static int Add(int a, int b)
{
    return a + b;
}

객체 상태를 사용하지 않는 기능이라면 static 메서드로 분리할 수 있다.

Object Method -> Object State 사용 가능
Static Method -> Object Instance 없이 호출

ref와 out

C#에서는 기본적으로 값이 복사되어 메서드에 전달된다.

void Add(int value)
{
    value++;
}

원본 변수의 값을 변경해야 한다면 ref를 사용할 수 있다.

void Add(ref int value)
{
    value++;
}
int value = 10;

Add(ref value);

out은 메서드가 호출자에게 값을 전달하는 다른 방법으로 사용할 수 있다.

bool TryGetValue(string text, out int value)
{
    return int.TryParse(text, out value);
}

특히 TryParse 패턴에서 자주 볼 수 있다.

일반 전달 -> 값 복사
ref -> 호출자 변수와 연결된 값 수정
out -> 메서드에서 호출자에게 값 전달

최근 C#에서는 여러 값을 반환해야 하는 상황에서 튜플이나 사용자 정의 타입을 사용하는 방법도 많기 때문에 ref, out을 무조건 사용할 필요는 없다.


nullable

C#에서는 참조 타입에 null이 들어갈 수 있다.

Player player = null;

null을 잘못 사용하면 NullReferenceException이 발생할 수 있다.

현대 C#에서는 nullable reference types를 사용할 수 있다.

Player? player = null;

?를 사용하면 이 참조가 null일 수 있다는 것을 타입 시스템에 명시할 수 있다.

값 타입에도 nullable을 사용할 수 있다.

int? score = null;

이 경우 int가 아닌 Nullable<int>에 해당한다.

Player -> 객체 또는 null
Player? -> null 가능성을 타입 시스템에 표현
int? -> 값 또는 null

예외 처리

C#은 try, catch, finally를 이용해 예외를 처리한다.

try
{
    int value = int.Parse("abc");
}
catch (FormatException)
{
    Console.WriteLine("Invalid format");
}

예외는 정상적인 프로그램 흐름을 표현하기 위한 용도라기보다는 예상하지 못했거나 특별히 처리해야 하는 오류 상황을 전달하는 용도로 사용하는 편이 좋다.

실패가 일반적인 상황이라면 반환값이나 TryXXX 패턴 같은 방식이 더 적절할 수 있다.

예외 발생 -> throw -> catch -> 오류 처리

delegate

C#에는 메서드를 객체처럼 전달할 수 있는 delegate가 있다.

delegate void Callback(int value);

메서드를 저장하거나 전달할 수 있다.

void Print(int value)
{
    Console.WriteLine(value);
}

Callback callback = Print;
callback(10);

delegate는 이벤트와 람다, 콜백 시스템의 기반이 된다.

Method -> Delegate -> 전달 / 저장 -> 호출

Action과 Func

실제로는 직접 delegate 타입을 선언하기보다 ActionFunc을 자주 사용한다.

Action<int> print = value =>
{
    Console.WriteLine(value);
};

Func은 반환값이 있는 함수를 표현한다.

Func<int, int, int> add = (a, b) => a + b;

마지막 타입이 반환 타입이다.

Func<int, int, int> -> int 입력 -> int 입력 -> int 반환

콜백이나 LINQ와 함께 사용하면 자주 등장한다.


이벤트

이벤트는 객체의 상태 변화나 특정 사건을 다른 객체에 전달할 때 사용한다.

public event Action OnDead;

발생시킬 때는 다음과 같이 작성할 수 있다.

OnDead?.Invoke();

다른 객체는 이벤트를 구독할 수 있다.

player.OnDead += HandlePlayerDead;

구독을 해제할 수도 있다.

player.OnDead -= HandlePlayerDead;

전체 흐름은 다음과 같다.

Event Publisher -> Event 발생 -> Subscriber 호출

이벤트를 사용하면 객체가 다른 객체의 구체적인 구현을 직접 호출하지 않고도 상태 변화를 전달할 수 있다.


람다

람다는 이름이 없는 함수를 표현한다.

Func<int, int, int> add =
    (a, b) => a + b;

컬렉션을 처리할 때 특히 많이 사용한다.

var result = scores
    .Where(x => x >= 100)
    .ToList();

여기서 x => x >= 100이 조건을 표현하는 람다다.

Lambda -> Delegate / Func / Action -> 함수처럼 전달

LINQ

LINQ(Language Integrated Query)는 컬렉션이나 데이터 소스를 질의하는 기능이다.

var result = scores
    .Where(x => x >= 100)
    .OrderByDescending(x => x)
    .ToList();

기존에는 반복문으로 작성해야 했던 필터링과 정렬 등의 작업을 표현식 형태로 작성할 수 있다.

대표적인 LINQ 메서드는 다음과 같다.

Where
Select
OrderBy
OrderByDescending
First
FirstOrDefault
Any
All
Contains
Count
GroupBy
ToList
ToDictionary

다만 LINQ가 항상 더 빠른 것은 아니다.

게임의 매 프레임 실행되는 코드처럼 성능에 민감한 부분에서는 반복적인 LINQ 사용으로 인한 할당이나 열거 비용 등을 확인할 필요가 있다.

Collection -> LINQ Query -> Filter / Transform / Sort -> Result

async와 await

C#은 비동기 프로그래밍을 위한 asyncawait를 제공한다.

async Task LoadData()
{
    var data = await LoadAsync();
}

await는 비동기 작업이 완료될 때까지 현재 메서드의 실행을 중단하고, 작업이 완료된 이후 이어서 실행하도록 만든다.

중요한 점은 async가 스레드를 새로 만드는 기능이 아니라는 것이다.

비동기 I/O 작업에서는 작업이 완료되기를 기다리는 동안 스레드를 계속 점유하지 않는 방식으로 활용할 수 있다.

웹 서버에서 데이터베이스나 네트워크 요청을 처리할 때 특히 중요하다.

Async Operation -> await -> 작업 대기 -> 작업 완료 -> 이후 코드 실행

Task

Task는 비동기 작업을 표현하는 대표적인 타입이다.

async Task<int> GetScoreAsync()
{
    await Task.Delay(1000);

    return 100;
}

호출하는 쪽에서는 다음처럼 사용할 수 있다.

int score = await GetScoreAsync();

여러 비동기 작업을 동시에 기다리는 것도 가능하다.

Task a = LoadAAsync();
Task b = LoadBAsync();

await Task.WhenAll(a, b);

이 경우 서로 독립적인 I/O 작업을 불필요하게 순차 실행하지 않을 수 있다.

LoadAAsync + LoadBAsync -> Task.WhenAll -> 두 작업 완료 -> 다음 코드 실행

가비지 컬렉션

C#은 기본적으로 가비지 컬렉터가 관리하는 메모리 모델을 사용한다.

Player player = new Player();

객체를 생성한 뒤 직접 delete할 필요는 없다.

더 이상 접근할 수 없는 객체는 GC가 나중에 메모리를 회수한다.

객체 생성 -> 사용 -> 참조 제거 -> GC 대상 -> 메모리 회수

개발자가 직접 메모리를 해제하지 않아도 된다는 점은 생산성을 크게 높여준다.

하지만 GC가 존재한다고 해서 메모리 문제가 사라지는 것은 아니다.

불필요한 객체를 계속 생성하면 GC가 자주 실행될 수 있고 이에 따른 비용이 발생한다.

특히 게임에서는 매 프레임 수많은 객체를 생성하는 코드를 작성하면 GC로 인한 프레임 끊김이 발생할 수 있다.

그래서 게임 개발에서는 객체 풀링이나 할당 최소화 같은 기법을 사용하기도 한다.

Object Allocation -> Garbage Accumulation -> GC -> Execution Cost

IDisposable과 using

GC가 관리하는 메모리와 운영체제 리소스는 별개의 문제다.

파일, 소켓, 데이터베이스 연결처럼 명시적으로 해제해야 하는 리소스가 있다.

이런 리소스를 관리할 때 IDisposable을 사용한다.

using (var stream = File.OpenRead("data.txt"))
{
    // 파일 사용
}

using 블록을 벗어나면 Dispose()가 호출된다.

현대 C#에서는 다음과 같이 더 간결하게 작성할 수도 있다.

using var stream = File.OpenRead("data.txt");

구조를 보면 다음과 같다.

Resource Acquire -> Use -> Dispose

C++의 RAII와 비슷한 목적을 가지지만 C#의 메모리 관리는 GC가 담당하고 Dispose는 GC가 아닌 개발자가 관리해야 하는 외부 리소스의 정리를 위한 것이라는 차이가 있다.


패턴 매칭

최근 C#은 패턴 매칭 문법도 상당히 발전했다.

if (value is int number)
{
    Console.WriteLine(number);
}

switch에서도 사용할 수 있다.

string result = value switch
{
    int n when n > 0 => "Positive",
    int n when n < 0 => "Negative",
    _ => "Other"
};

타입 검사와 조건 처리를 코드 안에서 명확하게 표현할 수 있다.

Value -> Pattern Match -> 조건 확인 -> 해당 분기 실행

record

record는 데이터를 표현하는 데 적합한 타입을 만들기 위한 기능이다.

public record PlayerData(
    string Name,
    int Level
);

클래스와 비슷하지만 값 기반 동등성 비교 같은 기능이 기본적으로 제공된다.

데이터 전달 객체나 불변 데이터 모델을 표현할 때 유용하다.

일반적인 게임 객체처럼 정체성과 생명주기가 중요한 객체에는 class가 더 자연스러운 경우가 많다.

Record -> Data 중심 객체
Class -> Identity + State + Behavior 중심 객체

C#의 메모리 구조를 볼 때

C#에서는 흔히 다음과 같이 설명한다.

Value Type -> 값 자체
Reference Type -> 객체에 대한 참조

하지만 이를 곧바로 다음과 같이 외우면 문제가 생긴다.

Value Type = Stack
Reference Type = Heap

실제 메모리 배치는 변수의 위치, 객체의 구성, JIT 최적화, 실행 환경 등에 따라 달라질 수 있다.

struct가 항상 스택에 존재하는 것도 아니고, 참조 타입 변수 자체가 항상 힙에 존재하는 것도 아니다.

따라서 다음 두 개념을 구분해야 한다.

Type Semantics -> 값 타입 / 참조 타입
Memory Placement -> 실제 메모리 배치

C#을 제대로 이해하려면 먼저 값 타입과 참조 타입의 의미를 이해하고, 그 다음 실제 메모리 배치를 별도의 문제로 보는 것이 좋다.


C#과 .NET

C#과 .NET은 같은 개념이 아니다.

C#은 프로그래밍 언어이고 .NET은 실행 환경과 표준 라이브러리, 개발 도구 등을 포함하는 플랫폼이다.

C# -> Compiler -> IL -> .NET Runtime

.NET은 크게 다음과 같은 요소로 볼 수 있다.

.NET -> Runtime + Base Class Library + SDK + Development Tools

그래서 C#으로 파일을 읽을 때 File, 네트워크 통신을 할 때 HttpClient, 컬렉션을 사용할 때 List<T> 등을 사용할 수 있다.

이 기능들은 C# 문법 자체라기보다 .NET에서 제공하는 라이브러리 기능이다.

C# -> Language
.NET -> Runtime + Library + SDK + Tooling

C#과 게임 개발

C#은 게임 개발에서도 상당히 많이 사용된다.

특히 Unity에서는 게임 로직을 C#으로 작성한다.

public class Player : MonoBehaviour
{
    void Update()
    {
        Move();
    }
}

Unity에서는 C# 스크립트가 엔진에서 제공하는 객체와 연결되어 게임 로직을 구현한다.

여기서는 일반적인 C# 문법뿐만 아니라 엔진의 생명주기, 메모리 관리, 직렬화, 이벤트, 프레임 실행 구조까지 같이 이해해야 한다.

C# -> Unity Script -> Unity Engine -> Game Runtime

C# 자체는 언어이고 Unity는 그 언어를 사용하는 게임 엔진이다.

둘을 구분해서 이해하는 것이 좋다.


C#을 사용할 때 생각해야 하는 것

C#은 C++에 비해 메모리를 직접 관리해야 하는 부담이 적다.

하지만 그렇다고 시스템적인 사고가 필요 없는 언어는 아니다.

실제로 개발하다 보면 다음 문제를 계속 만나게 된다.

Object Lifetime
Reference
Memory Allocation
GC
Concurrency
Async
Data Structure
Performance
Exception
Resource Management

특히 async/await, LINQ, delegate, event 같은 기능은 편리하지만 내부 동작을 모른 채 사용하면 성능이나 객체 수명 문제를 만들 수 있다.

언어가 제공하는 추상화 수준이 높아질수록 내부 동작을 전혀 몰라도 된다는 의미는 아니다.

High-Level Abstraction -> 편의성 증가
High-Level Abstraction -> 내부 비용 은닉
내부 동작 이해 -> 성능 / 메모리 / 수명 문제 판단

C# 전체 구조

C#을 하나의 흐름으로 보면 다음과 같이 정리할 수 있다.

C# Syntax -> Type System -> Value / Reference Type -> Class / Interface / Struct -> Generic / Collection -> Delegate / Event / Lambda -> LINQ -> Async / Await / Task -> GC / IDisposable -> .NET Runtime

실제 객체를 중심으로 보면 다음과 같다.

Type Definition -> Object Creation -> State Management -> Method Call -> Reference Relationship -> Object Lifetime -> GC / Dispose

비동기 처리는 다음 흐름으로 볼 수 있다.

Async Operation -> Task -> await -> I/O Wait -> Completion -> Continuation

게임 개발에서는 여기에 엔진의 실행 흐름이 추가된다.

C# Script -> Unity Engine -> Game Object -> Update -> Game Logic -> Rendering / Physics / Input

정리

C#은 단순히 문법이 쉬운 객체지향 언어로 볼 수 있는 언어가 아니다.

타입 시스템부터 객체 모델, 참조 관계, 메모리 관리, 비동기 처리, 이벤트, 제네릭, LINQ, .NET 런타임까지 서로 연결되어 있다.

핵심 구조를 압축하면 다음과 같다.

C# Language -> Type System -> Object Model -> Reference -> Lifetime -> Async -> Runtime

C#은 C++처럼 개발자가 메모리 할당과 해제를 직접 관리하는 언어는 아니다. 대신 GC를 통해 객체의 메모리 관리를 런타임에 맡기고 개발자는 객체의 구조와 참조 관계에 더 집중할 수 있다.

그렇다고 메모리를 신경 쓰지 않아도 되는 것은 아니다.

어떤 객체가 생성되고 얼마나 오래 살아있는지, 어떤 객체를 참조하고 있는지, 불필요한 할당이 발생하지 않는지를 알아야 한다.

특히 게임 개발에서는 GC, 객체 수명, 메모리 할당, Update 주기, 비동기 처리 같은 요소가 실제 성능과 직접 연결될 수 있다.

C#을 이해할 때 중요한 것은 문법을 많이 아는 것이 아니다.

Syntax -> Type -> Object -> Reference -> Lifetime -> Runtime Behavior

이 흐름을 이해하는 것이 핵심이다.

문법 자체는 익숙해지면 어렵지 않다. 결국 중요한 것은 그 문법이 런타임에서 어떤 객체와 메모리, 실행 흐름을 만들어내는지를 이해하는 것이다.