[PART8.상속과 인터페이스 사용법(11/11)] 업캐스트와 다운캐스트 — 안전한 타입 변환
자식→부모는 왜 무료이고 / 부모→자식은 왜 검사가 필요한가 / castclass vs isinst vs unbox.any의 IL 차이
목차
1. [문제 제기] 부모 타입으로 묶었더니 자식 기능을 못 쓴다
Unity에서 적 캐릭터를 한꺼번에 관리하려고 부모 클래스로 묶는 코드는 흔합니다.
public class Enemy : MonoBehaviour { public int Hp; }
public class Boss : Enemy { public void SpawnMinions() { } }
public class EnemyManager : MonoBehaviour
{
public List<Enemy> enemies = new();
void Start()
{
Boss boss = new Boss();
enemies.Add(boss); // ← 자식을 부모 리스트에 넣는다 (업캐스트)
}
void Update()
{
foreach (Enemy e in enemies)
{
// boss.SpawnMinions()를 호출하고 싶다 — 그런데 e는 Enemy 타입이라 못 부른다!
}
}
}
여기서 두 가지 의문이 생깁니다.
enemies.Add(boss)는 왜 캐스팅 키워드 없이 그냥 통과할까요?boss는 분명Boss타입인데Enemy리스트에 들어갑니다.- 반대로
Enemy참조를 다시Boss로 되돌리려면 왜(Boss)e같은 명시적 변환이 필요할까요? 게다가 잘못 쓰면 게임이 터집니다.
// ❌ 모든 적을 Boss로 다운캐스트 시도
foreach (Enemy e in enemies)
{
Boss b = (Boss)e; // 일반 Enemy면 InvalidCastException → 게임 강제 종료!
b.SpawnMinions();
}
자식 → 부모는 언제나 안전하지만, 부모 → 자식은 런타임 검사가 필요합니다. 이 차이를 모르면 디버그 빌드에서는 멀쩡하다가 라이브 서비스에서 크래시 리포트가 쌓이는 일이 벌어집니다.
이 글에서는 두 방향의 캐스팅이 IL 레벨에서 얼마나 다른지, 그리고 다운캐스트 세 가지 방법 — (T) · as · is T t — 을 어떻게 구분해 써야 하는지 짚어봅니다.
2. [개념 정의] 한 객체, 두 가지 시야
2.1. 비유 — 명함 한 장 vs 신분증 한 장
당신이 회사에서 "프론트엔드 개발자"이면서 동시에 회사의 일원인 "직원"입니다. 같은 사람인데 누가 보느냐에 따라 다른 이름표가 붙습니다.
- 업캐스트(자식 → 부모): 프론트엔드 개발자 명함 → 직원 명함. 아래로 내려놓는 거라 누구나 안전합니다. "직원입니다"라고 말해도 거짓말이 아니죠.
- 다운캐스트(부모 → 자식): 직원 명함 → 프론트엔드 개발자 명함. 이 직원이 정말 프론트엔드 개발자인지 확인해야 합니다. 백엔드 개발자에게 프론트엔드 명함을 주면 곤란해집니다.
핵심 비유:
객체의 메모리(실체)는 그대로입니다. 변하는 건 그 객체를 어떤 타입(시야)으로 바라보는가 뿐입니다.
2.2. 시각화 — 한 객체를 두 시야로 보다

