[PART7.클래스와 객체 입문(10/21)] Target-typed new — 왼쪽 타입을 반복하지 않는 생성 (C# 9)
좌변·인자·반환의 "문맥"으로 타입을 추론한다 / var는 변수 타입을 생략, new()는 생성 타입을 생략 / IL은 일반 new T()와 완전히 동일
목차
1. [문제 제기] — 같은 타입을 두 번 쓰는 게 정상인가
Unity에서 데이터 테이블이나 풀(Pool)을 짜다 보면 이런 코드를 한 줄에 욱여넣게 됩니다.
Dictionary<string, List<EnemySpawnInfo>> waveTable = new Dictionary<string, List<EnemySpawnInfo>>();
왼쪽에 한 번 쓴 Dictionary<string, List<EnemySpawnInfo>>를 오른쪽에 똑같이 한 번 더 씁니다. 타이핑이 두 배 늘어나고, 줄이 길어져 한 화면에 들어오지 않습니다. 더 큰 문제는 리팩토링입니다. EnemySpawnInfo를 다른 타입으로 바꾸려면 좌변 한 곳만 바꿔도 될 것 같은데 우변까지 같이 고쳐야 합니다. IDE가 자동으로 잡아주긴 하지만 코드를 읽는 사람 머릿속에서는 같은 정보가 두 번 처리됩니다.
해결책으로 var를 떠올리는 분이 많습니다.
var waveTable = new Dictionary<string, List<EnemySpawnInfo>>();
지역 변수에서는 var로 좌변을 줄일 수 있습니다. 그런데 필드·프로퍼티에서는 var를 쓸 수 없습니다.
public class WaveManager : MonoBehaviour
{
// ❌ 컴파일 에러 — 필드는 명시적 타입이 필수
var waveTable = new Dictionary<string, List<EnemySpawnInfo>>();
// 어쩔 수 없이 타입을 두 번 적는다
Dictionary<string, List<EnemySpawnInfo>> waveTable = new Dictionary<string, List<EnemySpawnInfo>>();
}
여기에 var로는 풀 수 없는 또 다른 케이스가 있습니다 — 메서드 인자, 반환문, 컬렉션 초기화 안의 요소. 이런 곳에서는 좌변이 이미 명확하지만 우변에서 타입을 또 적어야 합니다.
C# 9 가 도입한 Target-typed new 는 정확히 이 문제를 해결합니다. 좌변(또는 매개변수·반환·요소 타입)이 이미 알려져 있으면, 우변의 new 뒤에서 타입 이름을 생략할 수 있습니다.
Dictionary<string, List<EnemySpawnInfo>> waveTable = new();
이 글에서는 컴파일러가 어떻게 타입을 추론하는지, 어디서 쓸 수 있고 어디서 못 쓰는지, IL 레벨에서는 어떻게 보이는지, 그리고 Unity 신입이 자주 만나는 함정을 차례로 봅니다.
2. [개념 정의] — 문맥이 타입을 알려준다
비유 — "그거 두 잔 주세요"
카페에서 친구가 카푸치노를 시킨 직후 내가 "그거 두 잔 주세요" 라고 하면 직원은 카푸치노 두 잔을 내옵니다. 내가 카푸치노라는 단어를 입에 올리지 않았는데도 메뉴가 결정됩니다. 앞의 문맥이 "그거"를 카푸치노로 해석시켰기 때문입니다.
Target-typed new 도 같은 원리입니다. new() 자체에는 어떤 타입을 만들지 적혀 있지 않습니다. 주변의 문맥(좌변 변수, 매개변수, 반환 타입 등)이 타입을 알려주면 컴파일러가 그 타입의 생성자를 호출하는 코드로 변환합니다.
구조 — 추론의 방향
핵심: var는 우변에서 좌변을 추론하고, new()는 좌변에서 우변을 추론합니다. 추론의 방향이 정반대라서 둘을 섞으면(var x = new();) 컴파일러가 타입을 결정할 단서를 어디에서도 찾지 못합니다.
new()— Target-typed new 표현식 (C# 9) 객체 생성 시new키워드 뒤의 타입 이름을 생략하고, 좌변 변수·매개변수·반환 타입 등 대상(target) 타입으로부터 컴파일러가 자동 추론하게 한다. 인자가 있으면new("A", 10), 없으면new()형태로 쓴다.
예시:Player p = new("A", 10);좌변이Player이므로new Player("A", 10)과 동일하게 컴파일된다.
기본 코드 — new T() 와 new() 비교
Unity에서 이런 상황이 생긴다. 적 캐릭터(Player) 한 명을 만들고 적 리스트와 길드 테이블을 같이 초기화한다.
public class Player
{
public string Name { get; }
public int Hp { get; }
public Player(string name, int hp) { Name = name; Hp = hp; }
}
public class Demo
{
// 기존 방식 — 타입을 두 번 적는다
public static void Old()
{
Player p = new Player("A", 10);
List<Player> list = new List<Player>();
list.Add(p);
Dictionary<string, List<Player>> dict = new Dictionary<string, List<Player>>();
}
// C# 9 — 타입 이름을 생략한다
public static void Newer()
{
Player p = new("A", 10);
List<Player> list = new();
list.Add(p);
Dictionary<string, List<Player>> dict = new();
}
}
위처럼 동작하기 때문에 좌변에 한 번만 타입을 적으면, 우변의 new()는 같은 타입의 생성자를 호출하는 코드로 변환됩니다. 인자가 있으면 인자에 맞는 생성자가, 없으면 매개변수 없는 생성자가 선택됩니다.
이 두 메서드의 IL을 직접 비교해보겠습니다.
// === Old() — 기존 방식 ===
.method public hidebysig static void Old () cil managed
{
.maxstack 2
.locals init (
[0] class Player
)
IL_0000: ldstr "A" // 문자열 "A" 푸시
IL_0005: ldc.i4.s 10 // 정수 10 푸시
IL_0007: newobj instance void Player::.ctor(string, int32) // Player 생성자 호출
IL_000c: stloc.0 // 지역변수 p에 저장
IL_000d: newobj instance void class [...].List`1<class Player>::.ctor() // List<Player> 생성
IL_0012: ldloc.0 // p 푸시
IL_0013: callvirt instance void class [...].List`1<class Player>::Add(!0) // list.Add(p)
IL_0018: newobj instance void class [...].Dictionary`2<...>::.ctor() // Dictionary 생성
IL_001d: pop
IL_001e: ret
}
// === Newer() — Target-typed new ===
.method public hidebysig static void Newer () cil managed
{
.maxstack 2
.locals init (
[0] class Player
)
IL_0000: ldstr "A" // 동일
IL_0005: ldc.i4.s 10 // 동일
IL_0007: newobj instance void Player::.ctor(string, int32) // 동일
IL_000c: stloc.0 // 동일
IL_000d: newobj instance void class [...].List`1<class Player>::.ctor() // 동일
IL_0012: ldloc.0 // 동일
IL_0013: callvirt instance void class [...].List`1<class Player>::Add(!0) // 동일
IL_0018: newobj instance void class [...].Dictionary`2<...>::.ctor() // 동일
IL_001d: pop
IL_001e: ret
}
IL 해설. 두 메서드의 IL은 바이트 단위로 완전히 동일합니다. 코드 크기도 둘 다 0x1f(31바이트)이고, 명령어 시퀀스도 똑같습니다. Target-typed new 는 컴파일 타임 문법 설탕(syntactic sugar) 이라서, 컴파일러가 타입을 추론한 뒤 일반 new T() 로 치환해 IL을 생성합니다. 따라서 런타임 비용·박싱·할당 패턴 모두 동일합니다.
IL(Intermediate Language, 중간 언어) — C# 코드를 컴파일하면 곧바로 기계어가 되는 게 아니라, .NET 런타임이 이해하는 중립 명령어(IL)로 변환됩니다. 실행 시점에 JIT(Just-In-Time, 실행 직전 컴파일)이 IL을 기계어로 바꿉니다.
newobj명령 — 객체 인스턴스를 힙에 할당하고 지정된 생성자를 호출하는 IL 명령어입니다. C#의new T(...)는 항상 이 한 줄로 변환됩니다.
3. [내부 동작] — 컴파일러가 대상 타입을 결정하는 순서
추론 알고리즘의 흐름
new() 표현식을 만난 컴파일러는 다음 순서로 처리합니다.
이 다섯 단계 중 어느 하나라도 실패하면 컴파일 에러가 발생합니다. 특히 ② 단계에서 후보가 없거나, ③ 단계에서 후보가 둘 이상이면(메서드 오버로드 모호성) 에러입니다.
기본 생성자 vs 인자 있는 생성자
Unity에서 이런 상황이 생긴다. 매개변수 없는 생성자를 가진 컬렉션과, 인자가 있는 도메인 객체를 함께 다룬다.
public class Order
{
public int Id { get; }
public Order(int id) { Id = id; }
}
public class Demo
{
public static void Run()
{
// 매개변수 없는 생성자 → new()
List<Order> orders = new();
// 인자 있는 생성자 → new(인자)
Order o = new(42);
// 컬렉션 초기화 안에서도 동일
List<Order> presetOrders = new()
{
new(1),
new(2),
new(3),
};
}
}
위처럼 동작하기 때문에 인자가 있는 케이스(new(42))는 Order의 (int) 시그니처 생성자를, 인자가 없는 케이스(new())는 List<Order>의 매개변수 없는 생성자를 호출합니다. 컬렉션 이니셜라이저 안에서는 요소 타입(Order)이 대상이 되어 new(1)이 new Order(1)로 해석됩니다.
어디서 타입을 가져오는가 — 사용 가능한 문맥
| 문맥 | 예시 | 추론되는 타입 |
|---|---|---|
| 변수 선언 | Player p = new(...) |
Player |
| 필드 초기화 | private List<Item> items = new(); |
List<Item> |
| 자동 프로퍼티 초기화 | public Inventory Inv { get; set; } = new(); |
Inventory |
| 메서드 인자 | Process(new(...)) (Process(Order o) 호출) |
Order |
| 반환문 | return new(0, 10, 0); (반환 타입 Vector3) |
Vector3 |
| 컬렉션 요소 | List<Order> = new() { new(1), new(2) }; |
Order |
| 캐스트 | (Vector3)new(0, 0, 0) |
Vector3 |
| 삼항 연산자 | Enemy e = isBoss ? new BossEnemy() : new GruntEnemy(); |
좌변 타입에 맞춰 |
default 대용은 불가 |
var x = new(); |
❌ — var와는 못 씀 |
특히 자동 프로퍼티 기본값 초기화는 var로는 절대 줄일 수 없는 영역이라서 new() 의 가치가 가장 큽니다.
public class GameSettings
{
// 프로퍼티는 var를 못 쓴다 → new()로 우변만 줄인다
public List<WaveData> Waves { get; private set; } = new();
public Dictionary<string, int> ItemCounts { get; private set; } = new();
}
4. [실전 적용] — Before/After 와 Unity 패턴
4-1. 긴 제네릭 타입 초기화
Unity에서 이런 상황이 생긴다. 스폰 테이블처럼 중첩 제네릭이 깊은 자료구조를 필드로 가진다.
❌ Before — 타입 중복
public class WaveManager : MonoBehaviour
{
private Dictionary<string, List<EnemySpawnInfo>> waveTable
= new Dictionary<string, List<EnemySpawnInfo>>();
private Queue<Func<float, IEnumerator>> pendingRoutines
= new Queue<Func<float, IEnumerator>>();
}
✅ After — new() 로 우변 축약
public class WaveManager : MonoBehaviour
{
private Dictionary<string, List<EnemySpawnInfo>> waveTable = new();
private Queue<Func<float, IEnumerator>> pendingRoutines = new();
}
위처럼 동작하기 때문에 좌변의 타입 정보가 우변으로 그대로 흘러갑니다. 이후 EnemySpawnInfo 의 이름이 바뀌면 좌변 한 군데만 수정하면 됩니다.
4-2. 메서드 인자에서 즉석 객체 만들기
Unity에서 이런 상황이 생긴다. API 호출 결과를 즉석으로 DTO에 담아 넘긴다.
❌ Before
public class EventBus
{
public void Publish(GameEvent evt) { /* ... */ }
}
public class Player : MonoBehaviour
{
void OnDeath()
{
eventBus.Publish(new GameEvent("PlayerDied", transform.position));
}
}
✅ After
public class Player : MonoBehaviour
{
void OnDeath()
{
// Publish의 매개변수 타입(GameEvent)에서 추론
eventBus.Publish(new("PlayerDied", transform.position));
}
}
이때 두 코드의 IL을 비교하면 newobj instance void GameEvent::.ctor(string, valuetype Vector3) 한 줄로 동일하게 컴파일됩니다. 호출 경로·할당량·GC 압박이 완전히 같습니다.
DTO(Data Transfer Object, 데이터 전송 객체) — 계층·시스템 사이에서 데이터만 실어 나르기 위한 단순 객체. 메서드나 동작 없이 필드/프로퍼티만 가진 경우가 많습니다.
4-3. 팩토리 메서드의 반환문
Unity에서 이런 상황이 생긴다. 기본 설정으로 새 인벤토리를 생성하는 팩토리.
❌ Before
public static Inventory CreateDefault()
{
return new Inventory(new List<Item>(), 100);
}
✅ After
public static Inventory CreateDefault()
{
// 반환 타입(Inventory)과 매개변수 타입(List<Item>) 양쪽에서 추론
return new(new(), 100);
}
주의. new(new(), 100) 처럼 중첩 시킬수록 가독성이 급격히 떨어집니다. 안쪽 new() 가 무엇을 의미하는지 한눈에 안 들어오면 이 단계에서는 안쪽만 풀어 쓰는 편이 좋습니다.
// 절충안 — 바깥은 줄이고 안은 풀어 쓴다
public static Inventory CreateDefault()
{
return new(new List<Item>(), 100);
}
Unity 핫패스(hot path) 적용성 —Target-typed new는 IL 레벨에서 차이가 없으므로 핫패스에서 사용해도 GC 스파이크나 추가 할당이 발생하지 않습니다. 다만new자체의 힙 할당은 그대로이므로, 매 프레임 호출되는Update()에서List<T>·Dictionary<TKey,TValue>·StringBuilder같은 참조 타입을 만드는 패턴은new()로 줄여 쓰더라도 여전히 GC 압박의 원인입니다. 핫패스에서는 풀링·재사용을 우선합니다.
4-4. 컬렉션 이니셜라이저 가독성 극적 개선
Unity에서 이런 상황이 생긴다. 디자이너가 준 표를 코드로 옮긴다.
❌ Before
List<EnemySpawnInfo> wave1 = new List<EnemySpawnInfo>
{
new EnemySpawnInfo(EnemyType.Grunt, 5, 0.5f),
new EnemySpawnInfo(EnemyType.Ranger, 3, 1.0f),
new EnemySpawnInfo(EnemyType.Brute, 1, 2.0f),
};
✅ After
List<EnemySpawnInfo> wave1 = new()
{
new(EnemyType.Grunt, 5, 0.5f),
new(EnemyType.Ranger, 3, 1.0f),
new(EnemyType.Brute, 1, 2.0f),
};
위처럼 동작하기 때문에 표의 데이터 구조(타입 6번 반복)가 사라지고 데이터 자체(매 행의 값)에 시선이 집중됩니다. 데이터 시트 변환 코드의 가독성이 가장 크게 개선되는 패턴입니다.
5. [함정과 주의사항]
5-1. var x = new(); — 가장 흔한 실수
❌ 잘못된 코드
var enemy = new(); // CS8754: 'new()'에는 대상 형식이 있어야 합니다.
// 컴파일조차 되지 않으므로 IL 없음 — CS8754 에러
✅ 올바른 코드
Enemy enemy = new(); // OK — 좌변이 타입을 정함
// 또는
var enemy = new Enemy(); // OK — 우변이 타입을 정함
왜 안 되는가. var 는 우변의 타입을 보고 좌변을 결정하고, new() 는 좌변의 타입을 보고 우변을 결정합니다. 둘을 같이 쓰면 어느 쪽도 시작점을 제공하지 않아 추론이 막힙니다.
5-2. 메서드 오버로드 모호성
Unity에서 이런 상황이 생긴다. 같은 이름의 오버로드 메서드를 호출할 때 인자 타입이 모호하다.
❌ 잘못된 코드
using System.IO;
class BadDemo
{
static void Process(Stream s) { }
static void Process(MemoryStream ms) { }
static void Caller()
{
Process(new()); // CS0121: 'Process(Stream)'과 'Process(MemoryStream)' 사이의 호출이 모호함
}
}
Stream 도 가능하고 MemoryStream 도 가능하므로 컴파일러가 후보를 하나로 좁히지 못합니다.
✅ 올바른 코드
static void Caller()
{
// 명시적으로 타입을 지정해 모호성 제거
Process(new MemoryStream());
// 또는 변수에 먼저 받아서 호출
Stream s = new MemoryStream();
Process(s);
}
5-3. 인터페이스·추상 클래스로는 추론 못 함
❌ 잘못된 코드
IList<int> list = new(); // CS8754: 'IList<int>'는 인스턴스화할 수 없는 인터페이스 타입
IDamageable target = new(); // 추상 클래스/인터페이스도 동일하게 실패
좌변이 인터페이스나 추상 클래스이면 어떤 구현체를 만들어야 할지 컴파일러가 알 수 없습니다.
✅ 올바른 코드
IList<int> list = new List<int>(); // 구현체를 명시
5-4. Unity 라이프사이클 객체에는 못 씀 — MonoBehaviour / ScriptableObject
❌ 잘못된 코드
public class GameManager : MonoBehaviour
{
void Start()
{
Player p = new(); // ❌ Player가 MonoBehaviour를 상속한다면 런타임에 정상 동작 안 함
}
}
MonoBehaviour 와 ScriptableObject 는 Unity 엔진이 라이프사이클(Awake → Start → ... → OnDestroy)을 관리합니다. new 자체로 만들면 안 되는 클래스입니다 — 이건 Target-typed new 의 한계라기보다 모든 new 가 부적절한 경우입니다.
✅ 올바른 코드
public class GameManager : MonoBehaviour
{
void Start()
{
// MonoBehaviour: GameObject에 컴포넌트 추가
Player p = gameObject.AddComponent<Player>();
// ScriptableObject: 전용 팩토리 메서드 사용
var settings = ScriptableObject.CreateInstance<GameSettings>();
}
}
순수 C# 클래스(POCO, MonoBehaviour 비상속) · 구조체 · Vector3·Color·WaitForSeconds 같은 일반 타입에서는 new() 가 자유롭게 동작합니다.
POCO(Plain Old CLR Object) — Unity 엔진의 라이프사이클이나 특수한 베이스 클래스에 의존하지 않는 순수 C# 객체. 데이터 클래스·매니저 클래스·DTO 등이 여기에 해당합니다.
5-5. 가독성 함정 — new() 가 멀리 떨어진 타입에서 추론될 때
Unity에서 이런 상황이 생긴다. 50줄짜리 메서드의 끝에서 return new(); 만 있는 코드.
❌ 가독성 떨어지는 코드
public PlayerStats LoadFromCache()
{
// ... 40줄의 처리 ...
if (cacheMiss)
{
return new(); // 반환 타입이 50줄 위에 있음 — 한눈에 안 보임
}
// ...
}
new() 만 보고 무엇이 만들어지는지 알려면 메서드 시그니처(PlayerStats LoadFromCache)까지 거슬러 올라가야 합니다.
✅ 명시적이 더 나은 코드
public PlayerStats LoadFromCache()
{
// ...
if (cacheMiss)
{
return new PlayerStats(); // 멀리 떨어진 위치에서는 타입 명시
}
}
경험칙. new() 가 만드는 타입이 같은 화면(15~20줄 이내)에 보이지 않으면 풀어 쓰는 편이 낫습니다.
5-6. 코드 리뷰 과정에서 "어떤 타입인지 모르겠음" 피드백을 받았을 때
코드 리뷰에서 동료가 new() 의 타입을 묻는다면 그 자리는 풀어 쓰는 게 답입니다. 언어가 허용한다고 해서 항상 써야 하는 건 아닙니다 — 문맥이 짧고 명확할 때만 쓰는 도구입니다.
6. [C# 버전별 변화]
C# 1.0 ~ 8.0 — 두 번 적기
객체 생성 시 좌변·우변 모두 타입을 명시해야 합니다.
// C# 8.0까지의 정석
List<Item> inventory = new List<Item>();
Dictionary<string, int> counts = new Dictionary<string, int>();
C# 3.0 의 var 는 지역 변수에서만 좌변을 줄일 수 있었고, 필드·프로퍼티·매개변수·반환 타입에서는 도움이 되지 않았습니다.
C# 9.0 — Target-typed new 도입
좌변·매개변수·반환 등 대상 타입이 명확하면 우변의 타입 이름을 생략할 수 있습니다.
List<Item> inventory = new();
Dictionary<string, int> counts = new();
// 메서드 인자, 반환, 컬렉션 이니셜라이저까지 적용 가능
Process(new("first"));
같은 시점에 도입된 record 타입과 결합하면 선언형 데이터 모델링이 가능해집니다.
public record Person(string Name, int Age);
Person[] people =
{
new("Alice", 30),
new("Bob", 25),
};
C# 10.0 — 구조체 매개변수 없는 생성자와의 조합
C# 10 부터 구조체에서 명시적 매개변수 없는 생성자를 정의할 수 있게 되었고, new() 가 이 사용자 정의 기본 생성자를 호출하도록 동작합니다.
public struct Point
{
public int X;
public int Y;
public Point() { X = 1; Y = 1; } // C# 10 — 명시적 기본 생성자
}
Point p = new(); // X=1, Y=1 (default(Point) 와 다름)
default와new()의 미묘한 차이 — 구조체에서default(T)는 모든 필드를 0으로 초기화한 인스턴스를 만들지만,new T()또는Target-typed new()는 사용자 정의 매개변수 없는 생성자가 있으면 그것을 호출합니다. C# 10 이전에는 차이가 없었지만 이후로는 결과가 달라질 수 있습니다.
C# 11 / 12 / 13 — 문법 자체는 그대로
C# 11 의 required 멤버, C# 12 의 Primary Constructor, C# 13 의 컬렉션 표현식([1, 2, 3])이 도입됐지만 Target-typed new 자체의 문법은 C# 9 이후 변화 없이 유지되고 있습니다. 컬렉션 표현식이 등장하면서 일부 컬렉션 초기화 패턴은 new() 보다 [..] 가 더 간결해졌습니다.
// C# 12 까지 — Target-typed new
List<int> nums = new() { 1, 2, 3 };
// C# 12 (preview) / 13 — 컬렉션 표현식
List<int> nums = [1, 2, 3];
7. [정리] — 핵심 요약 체크리스트
- [ ]
Target-typed new는 좌변·매개변수·반환 타입 등 "대상 타입"으로부터 우변의 타입을 추론합니다.new뒤의 타입 이름을 생략할 수 있게 합니다. - [ ]
var와는 추론 방향이 정반대입니다.var는 우변에서 좌변을,new()는 좌변에서 우변을 추론합니다. 둘을 동시에 쓰는var x = new();는 항상 컴파일 에러입니다. - [ ] 사용 가능한 문맥: 변수 선언, 필드·프로퍼티 초기화, 메서드 인자, 반환문, 컬렉션 이니셜라이저, 캐스트.
- [ ] 사용 불가능한 문맥:
var와 결합, 인터페이스·추상 클래스 좌변, 메서드 오버로드 모호성이 있는 호출. - [ ] IL 레벨에서 일반
new T()와 완전히 동일합니다. 컴파일러가 타입 추론 후 같은newobj명령을 생성하므로 런타임 비용·할당·GC 압박이 동일합니다. - [ ] Unity 에서는
MonoBehaviour·ScriptableObject에 사용 불가합니다. 이들은new자체를 쓰면 안 되며AddComponent<T>/ScriptableObject.CreateInstance<T>를 써야 합니다. - [ ] 가독성 경계: 타입이 같은 화면(~20줄)에 보일 때만 줄입니다. 멀리 떨어진 위치에서는 풀어 쓰는 편이 더 명확합니다. 중첩
new(new(), ...)도 피합니다. - [ ] 가장 큰 효용: 필드·프로퍼티에서 긴 제네릭 타입 초기화,
record배열·딕셔너리 초기화, 디자이너 표 데이터를 코드로 옮기는 컬렉션 이니셜라이저.
'C# 기초' 카테고리의 다른 글
| [PART7.클래스와 객체 입문(12/21)] struct 기초 — 값 타입을 직접 정의하기 (0) | 2026.05.01 |
|---|---|
| [PART7.클래스와 객체 입문(11/21)] Primary Constructor — 클래스 헤더에 매개변수를 적는다 (C# 12) (0) | 2026.05.01 |
| [PART7.클래스와 객체 입문(9/21)] 객체 초기화자 — 생성과 설정을 한 표현식으로 (0) | 2026.05.01 |
| [PART7.클래스와 객체 입문(8/21)] 정적(static) 멤버 — 인스턴스 없이 호출하는 코드와 데이터 (0) | 2026.05.01 |
| [PART7.클래스와 객체 입문(7/21)] 생성자와 this — 객체가 태어나는 순간 무슨 일이 벌어지는가 (0) | 2026.05.01 |