Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(21일차) - C# 기초(10) / 형 변환과 상속의 개념

by 독기품은토끼 2025. 6. 13.
✅ 오늘의 학습 목표
1. C# 기초 (10) - 형 변환 / 객체지향 프로그래밍

 

1. 형 변환 (Casting)

자료형(데이터 타입)을 변경하는 것

🥕 예행 작업
1. Scene 생성 (Casting)
2. Script 생성 (StudyCasting)

 

1. 명시적 / 암시적 변환

using UnityEngine;

public class StudyCasting : MonoBehaviour
{
    int number1 = 1;
    float number2 = 1.99f;

    void Start()
    {
        // 명시적 형변환 : 데이터 소실 가능성 있음
        number1 = (int)number2;
        Debug.Log(number1); // 1 출력 (반올림 개념 없음)

        // 암묵적 형변환
        // Vector3는 float 타입으로 생성되기 때문에 10, 20, 30으로 초기화해도 암묵적으로 float형으로 변환된다.
        Vector3 vec = new Vector3(10, 20, 30);

        // 수학적인 기능 (C#에서는 Math / Unity에서는 Mathf)
        float number4 = Mathf.Floor(number2); // 내림
        float number5 = Mathf.Ceil(number2); // 올림
        float number6 = Mathf.Round(number2); // 반올림

        Debug.Log($"Floot 내림차순 : {number4}"); // 1 출력
        Debug.Log($"Ceil 오름차순 : {number5}"); // 2 출력
        Debug.Log($"Round 반올림 : {number6}"); // 2 출력
    }
}

 

명시적 형 변환은 데이터 타입을 강제로 변환하는 것이기 때문에 데이터 손실이 발생할 수 있고,

암묵적 형 변환의 경우 컴파일러가 형 변환이 가능한지의 여부를 파악하기 때문에 데이터 손실이 생기지 않는다.

 

2. 함수를 통한 변환

2.1. Parse()

숫자형 변환

using UnityEngine;

public class StudyCasting : MonoBehaviour
{
    void Start()
    {
        string str1 = "123";
        string str2 = "456";
        Debug.Log("String : " + str1 + str2); // 123456

        // 불가능 Parse를 사용해주어야 함
        //int num3 = (int)str1;
        //int num4 = str1;

        int num1 = int.Parse(str1);
        int num2 = int.Parse(str2);
        Debug.Log("Int : " + num1 + num2); // 123456
        Debug.Log("Int : " + (num1 + num2)); // 579
        Debug.Log($"Int : {num1} + {num2}"); // 123 + 456
    }
}

 

 

2.2. ToString()

문자열 변환

using System;
using UnityEngine;

public class StudyCasting : MonoBehaviour
{
    void Start()
    {
        int num1 = 123;

        // 불가능 -> ToString 사용
        //string str1 = num1;
        //string str2 = (string)num1;
        string str3 = num1.ToString();
    }
}

 

2.3. ToBoolean()

True / False 변환

using System;
using UnityEngine;

public class StudyCasting : MonoBehaviour
{
    void Start()
    {
        int num0 = 0;
        int num1 = 1;
        int num2 = 2;
        int num100 = 100;

        float fNum0 = 0f;
        float fNum1 = 1.57f;
        float fNum2 = 3.14f;

        string str1 = "true";
        string str2 = "false";
        string str3 = "안녕하세요.";

        Debug.Log("Num0 : " + Convert.ToBoolean(num0)); // False
        Debug.Log("Num1 : " + Convert.ToBoolean(num1)); // True
        Debug.Log("Num2 : " + Convert.ToBoolean(num2)); // True
        Debug.Log("Num100 : " + Convert.ToBoolean(num100)); // True

        Debug.Log("fNum0 : " + Convert.ToBoolean(fNum0)); // False
        Debug.Log("fNum1 : " + Convert.ToBoolean(fNum1)); // True
        Debug.Log("fNum2 : " + Convert.ToBoolean(fNum2)); // True

        Debug.Log("str1 : " + Convert.ToBoolean(str1)); // True
        Debug.Log("str2 : " + Convert.ToBoolean(str2)); // False
        Debug.Log("str3 : " + Convert.ToBoolean(str3)); // Error!!
    }
}

 

0은 false를 반환하고, 0을 제외한 숫자는 True로 변환한다.

문자열을 boolean으로 변환하면 True, False 값을 제외하고는 에러가 발생한다.

 

2.4. ToInt32()

bool 타입을 Int 타입으로 변환

using System;
using UnityEngine;

public class StudyCasting : MonoBehaviour
{
    void Start()
    {
        bool isBool1 = true;
        bool isBool2 = false;

        int num1 = Convert.ToInt32(isBool1);
        int num2 = Convert.ToInt32(isBool2);

        Debug.Log(num1); // 1
        Debug.Log(num2); // 0
    }
}

 

3. 클래스 변환