2.3. 기본 코드 — 양방향 변환
public class Animal { public string Name = "동물"; }
public class Dog : Animal { public void Bark() { } }
// 1) 업캐스트 — 암시적, 캐스팅 키워드 없음
Dog d = new Dog();
Animal a = d; // Dog는 Animal이다 (IS-A)
// a.Bark(); // ❌ 컴파일 에러: Animal에는 Bark가 없음
Console.WriteLine(a.Name); // ✅ Animal의 멤버는 접근 가능
// 2) 다운캐스트 — 명시적 캐스팅 필요
Dog d2 = (Dog)a; // 런타임 검사 후 통과
d2.Bark(); // ✅ 이제 Bark 호출 가능
업캐스트 (Upcast) — 자식 클래스 참조를 부모 클래스 참조로 변환하는 것. C#에서는 암시적으로 처리되며 캐스팅 키워드가 필요 없다. 실패할 수 없다.
예시:Animal a = new Dog();Dog 인스턴스를 Animal 변수에 직접 대입 가능
다운캐스트 (Downcast) — 부모 클래스 참조를 자식 클래스 참조로 변환하는 것. 명시적 캐스팅이 필요하며 실패할 수 있어 런타임 검사가 동반된다.
예시:Dog d = (Dog)a;a가 실제로 Dog 인스턴스가 아니면InvalidCastException
업캐스트는 시야를 좁힙니다(Dog의 Bark가 안 보임). 다운캐스트는 시야를 넓히되(Dog의 Bark가 보임), 그 시야가 실제 객체와 맞는지 CLR이 확인합니다.
2.4. IL 분석 — 업캐스트는 IL에 흔적조차 없다
IL(Intermediate Language) — C# 컴파일러가 만들어내는 중간 언어. CPU 명령어가 아니라 .NET 런타임이 해석·실행하는 가상 명령어 집합이다. CIL(Common Intermediate Language)이라고도 부른다.
// 업캐스트 메서드
public static Animal TestUpcast()
{
Dog d = new Dog();
Animal a = d;
return a;
}
// 다운캐스트 메서드
public static Dog TestExplicitDowncast(Animal a)
{
return (Dog)a;
}
이 두 메서드를 컴파일한 IL입니다.
// TestUpcast — Dog를 만들어 Animal로 반환
IL_0000: newobj instance void Dog::.ctor() // 새 Dog 인스턴스 생성
IL_0005: ret // 그대로 반환 — 캐스팅 명령어 없음!
// TestExplicitDowncast — Animal을 받아 Dog로 캐스팅
IL_0000: ldarg.0 // 인자 a를 스택에 로드
IL_0001: castclass Dog // ← Dog 호환성 검사 (실패 시 예외)
IL_0006: ret // 검증된 참조 반환
핵심 발견:
- 업캐스트는 IL에 명령어가 없다. Dog 인스턴스의 메모리는 그대로이고, 어차피 같은 참조이므로 변환할 게 없습니다. 컴파일러는 단지 정적 타입을
Animal로 추적할 뿐입니다. - 다운캐스트는
castclass명령어 한 개가 추가됩니다. 이 한 개 명령어가 객체의 타입 포인터를 따라가 상속 트리를 탐색하고, 호환되지 않으면InvalidCastException을 던집니다.
업캐스트가 "공짜"인 이유, 다운캐스트가 "비용이 있는" 이유가 IL 레벨에서 그대로 드러납니다.
3. [내부 동작] 다운캐스트 세 가지 방법의 IL 비교
다운캐스트에는 세 가지 표현이 있고, IL이 모두 다릅니다.
3.1. 시각화 — 세 가지 다운캐스트의 분기 흐름

