no image
C# - List vs Array(리스트 vs 배열)
✅ 리스트(List) vs 배열(Array) 핵심 차이 요약항목배열 (int[])리스트 (List)크기고정 (정적)가변 (동적)타입기본 자료형 (int[])제네릭 클래스 (List)기능제한적 (Add 불가)다양한 메서드 제공 (Add, Remove 등)성능빠름 (오버헤드 적음)약간의 오버헤드 있음 (성능 ↓)저장 위치힙 (※참조 타입)힙 (내부 배열도 힙) 📌 중요! 배열도 참조 타입이기 때문에 스택이 아닌 힙에 저장됨!❗ 오해 주의: "배열은 스택에 저장된다?" → ❌✳️ C#에서 배열은 참조 타입이야int[] arr = new int[3]; // 힙에 배열 생성arr라는 참조 변수는 스택에 존재하지만,실제 int[3] 배열 데이터는 힙에 저장됨📌 이건 List도 마찬가지야. 내부에 int[]를 가..
2025.07.04
C#
no image
C# - Collection(컬렉션)
✅ 컬렉션이란?"여러 데이터를 하나로 묶어 저장할 수 있는 자료구조 클래스"→ C#에서는 System.Collections, System.Collections.Generic 네임스페이스 아래에서 제공돼.✅ 컬렉션의 종류 (중요한 것들만 정리)분류컬렉션 이름특징리스트List가변 크기 배열, 가장 많이 사용배열Array고정 크기, 가장 기본적집합HashSet중복 불가, 빠른 검색딕셔너리DictionaryKey-Value 형태 저장큐QueueFIFO(선입선출)스택StackLIFO(후입선출)✅ 주요 컬렉션 설명1. ListList numbers = new List();numbers.Add(1);numbers.Add(2);Console.WriteLine(numbers[0]); // 1가변 크기Index 접근 가능..
2025.07.04
C#
no image
C# - Object Type(오브젝트 타입) & Object Memori(오브젝트 메모리 설명)
✅ object 타입이란?C#에서 object는 모든 타입의 최상위(Base) 타입이야.다시 말해, C#의 모든 타입은 object를 상속받고 있어.int, string, float, bool, DateTime, 사용자 정의 클래스 등 전부 다 object형으로 취급될 수 있어.object obj = 123; // int도 object로 받을 수 있고object str = "hello"; // string도 object로 받을 수 있어object list = new List(); // List도 마찬가지✅ 왜 필요할까?object 타입은 다형성과 범용성을 위해 사용돼.예를 들어, 어떤 메서드에 다양한 타입의 값을 전달해야 할 때 object로 받으면 어떤 타입이든 다 받을 수 있어.void..
2025.07.04
C#
no image
C# - Parent obj = new Child(); 메모리는 어떻게 될까? & 다형성
Parent obj = new Child(); 메모리는 어떻게 될까?🤔 질문클래스 상속을 사용할 때,Parent obj = new Child();이렇게 부모 타입으로 자식 객체를 참조하면,메모리는 부모 클래스만큼만 잡히는 걸까? 아니면 자식 클래스 전체가 메모리에 올라갈까?✅ 결론 먼저!👉 메모리에는 자식 클래스 전체가 생성된다.즉, **new Child()**를 하면자식 클래스가 부모를 포함한 상태로 메모리에 만들어지고,참조 변수(obj)가 그 전체 객체를 가리키게 된다.🧠 왜 그렇게 되는 걸까?1. new Child()는 "Child 인스턴스를 생성"하는 코드다여기서 생성되는 객체는 무조건 Child 전체 클래스Child는 내부적으로 Parent의 멤버도 다 포함하고 있음(상속이란 게 그런 것!..
2025.07.03
C#
no image
C# - base 키워드
✅ base 키워드란?자식 클래스에서 부모 클래스의 생성자나 메서드, 필드에 접근할 때 사용하는 키워드📌 1. 부모 클래스의 메서드 호출할 때class Parent{ public void Speak() { Console.WriteLine("부모가 말한다"); }}class Child : Parent{ public void Say() { base.Speak(); // 부모의 Speak() 호출 }}📌 2. 부모 생성자 호출class Parent{ public Parent(string name) { Console.WriteLine($"{name} 생성됨"); }}class Child : Parent{ public ..
2025.07.03
C#
no image
DesignPattern - Template Method Pattern(템플릿 메서드 패턴) vs virtual
✅ 템플릿 메서드 패턴이란?**템플릿 메서드 패턴(Template Method Pattern)**은상속을 기반으로 부모 클래스에서 **알고리즘의 전체 흐름(틀)**을 정의하고,일부 단계는 abstract 또는 virtual로 정의하여 자식 클래스가 자유롭게 재정의할 수 있도록 하는 패턴입니다. 참고 영상https://www.youtube.com/watch?v=TfwHAT1H04I 🧱 핵심 구성요소템플릿 클래스PlayerBase공통 기능 (Idle(), Move())은 virtual, 꼭 구현해야 하는 핵심 기능은 abstract로 정의구체 클래스Warrior, MagicianPlayerBase를 상속받아 필요한 메서드들을 override클라이언트TestPlayerBase 타입으로 실제 자식 인스턴스를 ..
2025.07.03
no image
C# - Class Inheritance(클래스 상속)& Interface(인터페이스) vs abstract Class
✅ 클래스 상속 (Inheritance)✔️ 정의class A : B처럼, 한 클래스가 다른 클래스의 기능을 물려받는 것✔️ 목적코드 재사용: 부모 클래스의 멤버(필드, 메서드 등)를 자식이 그대로 사용 가능기능 확장: 자식 클래스는 새로운 기능을 추가하거나, 부모 기능을 재정의 가능다형성(polymorphism): 부모 타입으로 자식 객체를 다룰 수 있음✔️ 예제class Character{ public virtual void Attack() { Console.WriteLine("기본 공격!"); }}class Warrior : Character{ public override void Attack() { Console.WriteLine("칼 휘두르기!..
2025.07.03
C#
no image
C# - Virtual & abstract
✅ virtual 키워드 정리📌 정의virtual은 부모 클래스에서 메서드나 속성을 "재정의 가능"하게 만드는 키워드입니다.📋 기본 문법class Parent{ public virtual void SayHello() { Console.WriteLine("안녕하세요. 부모입니다."); }}class Child : Parent{ public override void SayHello() { Console.WriteLine("안녕하세요. 자식입니다."); }}✅ 주요 특징기본 구현 제공부모 클래스가 기본 로직을 정의자식 클래스에서 변경 가능override 키워드로 오버라이딩 가능선택적 재정의자식 클래스에서 반드시 재정의하지 않아도 됨다형성 지원부모 타..
2025.07.03
C#
반응형

✅ 리스트(List) vs 배열(Array) 핵심 차이 요약

항목 배열 (int[]) 리스트 (List<int>)
크기 고정 (정적) 가변 (동적)
타입 기본 자료형 (int[]) 제네릭 클래스 (List<T>)
기능 제한적 (Add 불가) 다양한 메서드 제공 (Add, Remove 등)
성능 빠름 (오버헤드 적음) 약간의 오버헤드 있음 (성능 ↓)
저장 위치 힙 (※참조 타입) 힙 (내부 배열도 힙)
 

📌 중요! 배열도 참조 타입이기 때문에 스택이 아닌 힙에 저장됨!


❗ 오해 주의: "배열은 스택에 저장된다?" → ❌

✳️ C#에서 배열은 참조 타입이야

int[] arr = new int[3]; // 힙에 배열 생성
  • arr라는 참조 변수는 스택에 존재하지만,
  • 실제 int[3] 배열 데이터는 힙에 저장됨

📌 이건 List<int>도 마찬가지야. 내부에 int[]를 가지고 있고, 이 int[]도 힙에 있어.


✅ 메모리 구조 비교

1️⃣ int[] arr = new int[3];

stack:
 └─ arr (→ 힙 주소)

heap:
 └─ [0, 0, 0]  ← 실제 배열

2️⃣ List<int> list = new List<int>();

stack:
 └─ list (→ 힙 주소)

heap:
 └─ List 객체
     └─ 내부에 int[] 배열 (다시 힙에 있음)

즉, 둘 다 힙에 저장되며, List는 배열을 내부에 포함한 추가적인 클래스 구조야.


✅ 기능 차이 정리

배열

int[] arr = new int[3];
arr[0] = 1;
// arr.Add(1); ❌ 불가능

리스트

List<int> list = new List<int>();
list.Add(1); // ✅ 동적으로 크기 증가
list.Remove(1); // ✅ 삭제도 가능

✅ 성능 차이

  • 배열은 기능이 단순해서 메모리 할당, 접근 속도 모두 빠름
  • 리스트는 내부적으로 배열을 사용하지만, Add 시 크기 초과하면 배열을 재할당해야 하므로 오버헤드 발생

예: Add() → 배열 크기 부족 → 더 큰 배열 만들어 복사 → 기존 배열은 GC 대상


✅ 결론

항목 정리
저장 위치 배열도 리스트도 실제 데이터는 힙에 저장됨
크기 배열은 고정, 리스트는 가변
기능 배열은 단순, 리스트는 편리한 기능 많음
메모리 구조 리스트는 배열을 감싸는 클래스 형태의 구조
성능 배열이 더 빠르지만 유연성은 리스트가 더 좋음

흠..기본적인 간단한 구조라면 배열을 사용하고 그렇지않고 가변적으로 계속 사용해야 할때는 리스트가 확실히 나을듯 하네요!

그리고 개인적으로 든 생각은 정적인것은 스택, 동적인건 힙인줄 알고있었고

그래서 배열은 정적이니까 스택에 들어가고 리스트가 힙에 들어갈 줄 알았는데 둘다 힙에 들어간다니 새로운 사실을 발견했네여!!

배열도 참조 타입이기때문에 그렇다고 하니 이해가 됩니다.

반응형

C# - Collection(컬렉션)

Dev_Jen
|2025. 7. 4. 10:17
반응형

✅ 컬렉션이란?

"여러 데이터를 하나로 묶어 저장할 수 있는 자료구조 클래스"
→ C#에서는 System.Collections, System.Collections.Generic 네임스페이스 아래에서 제공돼.


✅ 컬렉션의 종류 (중요한 것들만 정리)

분류 컬렉션 이름 특징
리스트 List<T> 가변 크기 배열, 가장 많이 사용
배열 Array 고정 크기, 가장 기본적
집합 HashSet<T> 중복 불가, 빠른 검색
딕셔너리 Dictionary<TKey, TValue> Key-Value 형태 저장
Queue<T> FIFO(선입선출)
스택 Stack<T> LIFO(후입선출)

✅ 주요 컬렉션 설명

1. List<T>

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
Console.WriteLine(numbers[0]); // 1
  • 가변 크기
  • Index 접근 가능
  • 반복문에 자주 쓰임

2. Dictionary<TKey, TValue>

Dictionary<string, int> score = new Dictionary<string, int>();
score["철수"] = 95;
score["영희"] = 88;
Console.WriteLine(score["철수"]); // 95
  • Key를 이용해 값(Value) 저장
  • Key는 중복 불가
  • 빠른 검색/삽입

3. HashSet<T>

HashSet<int> set = new HashSet<int>();
set.Add(1);
set.Add(1); // 중복 안 됨
Console.WriteLine(set.Count); // 1
  • 중복 허용 안 함
  • 리스트보다 검색이 빠름

4. Queue<T> (FIFO)

Queue<string> q = new Queue<string>();
q.Enqueue("첫 번째");
q.Enqueue("두 번째");
Console.WriteLine(q.Dequeue()); // 첫 번째
  • 줄 서기처럼 먼저 넣은 게 먼저 나옴

5. Stack<T> (LIFO)

Stack<string> s = new Stack<string>();
s.Push("첫 번째");
s.Push("두 번째");
Console.WriteLine(s.Pop()); // 두 번째
  • 쌓는 방식. 나중에 넣은 게 먼저 나옴

✅ 컬렉션 공통 기능

  • Count : 요소 수
  • foreach : 반복
  • Contains() : 포함 여부
  • Clear() : 초기화
  • Remove() : 삭제

✅ 제네릭 vs 비제네릭 컬렉션

종류 설명
List<T>, Dictionary<TKey, TValue> 등 제네릭 컬렉션, 타입 안정성, 성능 우수
ArrayList, Hashtable 등 비제네릭 컬렉션, 구버전, object 기반이라 박싱/언박싱 발생

📌 지금은 거의 다 제네릭을 씀!


✅ 요약

컬렉션 특징
List<T> 배열처럼 사용, 동적 크기
Dictionary<K,V> Key-Value 쌍 저장
HashSet<T> 중복 제거
Queue<T> 선입선출
Stack<T> 후입선출

그럼 이제 궁금한게 배열, 리스트의 차이 그리고 큐와 스택의 차이가 궁금해졌습니다.

물론 큐와 스택은 FIFO, LIFO의 차이점을 잘 알고 있지만 한번 정리해보려구요! 

배열, 리스트 / 큐, 스택 밑에 따로 정리해보겠습니다!

https://dev-jen.tistory.com/entry/C-List-vs-Array%EB%A6%AC%EC%8A%A4%ED%8A%B8-vs-%EB%B0%B0%EC%97%B4

 

C# - List vs Array(리스트 vs 배열)

✅ 리스트(List) vs 배열(Array) 핵심 차이 요약항목배열 (int[])리스트 (List)크기고정 (정적)가변 (동적)타입기본 자료형 (int[])제네릭 클래스 (List)기능제한적 (Add 불가)다양한 메서드 제공 (Add, Remove 등

dev-jen.tistory.com

https://dev-jen.tistory.com/entry/C-Queue%ED%81%90-vs-Stack%EC%8A%A4%ED%83%9D-%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B5%AC%EC%A1%B0

 

C# - Queue(큐) vs Stack(스택) + 메모리구조

✅ 핵심 차이 한 줄 요약자료구조개념큐(Queue)FIFO: 먼저 들어간 게 먼저 나온다 (First-In First-Out)스택(Stack)LIFO: 나중에 들어간 게 먼저 나온다 (Last-In First-Out)✅ 실생활 비유자료구조비유 예시Queue줄

dev-jen.tistory.com

 

반응형
반응형

✅ object 타입이란?

C#에서 object는 모든 타입의 최상위(Base) 타입이야.
다시 말해, C#의 모든 타입은 object를 상속받고 있어.
int, string, float, bool, DateTime, 사용자 정의 클래스 등 전부 다 object형으로 취급될 수 있어.

object obj = 123;       // int도 object로 받을 수 있고
object str = "hello";   // string도 object로 받을 수 있어
object list = new List<int>(); // List<int>도 마찬가지

✅ 왜 필요할까?

object 타입은 다형성과 범용성을 위해 사용돼.

예를 들어, 어떤 메서드에 다양한 타입의 값을 전달해야 할 때 object로 받으면 어떤 타입이든 다 받을 수 있어.

void PrintObject(object obj) {
    Console.WriteLine(obj.ToString());
}

위 메서드는 어떤 타입이든 들어와도 받아줄 수 있어.


✅ 박싱(Boxing)과 언박싱(Unboxing)

object는 값 타입(int, float, bool 등)을 참조 타입인 object로 담을 때 박싱(Boxing) 이 발생하고,
다시 원래의 값 타입으로 꺼낼 때는 언박싱(Unboxing) 이 발생해.

int num = 10;
object obj = num; // 박싱 (값 타입 → object)

int result = (int)obj; // 언박싱 (object → 원래 값 타입)

📌 박싱과 언박싱은 성능에 영향을 줄 수 있기 때문에 주의해서 사용해야 해.


✅ 형변환 주의사항

object 타입으로 캐스팅된 값은 원래 타입을 정확히 알아야 다시 사용할 수 있어.

object obj = "Hello";

// 안전한 캐스팅
if (obj is string str) {
    Console.WriteLine(str.ToUpper());
}

// 또는 as 키워드
string? maybeStr = obj as string;
if (maybeStr != null) {
    Console.WriteLine(maybeStr.ToUpper());
}

✅ 실전 예시

void HandleAnything(object input) {
    if (input is int i) {
        Console.WriteLine($"정수: {i}");
    } else if (input is string s) {
        Console.WriteLine($"문자열: {s}");
    } else {
        Console.WriteLine("기타 타입");
    }
}

✅ 요약 정리

항목 타입
object 모든 타입의 최상위 타입 (C#의 루트)
사용 목적 범용 처리, 다형성 구현
박싱 값 타입 → 참조 타입으로 변환
언박싱 참조 타입 → 값 타입으로 꺼냄
주의점 박싱/언박싱은 성능 저하를 유발할 수 있음

오브젝트 타입은 모든 타입의 최상위 타입이고 타입중의 최상위 클래스네여 별로 어려운 부분은 없는 듯 합니다.

근데 이제 궁금한건 메모리에 관해서 어떻게 작동하는지가 궁금하네여 박싱/언박싱이 이루어진다면

주소값을 스택에 저장하고 힙에 데이터를 저장할텐데 말이죠! 한번 물어보겠습니다!

✅ C# 메모리 구조 간단 정리

C#은 주로 두 가지 메모리 영역을 사용해:

  • 스택(Stack): 값 타입 저장 (작고 빠름)
  • 힙(Heap): 참조 타입 저장 (크고 유연함, 가비지 컬렉션 대상)

✅ 값 타입과 참조 타입의 메모리 동작 차이

구분 값 타입 (int, float, struct) 참조 타입 (class, string, object)
저장 위치 스택 힙 (스택엔 참조 주소)
저장 내용 실제 값 힙 주소 (레퍼런스)
복사 시 값이 복사됨 참조(주소)가 복사됨

✅ object와 메모리 – 박싱(Boxing)

💡 예시:

int x = 10;
object obj = x; // 박싱

📌 박싱 시 메모리 흐름

  1. x는 스택에 저장돼 (int는 값 타입)
  2. obj = x 하면,
    • 힙에 새로운 int 값을 저장할 공간을 만들고
    • x 값을 거기에 복사해
    • obj는 그 힙 공간을 가리키는 참조를 가지게 돼

즉, 값 타입이 힙 객체로 변환되면서 메모리 복사 + 힙 할당이 발생하는 거야.


✅ 언박싱(Unboxing) – 다시 스택으로

int y = (int)obj; // 언박싱

📌 언박싱 시

  1. obj가 가리키고 있는 힙의 값을 다시 스택으로 복사
  2. 원래 타입(int)으로 변환해줌

이 과정 역시 비용이 발생해.


✅ 박싱/언박싱 메모리 시각화

int x = 10;          // 스택: [x = 10]
object obj = x;      // 힙: [10], 스택: [obj -> 힙주소]
int y = (int)obj;    // 스택: [y = 10]

 


✅ 참고: object에 참조 타입을 넣는 경우

string s = "hello";
object obj = s;

이건 박싱이 발생하지 않음.
왜냐면 string은 이미 참조 타입이기 때문에, 단순한 참조 복사만 일어나.
📌 박싱은 오직 "값 타입 → object"로 바뀔 때만 발생함.


✅ 요약

object는 참조 타입 값 타입을 넣으면 힙에 복사되고 주소가 저장됨
박싱 스택 값 → 힙에 새로 저장 + 주소 저장
언박싱 힙 값 → 스택에 복사 + 형변환
성능 주의 박싱/언박싱은 힙 할당 + 복사로 성능 저하 가능
참조 타입은? 박싱 안됨. 그대로 주소 복사

글로만 보니까 조금 이해하기 힘들수도있을듯해서 친구 블로그에서 그림을 가져왔습니다ㅎㅎ

그림으로 보면 더 이해하기 쉬울듯 해서요!

 

확실히 그냥 int a = 5 일때는 스택에 값을 저장하지만

object a = 5는 스택에 a가 주속값을 가지고 있고 힙에 값을 저장합니다.

그리고 int b = (int)a를 했을때 b는 힙에있는 5의 값을 저장하고 힙에있는 데이터 값을 저장하게되어

힙에 남아있는 5는 가비지가 되어 C#의 GC가 실행되어 없어질듯합니다.

✅ 정리: 각 단계에서 메모리 동작

1️⃣ int a = 5;

  • a는 값 타입스택에 5 저장
  • ✅ 정확함
stack:
 └─ a = 5
heap:
 └─ (비어 있음)

2️⃣ object a = 5; → 박싱(Boxing)

  • 5는 값 타입이지만 object는 참조 타입이므로,
  • 값 5가 힙에 복사되어 저장되고,
  • 스택의 a는 힙 주소값을 가짐
  • ✅ 정확함
stack:
 └─ a = 힙 주소
heap:
 └─ [값 5]

3️⃣ int b = (int)a; → 언박싱(Unboxing)

  • 힙에 있던 5를 다시 값 타입(int) 으로 스택에 복사
  • b는 스택에 5를 가지며 독립적인 값이 됨
stack:
 └─ b = 5
heap:
 └─ [값 5] (여전히 존재)

📌 여기서 중요한 점은:

  • 언박싱을 해도 힙의 기존 object는 여전히 존재
  • 단, 더 이상 참조되지 않는다면, C#의 GC(Garbage Collector)언젠가 메모리에서 회수함
  • ✅ "가비지가 된다"는 표현도 정확함!

🔍 보완 포인트

  • ❗ 힙에 있는 값 = 가비지가 즉시 발생하지는 않음
    • 언박싱 후에도 여전히 object a가 해당 힙 메모리를 참조하고 있다면 GC 대상이 아님
    • object a가 스코프 밖으로 사라지거나 null로 변경되어야 참조가 끊김 → 그때 GC 대상이 됨

✅ 최종 정리

상황 메모리 구조 설명
int a = 5; 스택에 5 저장 값 타입
object a = 5; 힙에 값 복사, 스택엔 주소 저장 박싱 발생
int b = (int)a; 힙에서 값 꺼내 스택에 저장 언박싱 발생, 힙 값은 살아있지만 가비지 가능성 있음

 

그림 가져온 친구 블로그 입니다!

https://velog.io/@khh7052/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-4%EC%9D%BC%EC%B0%A8-25.07.03

 

[내일배움캠프] 4일차 (25.07.03)

- object https://www.youtube.com/watch?v=a7iHDWiSSm4 모든 데이터를 다룰 수 있는 특별한 자료형 > > object는 참조타입 > > > >

velog.io

 

확실히 오브젝트 타입은 타입의 최상위 클래스이기 때문에 모든 값을 받을 수 있지만 박싱/언박싱이 일어나기 때문에

남용한다면 가비지가 계속 쌓일수도 있을 듯 합니다. 타입을 안다면 그 타입에 대해서 사용하는게 좋고 모른다면 

어쩔 수 없이 Object 타입으로 작성하는 수 밖에는 없겠네여!

반응형
반응형

Parent obj = new Child(); 메모리는 어떻게 될까?

🤔 질문

클래스 상속을 사용할 때,

Parent obj = new Child();

이렇게 부모 타입으로 자식 객체를 참조하면,
메모리는 부모 클래스만큼만 잡히는 걸까? 아니면 자식 클래스 전체가 메모리에 올라갈까?


✅ 결론 먼저!

👉 메모리에는 자식 클래스 전체가 생성된다.

즉, **new Child()**를 하면
자식 클래스가 부모를 포함한 상태로 메모리에 만들어지고,
참조 변수(obj)가 그 전체 객체를 가리키게 된다.


🧠 왜 그렇게 되는 걸까?

1. new Child()는 "Child 인스턴스를 생성"하는 코드다

  • 여기서 생성되는 객체는 무조건 Child 전체 클래스
  • Child는 내부적으로 Parent의 멤버도 다 포함하고 있음
    (상속이란 게 그런 것!)

💡 즉, 메모리에는 Parent + Child 모든 멤버가 존재


2. Parent obj는 “부모 타입의 변수”일 뿐이다

  • 이 변수는 Child 객체 전체를 가리키지만,
    사용할 수 있는 기능(메서드, 필드)은 Parent에 선언된 것만이다.
obj.SomeChildOnlyMethod();  // ❌ 오류 — Parent에 없기 때문
((Child)obj).SomeChildOnlyMethod(); // ✅ 캐스팅하면 가능

여기에서 바로 위에 코드가 이해가 안되는데 

Parent obj = new Child(); 자식 객체를 부모 타입 변수에 담음
obj.SomeChildOnlyMethod(); ❌ 불가 — 부모 타입엔 없음
((Child)obj).SomeChildOnlyMethod(); ✅ 가능 — 형변환해서 자식 메서드 호출

 

이렇게 설명해주니 이해가 되는듯합니다! 부모타입에는 없는 메서드를 호출하려고하니

형변환을해서 자식 메서드를 호출한다고 하는거같아요!


3. 그럼 왜 이렇게 쓰는 걸까?

이런 식으로 부모 타입으로 자식 객체를 참조하는 이유는 다형성(polymorphism) 때문이다!

  • 공통된 부모 타입을 이용하면 여러 자식 객체를 하나의 타입으로 다룰 수 있음
  • 예를 들어:
List<Parent> characters = new List<Parent>();
characters.Add(new Warrior());
characters.Add(new Magician());

foreach (var c in characters)
{
    c.Move();       // 각 클래스의 오버라이딩된 Move가 실행됨
    c.Attack();     // 다형성 발동!
}

🧠 이게 바로 다형성: 하나의 타입(Parent)으로 여러 타입(Warrior, Magician)을 다루는 방식


💡 정리: 메모리와 동작 구조

Parent obj = new Child();

메모리 구조:
[ Parent의 멤버 | Child의 멤버 ]

obj 참조:
↑
Parent 타입의 참조라서 Parent 멤버까지만 직접 접근 가능
(단, override된 메서드는 실제 Child의 것이 실행됨!)

🔍 실습 예제

class Parent
{
    public virtual void SayHello()
    {
        Console.WriteLine("안녕하세요. 부모입니다.");
    }
}

class Child : Parent
{
    public override void SayHello()
    {
        Console.WriteLine("안녕하세요. 자식입니다.");
    }

    public void OnlyChildMethod()
    {
        Console.WriteLine("자식만의 기능!");
    }
}
Parent obj1 = new Parent();
obj1.SayHello(); // → 안녕하세요. 부모입니다.

Child obj2 = new Child();
obj2.SayHello(); // → 안녕하세요. 자식입니다.

Parent obj3 = new Child();
obj3.SayHello(); // → 안녕하세요. 자식입니다. (다형성 발동)

((Child)obj3).OnlyChildMethod(); // → 자식만의 기능!

✅ 마무리 요약


코드 메모리 생성 사용할 수 있는 멤버
new Parent() Parent만 Parent 멤버만
new Child() Parent + Child Child 멤버 전부
Parent obj = new Child() Parent + Child Parent 멤버만 직접 사용 (다형성 O)

📝 한 줄 요약

부모 타입으로 자식 객체를 참조해도,
메모리는 자식 전체가 생성되며,
실행 시 다형성에 따라 자식 메서드가 호출된다.

 

다형성이 제대로 이해되지 않는다 흠.. 다시 한번 물어보자.

 

좋아, 이제 진짜 핵심 중의 핵심으로 왔어.
많은 사람이 객체지향에서 **“다형성(polymorphism)”**이라는 단어를 듣고
**“뭔가 중요한 것 같긴 한데… 솔직히 감이 안 잡혀”**라고 느끼거든.
지금 딱 그런 시점에 온 거야 —
지금부터 진짜 이해되는 방식으로 다형성을 설명해줄게 👇


✅ 다형성이란?

"같은 타입(부모 타입)으로 여러 객체(자식 객체)를 같은 방식으로 다루지만,
실제로는 각 객체에 맞는 동작이 실행되는 것"


🔥 핵심 포인트

  • "부모 타입 하나로 다양한 자식 객체를 처리한다." → 다형성
  • 다형성은 “형태는 같지만 행동은 다르게” 동작함

🎯 예제로 설명해볼게

 
class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("동물이 소리를 냅니다.");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("멍멍!");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("야옹!");
    }
}
List<Animal> animals = new List<Animal>();
animals.Add(new Dog());
animals.Add(new Cat());

foreach (Animal a in animals)
{
    a.Speak(); // ??? 어떤 소리가 나올까?
}

👉 출력 결과:

멍멍!
야옹!

🔍 여기서 중요한 건 → a.Speak()는 전부 Animal 타입인데,
실제로는 Dog, Cat 각각의 메서드가 실행된다!

➡️ 이게 바로 다형성(polymorphism)
➡️ 같은 코드(a.Speak()) → 다양한 동작


🧠 비유로 이해해보기

부모 타입 = 콘센트
자식 객체 = 다양한 기기 (선풍기, 에어컨, 충전기 등)

  • 콘센트 하나(Animal 변수)로 어떤 기기(Dog, Cat)를 꽂든,
  • 각각의 기기는 자기 기능대로 작동
콘센트.작동() →
→ 선풍기면 바람 나옴
→ 충전기면 충전됨
→ 에어컨이면 냉풍 나옴

➡️ 인터페이스는 같고, 동작은 다름
➡️ 다형성!


✅ 왜 중요한가?

이유 설명
유지보수가 쉬워짐 부모 타입 기준으로 다룰 수 있어서 코드가 유연해짐
코드 중복 줄임 동일한 인터페이스/부모 클래스로 묶을 수 있음
확장성 좋아짐 새로운 자식 클래스가 생겨도 부모 타입만 알면 됨

🎮 게임 예제

 
abstract class Enemy
{
    public abstract void Attack();
}

class Slime : Enemy
{
    public override void Attack() => Debug.Log("슬라임 점프 공격!");
}

class Dragon : Enemy
{
    public override void Attack() => Debug.Log("드래곤 불뿜기!");
}
List<Enemy> enemies = new List<Enemy>() {
    new Slime(), new Dragon()
};

foreach (Enemy e in enemies)
{
    e.Attack();  // 각 몬스터의 고유 공격 실행됨!
}

✅ 한 줄 정리

다형성은 **“하나의 타입(부모)으로 여러 타입(자식)을 다루면서도,
각 객체에 맞는 동작을 실행하는 객체지향의 핵심 기능”**이야.

 

그렇다고한다! 아직도 와닿지는 않지만 이해는 됐다. 직접 개발해봐야 느낌이 올듯하다. 확실히 나중에 개발하면 코드에 대한 유지보수나 여러 기능들 만들고 사용할때 많이 사용하니 잘 알아둬야 할듯하다.

반응형

C# - base 키워드

Dev_Jen
|2025. 7. 3. 12:26
반응형

✅ base 키워드란?

자식 클래스에서 부모 클래스의 생성자나 메서드, 필드에 접근할 때 사용하는 키워드


📌 1. 부모 클래스의 메서드 호출할 때

class Parent
{
    public void Speak()
    {
        Console.WriteLine("부모가 말한다");
    }
}

class Child : Parent
{
    public void Say()
    {
        base.Speak(); // 부모의 Speak() 호출
    }
}

📌 2. 부모 생성자 호출

class Parent
{
    public Parent(string name)
    {
        Console.WriteLine($"{name} 생성됨");
    }
}

class Child : Parent
{
    public Child() : base("자식이지만 부모도 초기화") 
    {
        Console.WriteLine("자식 생성자 실행");
    }
}

📌 3. 오버라이드한 메서드 안에서 부모의 원본 호출

class Character
{
    public virtual void Attack()
    {
        Console.WriteLine("기본 공격!");
    }
}

class Warrior : Character
{
    public override void Attack()
    {
        base.Attack(); // 부모의 기본 공격도 같이 실행됨
        Console.WriteLine("검 휘두르기!");
    }
}

💡 언제 쓰는가?

부모의 로직을 확장하고 싶을 때 base.메서드() 호출
부모의 생성자 초기화가 필요한 경우 : base(...) 사용
부모의 기본 동작도 유지하고 싶을 때 오버라이드 안에서 base 사용
 

⚠️ 주의

  • base는 상속 관계에서만 사용 가능
  • 자식이 부모를 가리킬 때만 쓸 수 있음
  • base는 부모 클래스의 "구현된 기능"에 접근할 때 쓰는 거지, 추상 인터페이스에는 직접 쓰지 않음

✅ 한 줄 정리

base는 자식 클래스에서 "부모 클래스의 기능을 호출"할 수 있도록 해주는 키워드야.

 

흠..잘 알겠지만 이건 잘 사용하지 않을듯한 느낌이 든다.. 아직 내가 유니티에서 사용하지 않아봐서 그럴지도 모른다.

그래서 GPT한테 base 키워드에한 중요성을 물어봤다!

 

진짜 좋은 질문이야.
많은 사람이 처음에 base를 보면 “어? 굳이 이걸 꼭 써야 하나?”,
“내가 override 해서 다 재정의하면 되지 않나?” 이런 생각을 해.
근데 실제로는 잘 쓰면 엄청 유용하고, 안 쓰면 부모 클래스 기능을 날려먹을 수 있어.


✅ base의 중요성은 “부모 기능을 이어받되, 추가하거나 확장할 수 있게 해주는 것”이야.


🎯 base를 안 쓰면 생기는 문제

예시: 캐릭터 기본 공격을 무시해버림

class Character
{
    public virtual void Attack()
    {
        Console.WriteLine("기본 공격!");
    }
}

class Warrior : Character
{
    public override void Attack()
    {
        Console.WriteLine("워리어 스킬 발동!");
    }
}
  • 👉 이 경우 기본 공격 출력 안 됨
  • Character의 공격 로직이 완전히 무시됨 → 부모 기능 상실

✅ base를 써서 부모 기능도 유지

class Warrior : Character
{
    public override void Attack()
    {
        base.Attack();  // 부모의 기본 공격 먼저 실행
        Console.WriteLine("워리어 스킬 발동!");
    }
}

➡️ 결과:

 
기본 공격!
워리어 스킬 발동!

✅ → 부모 로직 + 자식 로직이 순차적으로 실행됨 = 기능 확장


🔥 실전에서 base를 잘 쓰는 대표적인 상황

UI 초기화, 애니메이션 제어 부모가 기본 처리 다 해놓고, 자식은 추가 효과만 넣고 싶을 때
MonoBehaviour의 Start(), Update() 재정의 자식에서 base.Start() 호출 안 하면 부모의 Start 로직이 사라짐
디버깅, 추적, 로깅 부모에서 로깅 다 해놓고, 자식이 그 위에 로직 덧붙일 수 있음
템플릿 메서드 패턴 부모의 흐름 유지하면서 일부만 바꿔야 할 때 base로 확장 가능

❗ 안 쓰면 벌어지는 일

  • 부모 클래스의 중요한 로직이 사라짐
  • 자식 클래스가 전체 흐름을 깨뜨려버림
  • 유지보수할 때 “왜 안 되지?”하고 원인을 찾기 어려워짐

✅ 한 줄 정리

base는 **"부모의 로직을 살리면서 자식에서 기능을 확장"**하고 싶을 때 정말 중요한 도구야.

 

오 그래도 이렇게 보니까 base의 기능이나 필요성을 잘 알거같다 나중에 유니티 개발할때 꼭 사용해봐야겠다!

반응형
반응형

✅ 템플릿 메서드 패턴이란?

**템플릿 메서드 패턴(Template Method Pattern)**은
상속을 기반으로 부모 클래스에서 **알고리즘의 전체 흐름(틀)**을 정의하고,
일부 단계는 abstract 또는 virtual로 정의하여 자식 클래스가 자유롭게 재정의할 수 있도록 하는 패턴입니다.

 

참고 영상

https://www.youtube.com/watch?v=TfwHAT1H04I

 


🧱 핵심 구성요소

템플릿 클래스 PlayerBase 공통 기능 (Idle(), Move())은 virtual, 꼭 구현해야 하는 핵심 기능은 abstract로 정의
구체 클래스 Warrior, Magician PlayerBase를 상속받아 필요한 메서드들을 override
클라이언트 Test PlayerBase 타입으로 실제 자식 인스턴스를 받아 실행 (다형성 활용)

🔍 코드 구조 분석

1. 템플릿 클래스: PlayerBase

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Template
{
    // 템플릿 클래스
    // 공통 기능(Idle, Move)은 virtual로 제공하고
    // 핵심 기능(MoveTarget, Attack)은 abstract로 정의해 자식이 반드시 구현하도록 강제함
    public abstract class PlayerBase : MonoBehaviour
    {
        public virtual void Idle()
        {
            // 공용 Idle 구현
        }

        public virtual void Move()
        {
            // 공용 Move 구현
        }

        public abstract void MoveTarget(); // 반드시 자식이 구현해야 할 핵심 로직
        public abstract void Attack();     // 반드시 자식이 구현해야 할 핵심 로직
    }
}
  • 공통 동작은 virtual로 정의하여 자식이 선택적으로 재정의
  • 핵심 필수 동작은 abstract로 정의하여 자식이 반드시 구현하게 강제

2. 구체 클래스: Warrior, Magician

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Template
{
    // Warrior 클래스는 PlayerBase를 상속받아
    // 추상 메서드를 구체적으로 구현한다
    public class Warrior : PlayerBase
    {
        public override void MoveTarget()
        {
            // 워리어 MoveTarget 구현
            // abstract로 받아서 직접 구현해줘야함
        }

        public override void Attack()
        {
            // 워리어 Attack 구현
            // abstract로 받아서 직접 구현해줘야함
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Template
{
    // Magician 클래스는 PlayerBase를 상속받고
    // Move()까지 override해서 고유 이동 방식 정의
    public class Magician : PlayerBase
    {
        public override void Move()
        {
            // 메지션 Move 구현
            // 메지션은 따로 움직임이 필요해서 만든 것
        }

        public override void MoveTarget()
        {
            // 메지션 MoveTarget 구현
            // abstract로 받아서 직접 구현해줘야함
        }

        public override void Attack()
        {
            // 메지션 Attack 구현
            // abstract로 받아서 직접 구현해줘야함
        }
    }
}
  • Warrior는 기본 Move() 로직 사용 → override 안함
  • Magician은 이동이 특이하므로 Move()도 override

3. 실행 코드 (클라이언트): Test

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Template
{
    public class Test : MonoBehaviour
    {
        [SerializeField] PlayerBase _playerBase;

        private void Start()
        {
            // Idle
            _playerBase.Idle();

            // Move
            _playerBase.Move();

            // MoveTarget
            _playerBase.MoveTarget();

            // Attack
            _playerBase.Attack();

            // 사용한다면 이런식으로 사용한다.
            // 다양성을 이용해서 만들어놓은 워리어, 메지션 등 사용할 수 있다.
            // => 이게 바로 템플릿 메서드 패턴의 핵심! 알고리즘 틀은 고정, 세부는 다양
        }
    }
}
  • PlayerBase 타입 하나로 Warrior, Magician 등 다양한 자식 타입을 다룰 수 있음
  • 다형성(Polymorphism) 활용 → 코드 변경 없이 캐릭터 전환 가능

🎮 정리: 왜 템플릿 메서드 패턴이 좋은가?

장점설명
알고리즘의 흐름 고정 공통 로직은 부모가 제공하여 일관성 유지
세부 단계 유연화 자식 클래스가 필요한 부분만 재정의 가능
중복 제거 공통 코드 반복을 줄이고 관리 포인트를 줄임
다형성 활용 PlayerBase 하나로 모든 캐릭터를 통합해서 제어 가능
 

✅ 최종 요약

  • PlayerBase는 게임 캐릭터 로직의 골격을 정의한 템플릿 클래스
  • Warrior, Magician은 세부 구현을 담당하는 서브 클래스
  • Test는 추상 타입을 통해 다양한 캐릭터를 처리하는 클라이언트
  • abstract, virtual을 적절히 활용하여 강제성과 유연성의 균형을 유지

음..사실 템플릿 메서드가 영상을 보고도 왔지만 정확히 이해가 잘 되지 않았다 템플릿을 왜 쓰는걸까? 그냥 virtual이랑 abstract를 사용해서 상속받고 정의하고 뭐 이런느낌인거같은데 템플릿을 왜 쓰는지 정확히 이해가 되지 않았다.

그래서 GPT 에게 다시 물어봤다 왜쓰는지.

 

✅ 템플릿 메서드는 왜 쓰는 걸까?

"전체 알고리즘의 흐름은 고정하되, 일부 단계는 유연하게 바꿀 수 있도록 하기 위해"


🎯 예시로 풀어보자

게임에서 "캐릭터 행동"이라는 공통 흐름이 있다고 해봐요:

1. Idle 상태 진입
2. 이동 처리
3. 타겟 탐색
4. 공격 처리

이 흐름은 모든 캐릭터가 공유하는 로직이에요.
하지만 각각의 캐릭터는 공격 방식이나 타겟 찾는 방식이 다를 수 있죠?

  • 전사는 근거리에서 찌르고
  • 궁수는 원거리에서 조준하고
  • 마법사는 주문을 시전하고

이때 쓰는 게 바로 템플릿 메서드 패턴이에요.


✅ 정리하자면, 템플릿을 쓰는 이유는?

공통 흐름을 재사용하고 싶어서 알고리즘 틀은 매번 새로 만들 필요 없이 재사용 가능
세부 로직만 바꾸고 싶어서 자식 클래스가 필요한 부분만 override 하면 됨
중복 코드를 줄이고 싶어서 부모에 공통 로직을 두면 중복이 줄어듦
확장성 좋고 깔끔한 구조 만들기 위해 새로운 캐릭터, 새로운 동작 추가 시 최소한의 코드만 작성하면 됨

💬 템플릿을 안 쓰면 어떤 문제가 생길까?

class Warrior
{
    public void DoAll()
    {
        Idle();
        Move();
        FindTarget();  // 전용
        Attack();      // 전용
    }
}

class Magician
{
    public void DoAll()
    {
        Idle();
        Move();
        FindTarget();  // 전용
        Attack();      // 전용
    }
}

➡️ 공통 흐름이 중복됨
➡️ 흐름을 바꾸려면 모든 클래스 수정해야 함
➡️ 버그 생기고 유지보수 어려워짐


✅ 템플릿은 이렇게 정리할 수 있어요

템플릿 메서드 패턴은
"전체 알고리즘 흐름은 부모 클래스가 정하고,
그 일부 단계를 자식 클래스가 유연하게 바꿀 수 있도록 만드는 패턴"

 

그러면...그냥 오버 버츄얼로 오버라이드해서 사용하면 되는거 아닌가? 라는 생각이 들었다.

 

“그냥 virtual 메서드로 오버라이드해서 쓰면 되는데,
굳이 템플릿 메서드 패턴을 왜 따로 써야 해?”

✅ 결론부터 말하자면:

단순히 virtual로 메서드 하나하나 오버라이드하는 것과
"템플릿 메서드 패턴"을 적용하는 것의 차이는
알고리즘 전체 흐름을 ‘고정’하고 ‘설계 의도를 명확히 하는가’에 있어요.


🎯 차이점 설명

virtual 단독 오버라이드 각 기능을 개별적으로 바꿔치기만 가능 (공통된 흐름 없음)
템플릿 메서드 패턴 전체 흐름은 부모가 고정하고, 일부 단계만 자식이 커스터마이징

📌 예를 들어보자

① 그냥 virtual 방식

class Player
{
    public virtual void Move() { /* 기본 이동 */ }
    public virtual void Attack() { /* 기본 공격 */ }
}

class Warrior : Player
{
    public override void Move() { /* 워리어 이동 */ }
    public override void Attack() { /* 워리어 공격 */ }
}

➡️ 자식은 알아서 각자 필요한 것만 바꿈
❌ 하지만 **"이 둘을 어떤 순서로 실행해야 하는지"**에 대한 틀이 없음


② 템플릿 메서드 방식

abstract class Player
{
    public void Act()
    {
        Idle();
        Move();
        MoveTarget();
        Attack();
    }

    protected virtual void Idle() { }
    protected virtual void Move() { }
    protected abstract void MoveTarget();
    protected abstract void Attack();
}

➡️ Act()라는 전체 흐름(알고리즘)을 부모가 직접 고정
➡️ 자식은 일부 단계만 정해진 순서에 따라 구현
틀은 지키면서 다양성을 제공


🔥 왜 중요할까?

  • virtual만 쓰면 → 각 메서드가 독립적이라 전체 흐름이 흩어짐
  • 템플릿 패턴 쓰면 → 일관된 흐름 + 유연한 세부 단계 변경 가능
  • 설계 의도를 명확히 할 수 있음
    👉 “이 흐름은 무조건 이렇게 진행돼야 해! 하지만 이 부분은 너가 알아서 해”

✅ 한 줄 정리

그냥 virtual은 부분만 바꾸는 도구,
템플릿 메서드는 흐름을 지키면서 유연하게 바꾸는 설계 도구다.

 

흠.. 그렇다고 한다. 이것도 유니티로 직접 구현해봐야 이해할 수 있을듯 하다. 이론적으로는 알겠다. 구현이 문제군..

반응형
반응형

✅ 클래스 상속 (Inheritance)

✔️ 정의

class A : B처럼, 한 클래스가 다른 클래스의 기능을 물려받는 것

✔️ 목적

  • 코드 재사용: 부모 클래스의 멤버(필드, 메서드 등)를 자식이 그대로 사용 가능
  • 기능 확장: 자식 클래스는 새로운 기능을 추가하거나, 부모 기능을 재정의 가능
  • 다형성(polymorphism): 부모 타입으로 자식 객체를 다룰 수 있음

✔️ 예제

class Character
{
    public virtual void Attack()
    {
        Console.WriteLine("기본 공격!");
    }
}

class Warrior : Character
{
    public override void Attack()
    {
        Console.WriteLine("칼 휘두르기!");
    }
}
Character c = new Warrior();
c.Attack();  // 출력: 칼 휘두르기!

➡️ 부모 타입으로 자식 인스턴스를 다룰 수 있다 = 다형성


⚠️ 단점

  • 단일 상속만 가능: 부모는 한 명만
  • 상속 깊이가 깊어지면 유지보수가 어려워짐
  • 상속 관계가 강한 결합을 만들 수 있음 (부모 변경 시 자식에 영향 큼)

✅ 인터페이스 (Interface)

✔️ 정의

interface IAttackable처럼, 기능의 "약속"만 정의하는 타입
구현 없이 메서드 시그니처만 선언
→ 구현은 클래스에서 직접 해야 함


✔️ 목적

  • 다형성 지원: 특정 인터페이스를 구현한 객체들을 동일하게 다룸
  • 의존성 분리: 구체 타입보다 "역할"에 의존
  • 다중 상속 가능: 하나의 클래스가 여러 인터페이스를 동시에 구현 가능

✔️ 예제

 
interface IAttackable
{
    void Attack();
}

class Archer : IAttackable
{
    public void Attack()
    {
        Console.WriteLine("화살 발사!");
    }
}
IAttackable a = new Archer();
a.Attack();  // 출력: 화살 발사!

➡️ 누구든 IAttackable만 구현하면 같은 방식으로 다룰 수 있음 → 유연함, 약한 결합


✅ 클래스 상속 vs 인터페이스 정리

항목 클래스 상속 인터페이스
목적 기능 물려주기 (재사용) 역할 정의 (규칙 강제)
코드 재사용 ✅ 가능 ❌ 없음 (직접 구현 필요)
다중 상속 ❌ 안 됨 ✅ 가능
구현 제공 ✅ 가능 (virtual, 필드 등) ❌ 없음 (단순 시그니처)
추상화 수준 구체적 (기능 포함) 완전 추상적
설계 유연성 낮음 (강한 결합) 높음 (약한 결합)

그럼 클래스의 abstract와 인터페이스는 좀 비슷한거 아니야? 라고 생각이 들었다. 

 

💯 딱 그거야!
많은 사람들이 처음에 “abstract class랑 interface… 비슷한데 뭐가 달라?” 라고 궁금해해.
왜냐면 둘 다 “강제 구현”을 요구하고, 직접 인스턴스를 만들 수 없고, 자식이 override 해야 하니까 헷갈릴 수 있어.

근데! 실제로는 의도와 사용 목적이 분명하게 다르기 때문에


**"언제 abstract class를 쓰고 언제 interface를 쓰는가?"**가 중요한 핵심이야.

✅ 공통점

인스턴스 생성 불가 abstract class, interface 모두 직접 new로 못 만듦
메서드 강제 구현 자식 클래스가 반드시 override해야 하는 메서드 있음
다형성 지원 부모 타입으로 자식 인스턴스를 다룰 수 있음 (Animal a = new Dog())

⚔️ 차이점

항목 abstract class interface
목적 공통 로직 공유 + 일부 구현 강제 **기능 명세(역할)**만 정의
기본 구현 제공 ✅ 가능 (필드, 메서드, 속성 등) ❌ 원칙상 불가능 (C# 8부터 일부 가능하긴 함)
필드 선언 ✅ 가능 ❌ 불가능
생성자 ✅ 가질 수 있음 ❌ 없음
상속 개수 ❌ 단일 상속만 가능 ✅ 다중 상속 가능
접근 제한자 ✅ 사용 가능 (protected, private, 등) ❌ 기본적으로 public only

🎮 메이플스토리 비유로 다시 정리!

abstract class 공통 기능 + 각 직업 전용 스킬: 기본 체력 시스템, 레벨업 시스템 등 공유됨 + 각 전직 클래스가 고유 스킬 구현
interface 공격 가능한 존재: 플레이어, 몬스터, 보스가 각각 다른 방식으로 Attack() 구현함 → 역할만 공유함

✅ 언제 abstract class를 쓰고 언제 interface를 써?

공통 로직도 공유해야 함 ✅ abstract class
여러 클래스가 같은 기능을 수행하지만, 내부 로직은 각자 다르게 하고 싶을 때 ✅ interface
여러 개의 기능(역할)을 동시에 구현해야 함 ✅ interface
기본 멤버/필드/생성자도 물려줘야 함 ✅ abstract class

🧠 기억하기 좋은 한 줄 요약

interface는 “무엇을 할 수 있는가”를 말하고,
abstract class는 “무엇을 공유할 것인가”를 말한다.

 

흠.. 클래스 상속과 인터페이스는 알겠는데 아직까지 솔직히 abstract Class와 Interface의 차이가 이론적으로는 알겠지만 마음으로 와닿지 않는다 이건 직접 유니티로 구현해보면서 경험을 쌓는 수 밖에 없을 듯 하다.. 

반응형

'C#' 카테고리의 다른 글

C# - Parent obj = new Child(); 메모리는 어떻게 될까? & 다형성  (1) 2025.07.03
C# - base 키워드  (3) 2025.07.03
C# - Virtual & abstract  (0) 2025.07.03
C# - 자료구조  (1) 2025.07.02
C# - 해쉬셋(HashSet) vs Dictionary(HashTable)  (2) 2025.07.02

C# - Virtual & abstract

Dev_Jen
|2025. 7. 3. 10:04
반응형

✅ virtual 키워드 정리

📌 정의

virtual은 부모 클래스에서 메서드나 속성을 "재정의 가능"하게 만드는 키워드입니다.


📋 기본 문법

class Parent
{
    public virtual void SayHello()
    {
        Console.WriteLine("안녕하세요. 부모입니다.");
    }
}

class Child : Parent
{
    public override void SayHello()
    {
        Console.WriteLine("안녕하세요. 자식입니다.");
    }
}

✅ 주요 특징

기본 구현 제공 부모 클래스가 기본 로직을 정의
자식 클래스에서 변경 가능 override 키워드로 오버라이딩 가능
선택적 재정의 자식 클래스에서 반드시 재정의하지 않아도 됨
다형성 지원 부모 타입으로 자식 객체를 가리킬 때, 실제 자식의 메서드가 실행됨

🔍 예시

 
Parent obj1 = new Parent();
obj1.SayHello();  // 출력: 안녕하세요. 부모입니다.

Child obj2 = new Child();
obj2.SayHello();  // 출력: 안녕하세요. 자식입니다.

Parent obj3 = new Child();
obj3.SayHello();  // 출력: 안녕하세요. 자식입니다. (다형성!)

🧠 언제 쓸까?

  • 기본 동작은 있으나, 자식 클래스에서 상황에 따라 동작을 바꾸고 싶을 때
  • 템플릿 메서드 패턴에서 일부 단계를 virtual로 두면 자식 클래스가 필요 시 바꿀 수 있음

❗ 주의

  • virtual 메서드는 자식 클래스에서 override 키워드로 재정의해야
  • virtual 없이 그냥 override는 불가능함 → 부모가 virtual 또는 abstract여야 함

 

그럼 궁금한게 꼭 버츄얼을 써야 오버라이드가 가능한건가? 싶기도하고 abstract는 또 뭘까 싶었다.

 

✅ 1. 꼭 virtual을 써야 override가 가능한가?

👉 네, 맞습니다.

  • override 키워드는 반드시 부모 클래스에서 virtual, abstract, 또는 override로 선언된 메서드만 재정의할 수 있어요.
  • 즉, virtual 없이 그냥 만든 메서드는 자식 클래스에서 override 할 수 없습니다.
class Parent
{
    public void Speak()  // virtual이 아님
    {
        Console.WriteLine("부모가 말합니다");
    }
}

class Child : Parent
{
    // ❌ 오류! override 불가능
    public override void Speak()
    {
        Console.WriteLine("자식이 말합니다");
    }
}

✅ 2. 그럼 abstract는 뭐야?

👉 abstract는 부모 클래스에서 "이건 반드시 자식 클래스가 직접 구현해야 해!" 라고 강제할 때 사용해요.

📌 특징 비교

  

구분 virtual abstract
기본 구현 ✅ 있음 ❌ 없음 (구현 금지)
재정의 여부 자식 클래스에서 재정의 선택 가능 자식 클래스에서 반드시 override 해야 함
용도 "기본 구현은 있지만, 필요하면 바꿔 써도 돼" "이건 자식이 반드시 구현해야 돼"

 


🎯 코드 예시로 완전 비교

🔹 virtual 예시

class Parent
{
    public virtual void Greet()
    {
        Console.WriteLine("Hello from Parent");
    }
}

class Child : Parent
{
    public override void Greet()
    {
        Console.WriteLine("Hello from Child");
    }
}
 

🔹 abstract 예시

abstract class Animal
{
    public abstract void Speak();  // 구현 ❌
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("멍멍!");
    }
}

→ Speak()는 무조건 자식이 override 해야 함.
→ 안 하면 컴파일 오류 발생


🧠 요약 정리

질문답변
virtual 없이 override 가능? ❌ 불가능 (virtual, abstract, 또는 override 필요)
abstract는 뭐야? 자식 클래스가 반드시 구현해야 하는 메서드를 정의할 때 사용
virtual과 abstract 차이는? 기본 구현 유무 + 오버라이드 강제 여부의 차이

그러면 굳이 abstract를 써야하는 이유가있나?..싶었다..

그럼 도대체 두개의 차이점이 뭘까..

 

✅ 핵심 요약: 둘의 의도와 차이

 

항목 virtual abstract
기본 구현 있다 → 자식이 그대로 써도 됨 없다 → 자식이 반드시 구현해야 함
자식에서 override 선택 가능 강제
설계 의도 "기본 동작이 있지만, 필요하면 바꿔 써" "이건 네가 알아서 반드시 만들어"
목적 유연성 강제성, 명확한 계약
사용 가능 클래스 일반 클래스, 추상 클래스 추상 클래스에서만 사용 가능
장점 코드 재사용 가능 설계 실수 방지, 역할 명확
단점 재정의 안 하면 예측 어려움 반복적 구현 필요 (유지비 증가)
 

🎯 예시 비교로 감 잡기

✅ 예제 상황: "게임 캐릭터"

1. virtual 예시

class Character
{
    public virtual void Attack()
    {
        Console.WriteLine("기본 공격: 펀치!");
    }
}

class Knight : Character
{
    public override void Attack()
    {
        Console.WriteLine("검으로 공격!");
    }
}

class Villager : Character
{
    // Attack() 안 바꿔도 됨 → 기본 공격 그대로 사용
}

➡️ Villager는 특별한 공격이 없으니 virtual 덕분에 기본 공격을 그대로 써도 괜찮음
➡️ 유연함이 중요할 때 → virtual


2. abstract 예시

abstract class Character
{
    public abstract void Attack();  // 반드시 구현!
}

class Knight : Character
{
    public override void Attack()
    {
        Console.WriteLine("검으로 공격!");
    }
}

class Villager : Character
{
    // ❌ Attack() 안 만들면 컴파일 에러 → 강제
}

➡️ 모든 캐릭터는 반드시 자신만의 공격을 가져야 해! → 이런 명확한 룰을 강제할 때 abstract
➡️ 설계 실수 방지하고 싶을 때 → abstract


📌 그럼 뭐가 더 효율적인가?

👉 "효율"은 상황에 따라 달라요.

기본 동작은 있지만, 바꿀 수 있도록 열어두고 싶다 ✅ virtual
꼭 자식이 직접 구현해야만 의미가 있다 ✅ abstract
공통 로직은 부모에서 쓰고, 일부만 다르게 처리하고 싶다 ✅ 템플릿 메서드 + abstract + virtual 혼용
 

🧠 결론

  • virtual → "기본은 줄게, 필요하면 바꿔 써"
    → 재사용 중심, 유연한 구조
  • abstract → "이건 네가 반드시 해"
    → 계약 기반 설계, 실수 방지, 역할 명확화

🎯 기억하기 쉽게 요약하면:

"바꿔도 되고 안 바꿔도 되면 virtual,
무조건 바꿔야 한다면 abstract"

 

음...정확하게 뭔가 와닿지는 않지만 캐릭터의 스킬이나 공격스킬로 따지자면

virtual - 기본 공격 스타일 메이플의 기본공격정도이지 않을까 싶다. 기본적으로 찌르기,내려치기,올려치기 등등 기술을 사용할때 정도?

abstract - 메이플에서의 전사,궁수,해적,마법사,도적 등등 전직했을때의 각각의 스킬로 사용할때정도로 정의되지 않을까 싶다!!  

 

🔥 비유 리뷰

virtual 기본 공격 스타일 (찌르기, 내려치기 등) "기본은 이거야, 하지만 바꿔도 돼" → 재정의 가능, 선택적
abstract 전직 클래스별 고유 스킬 (전사, 궁수, 해적 등) "반드시 직접 구현해야 해" → 자식 클래스가 구현 강제됨
 

→ 이 비유는 진짜로 설계 철학에 딱 맞아.

라고 GPT가 친절하게 설명해주네여ㅎㅎ

반응형