🥕 예행 작업
1. Scripts 생성 (Moster, Orc, Goblin)
2. Orc, Goblin → Monster
클래스 형 변환을 하기 위해 부모/자식 관계 만들기
// public class Orc : Monster
// public class Goblin : Monster

 

 

3.1. Up Casting

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

public class StudyCasting : MonoBehaviour
{
    List<Orc> orcs = new List<Orc>();
    List<Goblin> goblins = new List<Goblin>();
    List<Monster> monsters = new List<Monster>();

    void Start()
    {
        Orc o = new Orc();
        Goblin g = new Goblin();

        // 명시적 클래스 형 변환 (가독성)
        Monster m1 = (Monster) o;
        Monster m2 = (Monster) g;

        // 암시적 클래스 형 변환 (개발 효율)
        Monster m3 = o;
        Monster m4 = g;
        
        monsters.Add(o);
        monsters.Add(g);

        // 부모로 통합해서 공격
        // monster.AllAttack();

        // 자식 전체 공격 -> 몬스터 종류가 많아질수록 관리 불가!! = 부모로 통합할 것
        // orcs.AllAttack();
        // goblins.AllAttack();
    }
}

 

 

3.2. Down Casting

// 다운 캐스팅 -> 예외 발생 가능!
Monster m5 = new Monster();
Orc o2 = (Orc)m5;
Debug.Log(o2);

 

 

3.3. Is와 As

  • Is : 타입을 확인하여 bool 타입 반환 → 값, 참조 타입 모두 가능
  • As : 형 변환이 가능한지 확인한 후 변환 or Null 반환 → 참조 타입만 가능

 

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

public class StudyCasting : MonoBehaviour
{
    void Start()
    {
        Monster m = new Monster();

        // Orc o1 = m; // 암시적 변환 X
        // Orc o = (Orc)m; // 명시적 변환 X -> 에러

        Orc o = m as Orc; // 성공시 형변환 / 실패시 Null

        if (o != null)
        {
            Debug.Log(o);
        }
        else
        {
            Debug.Log("형변환 되지 않음");
        }
    }
}

 

다운캐스팅을 할 때에는 객체의 실제 타입이 대상 타입과 다를 수 있으므로

is 키워드로 타입을 먼저 검사하거나, as 키워드로 캐스팅 후 null 체크를 통해 형변환이 안전하게 이루어졌는지 확인하는 것이 좋다.

 

 

4. Boxing(박싱)

값 타입→ 참조 타입으로 변환하는 과정으로,

int, float, bool, struct 등이 값 타입인데 이걸 object나 interface로 변환할 때 박싱이 발생한다.

 

 

4.1. Boxing

int number = 10;
object obj = number; // OK

 

  • int는 값 타입이라 스택에 저장됨
  • object는 참조 타입이니까 에 저장됨
  • number 값을 에 복사해서 object 형태로 감쌈 = 10을 힙에 올리고 그 참조를 obj에 저장함

 

4.2. UnBoxing

int number2 = (int)obj;

 

 

  • 여기서 obj는 실제로 힙에 있는 int(10)의 참조를 가지고 있음
  • (int)obj는 다시 힙에서 값을 꺼내 스택에 저장하는 과정
  • 이때 정확한 타입으로 캐스팅해야 함
    여기서 float, string으로 캐스팅하면 InvalidCastException 발생

 

이렇게 스택 → 힙 / 힙 → 스택 변환 과정을 반복하기 때문에 박싱은 성능 저하의 원인이 될 수 있다.

무엇보다 내가볼땐 가독성도 에바참치임

 

 

5. Generic (제너릭) <T>

데이터 타입을 파라미터로 활용하는 변수

  • Boxing & UnBoxing 성능 저하 문제 해결
  • 제너릭은 컴파일 타임에 타입이 고정되므로 런타임 중 박싱이나 캐스팅 같은 성능 비용이 발생하지 않는다.
  • 코드 재사용성 ↑

 

보통 게임 개발에서 여러 타입의 오브젝트(예: 몬스터, 오크, 고블린 등)를 프리팹으로 만들어 두고, 각각의 팩토리나 매니저를 따로 만드는 경우가 많다.

// 제너릭을 사용하지 않았을 때
public class Factory : MonoBehaviour
{
    public GameObject prefab;
    public Monster monster;
    public Orc orc;
    public Goblin goblin;
}

 

해당 방식은 간단하지만, 타입이 늘어날수록 점점 더 필드가 복잡해지고, 유지보수가 어려워진다.
이 문제를 해결하기 위해 제너릭(Generic) 을 사용한다.

 

5.1. 제너릭을 적용한 팩토리 클래스

🥕 예행 작업
1. Scene 생성 (Generic)
2. Scripts 생성 (StudyGeneric, Factory)
using UnityEngine;

public class Factory<T> : MonoBehaviour
{
    public T prefab;

    public Factory()
    {
        Debug.Log($"Factory는 {typeof(T)} 타입 입니다.");
    }
}
using UnityEngine;