3.2. 코드와 IL 한꺼번에 비교
// 1) 명시적 캐스팅
public static Dog TestExplicitDowncast(Animal a) => (Dog)a;
// 2) as 연산자
public static Dog TestAsDowncast(Animal a) => a as Dog;
// 3) is 패턴
public static int TestIsPattern(Animal a)
{
if (a is Dog d) return 1;
return 0;
}
세 메서드의 IL을 나란히 보면 차이가 분명합니다.
// (Dog)a — castclass 사용
IL_0000: ldarg.0
IL_0001: castclass Dog // ← 호환되지 않으면 즉시 InvalidCastException
IL_0006: ret
// a as Dog — isinst 사용
IL_0000: ldarg.0
IL_0001: isinst Dog // ← 호환되면 참조, 아니면 null을 스택에 푸시
IL_0006: ret
// if (a is Dog d) — isinst + stloc + brfalse
IL_0000: ldarg.0
IL_0001: isinst Dog // ← 호환되면 참조, 아니면 null
IL_0006: brfalse.s IL_000a // null이면 if 블록 건너뛰기
IL_0008: ldc.i4.1 // d는 isinst 결과를 그대로 사용 (별도 캐스팅 없음)
IL_0009: ret
IL_000a: ldc.i4.0
IL_000b: ret
castclassvsisinst— 둘 다 객체의 실제 타입을 검사하는 명령어다. 차이는 실패 처리다.castclass는 즉시 예외를 던지고,isinst는 null을 스택에 남긴다.
핵심 발견:
as와is T t는 같은 명령어isinst를 사용한다. 차이는 그 결과를 어떻게 후처리하느냐 —as는 그대로 반환,is T t는 brfalse로 분기.(T)만castclass를 사용한다. 실패 = 예외 분기 = 비싼 길.is T t패턴은 캐스팅을 한 번만 한다.isinst결과를 그대로 변수d에 담아 사용하므로 추가 검사가 없습니다.
isinst는 "확인용", castclass는 "단정용"이라고 외우면 됩니다.
3.3. 값 타입 다운캐스트 — unbox.any
값 타입(int, struct 등)을 object로 담았다가 다시 꺼낼 때는 또 다른 IL 명령어를 씁니다.
public static object TestBoxing()
{
int x = 42;
object o = x; // 박싱 — 힙에 새 객체 생성
return o;
}
public static int TestUnboxing(object o)
{
return (int)o; // 언박싱 — 박스 안의 값 추출
}
IL을 보면:
// TestBoxing — int → object
IL_0000: ldc.i4.s 42
IL_0002: box [System.Runtime]System.Int32 // ← 힙에 박스 객체 할당, 42 복사
IL_0007: ret
// TestUnboxing — object → int
IL_0000: ldarg.0
IL_0001: unbox.any [System.Runtime]System.Int32 // ← 박스 검사 + 값 복사
IL_0006: ret
박싱 (Boxing) — 값 타입을 참조 타입(object또는 인터페이스)으로 변환하는 과정. 힙에 새 객체를 할당하고 값을 복사한다.
언박싱 (Unboxing) — 박싱된 객체에서 원래 값 타입을 꺼내는 과정. 명시적 다운캐스트 문법이 필요하며 ILunbox.any명령어로 처리된다.
값 타입 다운캐스트의 특징:
- 반드시 박싱된 정확한 타입으로만 언박싱 가능.
long으로 박싱된 것을int로 언박싱하면InvalidCastException. as연산자는 값 타입에 사용할 수 없습니다.int? x = o as int?처럼 Nullable 형태로만 가능합니다.- 박싱은 GC 할당을 발생시킵니다. Unity 핫패스에서는 절대 피해야 할 패턴입니다.
long l = 99;
object o = l;
// int i = (int)o; // ❌ InvalidCastException — long 박스를 int로 못 꺼냄
long l2 = (long)o; // ✅ 정확한 타입이면 OK
4. [실전 적용] Unity에서 다운캐스트는 언제 필요한가
4.1. Before/After — 적 군중에서 보스만 추리기
Before — 이중 캐스팅 안티패턴
public void DamageBosses(List<Enemy> enemies, int damage)
{
foreach (Enemy e in enemies)
{
if (e is Boss) // ← 1차 검사
{
Boss b = (Boss)e; // ← 2차 검사 (중복!)
b.TakeBossDamage(damage);
}
}
}
이 코드의 IL을 보면 검사가 두 번 일어납니다.
IL_0000: ldarg.0
IL_0001: isinst Dog // ← 1차: is 검사
IL_0006: brfalse.s IL_0011 // null이면 if 끝
IL_0008: ldarg.0
IL_0009: castclass Dog // ← 2차: 강제 캐스팅 (또 검사!)
IL_000e: pop // 결과 사용 안 하면 pop
IL_000f: ldc.i4.1
IL_0010: ret
IL_0011: ldc.i4.0
IL_0012: ret
isinst Dog로 한 번 확인한 뒤 castclass Dog로 또 한 번 검사합니다. 같은 객체에 대해 상속 트리를 두 번 탐색합니다. 1000마리의 적을 매 프레임 순회하는 핫패스에서는 이 중복 검사가 누적됩니다.
After — is T t 패턴 한 번에 처리
public void DamageBosses(List<Enemy> enemies, int damage)
{
foreach (Enemy e in enemies)
{
if (e is Boss b) // ← 검사 + 캐스팅 + 변수 선언 한 번
{
b.TakeBossDamage(damage);
}
}
}
IL은 isinst Dog 한 번으로 끝납니다.
IL_0011: ldloc.1
IL_0012: isinst Dog // 한 번만 검사
IL_0017: stloc.2 // 결과를 b에 저장
IL_0018: ldloc.2
IL_0019: brfalse.s IL_0023 // null이면 건너뛰기
IL_001b: ldloc.2 // b를 그대로 사용 (추가 캐스팅 없음)
IL_001c: callvirt instance void Dog::Bark()
같은 동작을 절반의 비용으로 처리합니다.
4.2. Before/After — 가상 메서드로 다운캐스트 자체를 없애기
더 좋은 답은 다운캐스트가 필요하지 않게 설계하는 것입니다.
Before — 타입별 분기
public class Game
{
public void HandleEnemies(List<Enemy> enemies)
{
foreach (Enemy e in enemies)
{
if (e is Boss b) b.SpawnMinions();
else if (e is Goblin g) g.Steal();
else if (e is Dragon d) d.BreatheFire();
// 새 적 추가할 때마다 여기에 분기 추가 → 확장성 X (OCP 위반)
}
}
}
새 적 클래스(Slime, Skeleton...)가 추가될 때마다 이 메서드를 수정해야 합니다. 개방-폐쇄 원칙(OCP, Open/Closed Principle — 확장에 열려 있고 수정에 닫혀 있어야 한다) 위반입니다.
After — 가상 메서드로 다형성 활용
public abstract class Enemy
{
public abstract void PerformSpecialAction(); // 각 자식이 알아서 구현
}
public class Boss : Enemy
{
public override void PerformSpecialAction() => SpawnMinions();
}
public class Goblin : Enemy
{
public override void PerformSpecialAction() => Steal();
}
public class Game
{
public void HandleEnemies(List<Enemy> enemies)
{
foreach (Enemy e in enemies)
{
e.PerformSpecialAction(); // 다운캐스트 없이 다형성
}
}
}
새 Slime 적이 생기면 Slime만 만들면 됩니다. HandleEnemies는 한 줄도 건드리지 않습니다.
요약: 다운캐스트 분기가 보이면 가상 메서드로 대체할 수 있는지 먼저 검토합니다. 다운캐스트는 진짜로 자식만의 고유 동작이 필요하거나, 외부 API(object로 반환되는 LINQ·리플렉션 결과 등)를 다룰 때만 합리화됩니다.
4.3. Unity의 GetComponent<T>() — 다운캐스트가 아니다
신입 개발자가 자주 헷갈리는 지점입니다.
Rigidbody rb = GetComponent<Rigidbody>();
이 코드는 다운캐스트가 아닙니다. Unity의 GetComponent<T>()는:
GameObject에 부착된 컴포넌트 배열을 순회T타입(또는 그 자식)과 일치하는 컴포넌트를 찾아 반환- 없으면
null반환
C# 다운캐스트는 "이 객체 참조의 정체를 다시 본다"는 행위지만, GetComponent는 "다른 객체를 찾아온다"는 검색입니다. 비용도 다릅니다.
| 동작 | 비용 |
|---|---|
(Dog)animal |
IL castclass 한 번 — CLR 메타데이터 한 번 조회 |
GetComponent<Rigidbody>() |
컴포넌트 배열 순회 — Unity 네이티브 코드 호출 |
핫패스에서 매번 GetComponent를 호출하면 안 되는 이유가 여기에 있습니다.
Before — 매 프레임 GetComponent 호출
public class PlayerController : MonoBehaviour
{
void FixedUpdate()
{
// ❌ 매 프레임 호출 — 비싼 검색을 60번/초 반복
GetComponent<Rigidbody>().AddForce(Vector3.forward * 10f);
}
}
After — Awake에서 캐싱
public class PlayerController : MonoBehaviour
{
private Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>(); // 시작 시 한 번
}
void FixedUpdate()
{
rb.AddForce(Vector3.forward * 10f); // ✅ 캐싱된 참조 사용
}
}
같은 원리로 다운캐스트 결과도 핫패스 진입 전에 캐싱합니다. 매 프레임 같은 객체를 다운캐스트하지 않습니다.
4.4. 공변성/반공변성 — 제네릭에서 캐스팅을 컴파일러에 맡기기
C#은 제네릭 인터페이스에서도 안전한 업캐스트/다운캐스트를 지원합니다. out(공변)과 in(반공변) 키워드를 통해서입니다.
공변성 (Covariance,out) — 제네릭 인터페이스의 타입 매개변수가 출력 위치(반환값)로만 쓰일 때 허용.IEnumerable<Dog>을IEnumerable<Animal>에 직접 대입할 수 있게 한다.
예시:IEnumerable<Dog> dogs = new List<Dog>(); IEnumerable<Animal> animals = dogs;
반공변성 (Contravariance,in) — 타입 매개변수가 입력 위치(매개변수)로만 쓰일 때 허용.Action<Animal>을Action<Dog>에 대입할 수 있게 한다.
예시:Action<Animal> feed = a => Feed(a); Action<Dog> feedDog = feed;
// 공변 — 출력만 가능 (안전한 업캐스트)
IEnumerable<Dog> dogs = new List<Dog> { new Dog() };
IEnumerable<Animal> animals = dogs; // ✅ Dog는 Animal이므로 OK
foreach (Animal a in animals) { /* Animal로 꺼냄 */ }
// 반공변 — 입력만 가능 (안전한 다운캐스트)
Action<Animal> feedAnimal = a => Console.WriteLine($"먹이를 줍니다: {a.Name}");
Action<Dog> feedDog = feedAnimal; // ✅ Dog도 Animal로 처리 가능
feedDog(new Dog()); // 실행 OK
Unity에서의 활용: IReadOnlyList<T>, IEnumerable<T>, Func<...>, Action<...> 같은 제네릭 타입의 매개변수에 자식 타입 컬렉션이나 부모 타입 콜백을 자유롭게 넘길 수 있습니다. 명시적 캐스팅 코드 없이 컴파일 타임에 안전성이 보장됩니다.
public void FeedAll(IEnumerable<Animal> animals)
{
foreach (var a in animals) Feed(a);
}
// 사용처: List<Dog>를 그대로 넘긴다
List<Dog> myDogs = new List<Dog> { new Dog(), new Dog() };
FeedAll(myDogs); // ✅ IEnumerable<Dog>가 IEnumerable<Animal>로 공변 변환
out/in은 깊이 들어가면 별도 주제지만, "제네릭 컬렉션에 자식 리스트를 부모 매개변수에 넘길 수 있는 이유"가 바로 이것이라는 점만 기억하면 됩니다.
5. [함정과 주의사항]
5.1. ❌ try-catch로 캐스팅 실패를 잡지 마라
❌ 안티패턴
public Dog ConvertToDog(object obj)
{
try
{
return (Dog)obj; // 실패하면 castclass가 예외 던짐
}
catch (InvalidCastException)
{
return null;
}
}
이 코드의 IL은 try { castclass Dog; ... } catch { ldnull; ... } 구조로 컴파일됩니다. 예외 발생 시 CLR이 스택 트레이스를 만들고 catch 블록을 찾는 비용이 매우 큽니다. Unity 핫패스에서 1000번/초 호출되면 GC 스파이크와 프레임 드롭이 따라옵니다.
✅ 올바른 코드
public Dog ConvertToDog(object obj)
{
return obj as Dog; // 실패하면 null — IL isinst 한 줄
}
as는 IL isinst 한 줄로 끝나며 예외 비용이 없습니다.
5.2. ❌ as 결과의 null 체크를 빠뜨리지 마라
❌ 위험한 코드
void OnHit(object collider)
{
Enemy e = collider as Enemy;
e.TakeDamage(10); // collider가 Enemy가 아니면 NullReferenceException!
}
as는 실패해도 예외를 던지지 않기 때문에, null 체크를 빼먹으면 다음 줄에서 참조 예외가 터집니다. 위치가 한 줄 미루어져 디버깅이 까다로워집니다.
✅ 올바른 코드 — is T t 패턴
void OnHit(object collider)
{
if (collider is Enemy e) // 검사 자체가 분기 조건
{
e.TakeDamage(10); // 여기서 e는 절대 null이 아님
}
}
타입 패턴은 검사가 분기 조건이므로 null 체크를 빼먹을 수 없습니다. C# 7 이상이라면 as + null 체크보다 is T t 패턴을 우선합니다.
5.3. ❌ 핫패스에서 박싱·언박싱을 반복하지 마라
❌ Unity에서 자주 보이는 안티패턴
public class ScoreManager
{
private List<object> scores = new(); // ← object 컬렉션!
public void AddScore(int score)
{
scores.Add(score); // ❌ 박싱 — 힙 할당
}
public int GetTotal()
{
int total = 0;
foreach (object s in scores)
{
total += (int)s; // ❌ 언박싱 — 매번 unbox.any
}
return total;
}
}
이 코드는 점수 추가마다 힙 할당이 발생하고, 합산할 때마다 unbox.any 명령어로 검사를 반복합니다. Unity의 GC는 이런 미세한 할당이 누적되면 GC 스파이크를 일으킵니다.
✅ 제네릭으로 박싱 회피
public class ScoreManager
{
private List<int> scores = new(); // ← 값 타입 그대로
public void AddScore(int score)
{
scores.Add(score); // ✅ 박싱 없음
}
public int GetTotal()
{
int total = 0;
foreach (int s in scores) // ✅ 언박싱 없음
{
total += s;
}
return total;
}
}
List<int>는 내부적으로 int[] 배열을 사용해 값 타입을 직접 저장합니다. 박싱·언박싱이 모두 사라지고, IL에는 unbox.any가 나타나지 않습니다.
5.4. ❌ Unity 객체에는 as보다 TryGetComponent를 우선하라
Unity의 UnityEngine.Object는 C# 객체 수명과 네이티브 객체 수명이 다릅니다. as로 받은 참조가 null은 아니지만 네이티브 측은 이미 파괴된 "좀비 객체"일 수 있습니다.
❌ 위험한 코드
void OnTriggerEnter(Collider other)
{
Enemy enemy = other.GetComponent<Enemy>() as Enemy;
enemy.TakeDamage(10); // 좀비 객체일 수 있음 — MissingReferenceException 가능
}
✅ 권장 코드
void OnTriggerEnter(Collider other)
{
if (other.TryGetComponent(out Enemy enemy)) // out 변수로 한 번에
{
enemy.TakeDamage(10); // 좀비 검사 통과한 참조
}
}
TryGetComponent는 Unity 2019.2부터 도입된 API로 GetComponent 두 번 호출과 좀비 검사를 한 번에 처리합니다.
6. [C# 버전별 변화]
다운캐스트의 도구는 C# 버전마다 더 안전하고 간결해졌습니다.
6.1. C# 1~6 — 분리된 검사와 캐스팅
// C# 6 이전
if (obj is Enemy) // 1차 검사 (bool 반환만)
{
Enemy e = (Enemy)obj; // 2차 캐스팅 (또 검사)
e.Hp = 100;
}
// 또는
Enemy e = obj as Enemy;
if (e != null) { e.Hp = 100; }
이 시기엔 is가 단순 bool 반환이고, 변수 선언을 못 했습니다. 두 단계가 강제됐습니다.
6.2. C# 7 (2017) — 타입 패턴
// C# 7
if (obj is Enemy e) // 검사 + 캐스팅 + 선언 한 번
{
e.Hp = 100;
}
// switch 문도 패턴 매칭 지원
switch (obj)
{
case Boss b: b.SpawnMinions(); break;
case Enemy e: e.TakeDamage(10); break;
}
이중 캐스팅이 사라지고 IL이 isinst 한 번으로 줄었습니다.
6.3. C# 8 (2019) — switch 표현식
// C# 8
string desc = obj switch
{
Boss b => $"보스 (HP: {b.Hp})",
Enemy e => $"적 (HP: {e.Hp})",
null => "없음",
_ => "알 수 없음"
};
타입 패턴이 표현식으로 격상돼 변수 대입에 직접 쓸 수 있게 됐습니다.
6.4. C# 9 (2020) — 속성 패턴, 논리 패턴
// C# 9
if (obj is not null) { /* ... */ } // 논리 패턴
if (age is > 0 and < 130) { /* ... */ } // 관계 + 논리
if (obj is Enemy { Hp: > 1000, IsAlive: true } boss) // 속성 패턴 + 변수 선언
{
boss.Berserk();
}
타입 + 속성 + 변수 선언을 한 줄에 표현 가능합니다. IL에는 여전히 isinst 한 번 + 필드 비교 명령어로 인라인 분기 처리됩니다.
6.5. C# 10~11 (2021~2022) — 확장 속성 패턴, 리스트 패턴
// C# 10 — 확장 속성 패턴 (점 표기)
if (player is { Inventory.Gold: > 100 } richPlayer) { /* ... */ }
// C# 11 — 리스트 패턴
int[] arr = { 1, 2, 3 };
if (arr is [1, 2, 3]) { /* ... */ }
if (arr is [1, .., 3]) { /* 첫 1, 마지막 3 */ }
핵심 흐름: C# 7 이후 다운캐스트의 모든 길은 is T t 또는 switch 패턴으로 통합되었습니다. 이중 캐스팅 안티패턴은 C# 6 이전 코드에서나 보입니다. 새 코드를 짤 때 (T)는 "실패가 곧 버그"인 상황에만 쓰고, 나머지는 모두 is T t 패턴으로 대체합니다.
7. [정리]
핵심 체크리스트:
- [ ] 업캐스트는 IL에 명령어가 없다 —
Animal a = new Dog();은 단지 정적 타입만 변경, 메모리·참조·CLR 검사 없음 - [ ] 다운캐스트는 런타임 검사 필요 — 부모 참조가 실제로 자식 객체인지 CLR이 확인해야 안전
- [ ]
(T)는castclass— 실패 시InvalidCastException, "이건 반드시 T다"라는 단정용 - [ ]
as는isinst— 실패 시 null, 참조 타입과 Nullable에만 사용 - [ ]
is T t는isinst+ stloc — 검사+캐스팅+변수 선언을 한 번에, C# 7 이후 기본 도구 - [ ] 이중 캐스팅 안티패턴 금지 —
if (a is T) { (T)a; }는isinst+castclass두 번,is T t로 대체 - [ ] try-catch + (T) 안티패턴 금지 — 예외 비용은 매우 비쌈,
as나is T t로 대체 - [ ] 값 타입은
unbox.any—(int)obj는 박싱된 정확한 타입으로만 가능, Nullable이 아니면as불가 - [ ] 다운캐스트 분기가 보이면 가상 메서드/인터페이스로 대체 검토 — OCP 위반은 코드 스멜
- [ ] Unity
GetComponent<T>()는 다운캐스트가 아니라 검색 — 비용이 더 크므로 Awake에서 캐싱 - [ ]
TryGetComponent(out T)로 좀비 객체 안전성 확보 — Unity 객체에는as보다 우선 - [ ] 공변(
out)/반공변(in)으로 제네릭 캐스팅을 컴파일러에 맡기기 —IEnumerable<Dog>→IEnumerable<Animal>자동 - [ ] 핫패스에서 박싱·언박싱 반복 금지 —
List<object>보다List<int>같은 제네릭으로 박싱 회피
업캐스트는 컴파일러의 정적 타입 추적일 뿐이고, 다운캐스트는 CLR이 객체 헤더를 따라가는 런타임 행위입니다. IL 한 줄(castclass 또는 isinst)의 차이가 게임의 안정성과 성능을 가릅니다. 결론: 다운캐스트는 의심스러우면 is T t, 확신할 때만 (T)를 쓴다. 그리고 다운캐스트가 보이면 먼저 가상 메서드로 대체할 수 있는지부터 묻는다.
'C# 기초' 카테고리의 다른 글
| [PART9.컬렉션 기본 사용법(2/8)] Dictionary<TKey, TValue> — 키로 즉시 찾는 컬렉션 (0) | 2026.05.04 |
|---|---|
| [PART9.컬렉션 기본 사용법(1/8)] List<T> — 가장 자주 쓰는 컬렉션 (0) | 2026.05.04 |
| [PART8.상속과 인터페이스 사용법(10/11)] is · as · 타입 패턴 — 안전한 타입 검사 세 가지 방법 (0) | 2026.05.03 |
| [PART8.상속과 인터페이스 사용법(9/11)] 명시적 인터페이스 구현 — `IFoo.Method()` (1) | 2026.05.03 |
| [PART8.상속과 인터페이스 사용법(8/11)] 정적 추상 인터페이스 멤버 — 타입 자체를 다형성에 끌어들이다 (0) | 2026.05.03 |