public class StudyGeneric : MonoBehaviour
{
    void Start()
    {
        Factory<Monster> factory = new Factory<Monster>();
    }
}

 

이 방식은 타입별로 팩토리 클래스를 중복 정의할 필요 없이 타입만 바꿔서 유연하게 사용할 수 있다.

 

그런데!!

제너릭 타입은 컴파일 타임에 결정되기 때문에, Factory<Monster>로 만들면 해당 팩토리는 끝까지 Monster 타입만 쓸 수 있다

factory.prefab = new Orc();

 

위 코드처럼 Orc를 넣는 것은 허용되지 않는다는 뜻이다.

다시 말해 Orc는 Monster의 자식이지만, T는 Monster 타입으로 고정되어 있어 런타임에 다형성을 이용해도, 제너릭 자체는 바뀌지 않는다.

 

이처럼 제너릭은 타입 안전성과 성능을 확보해주지만, 
동적으로 여러 타입을 동시에 다뤄야 하는 경우(하나의 리스트에 Monster, Orc, Goblin을 섞어 저장 등)에는 object를 사용할 수밖에 없고 이때 박싱/언박싱이 발생할 수 있다.

 

2. 객체 지향 프로그래밍 (OOP)

현실 세계를 모형으로 모든 것을 객체(Object)로써 표현하는 프로그래밍 방식

 

  • 객체 지향 프로그래밍의 특징
    • 추상화 (확장성)
    • 상속 (다양성, 유연성)
    • 다형성 (다양성)
    • 캡슐화 (은닉성)

 

1. Overloading

동일한 함수명에 매개변수만 다르게 사용하는 방법

using UnityEngine;

public class StudyGeneric : MonoBehaviour
{
    public void CreateAccount(string name)
    {
        int dummyAge = 20;
        CreateAccount(name, dummyAge);
    }

    public void CreateAccount(string name, int age)
    {
        string dummyPhoneNumber = "01012345678";
        CreateAccount(name, age, dummyPhoneNumber);
    }

    public void CreateAccount(string name, int age, string phoneNumber)
    {
        string dummyMail = "HelloUnity@unity.com";
        CreateAccount(name, age, phoneNumber, dummyMail);
    }

    public void CreateAccount(string name, int age, string phoneNumber, string eMail)
    {
        // 계정 생성
    }
}

 

2. Inheritance (상속)

클래스가 다른 클래스를 물려받아 기존 기능을 그대로 사용하거나 새롭게 확장해서 사용하는 방법

public class Monster
{
    public void Move()
    {
        Debug.Log("몬스터가 이동합니다.");
    }
}

 

이 Monster 클래스를 상속해서 Orc, Goblin 같은 자식 클래스를 만들면

public class Orc : Monster
{
    public void Shout()
    {
        Debug.Log("오크의 분노!!");
    }
}

 

 

Orc는 Monster의 기능인 Move()를 그대로 사용 가능하면서 Shout() 같은 고유 기능도 추가할 수 있다.

 

3. Override (오버라이드)

부모 클래스의 메서드를 자식 클래스에서 재정의하는 방법

단, 반드시 부모 메서드가 가상화(virtual) 또는 추상화(abstract)로 선언되어 있어야 자식에서 override 할 수 있다.

// 가상화 예시
public class Monster
{
    public virtual void Attack()
    {
        Debug.Log("몬스터가 공격합니다!");
    }
}

public class Orc : Monster
{
    public override void Attack()
    {
        Debug.Log("오크가 도끼로 공격합니다!");
    }
}

Monster monster = new Orc(); // 생성자
monster.Attack();  // 오크가 도끼로 공격합니다!

 

  • 다형성(Polymorphism)
    • 변수는 부모 타입(Monster)으로 선언되었지만, 실제 인스턴스는 Orc이기 때문에 자식 클래스에서 재정의한 메서드가 실행된다.
    • 이처럼 실행 시점에 실제 타입의 메서드를 호출하는 것을 '다형성(Polymorphism)'이라고 한다.

 

▶ 가상화와 추상화의 차이점

구분 설명
Virtual 선택적으로 오버라이드 가능
Abstract 자식 클래스가 반드시 오버라이드 해야 함
// 추상화 예시
public abstract class Monster
{
    public abstract void Die(); // 구현 X
}

public class Orc : Monster
{
    public override void Die() => Debug.Log("오크 사망");
}

 

 


 

 

 

드디어 금요일!

오늘 수업은 유니티 실습은 하지않고 OOP 개념, 이론에 대해 공부하였다.

어제까지만 해도 유니티 실습이 이제 뭐 적응이 다 돼서 대충 하게 된다~ 막 이랬는데 오늘에서야 유니티가 천국이었음을 다시 깨닫고.. 한 두시간? 정도는 졸았던 거 같다.. ^-^.. 그래서 점심도 안 먹고 그냥 그 시간에 쿨쿨띠 😪

주말 동안 컨디션 관리 잘해서 다음 주는 졸지 않고 으쌰쌰 해보자!! 아좌좌~~🎈