내일배움캠프 3주차 4일차 TIL - 인터페이스 파헤치기

백흰범·2024년 5월 2일
0

오늘 한 일

  • 스파르타 코딩클럽 진도 (~5-2)
  • 유니티 4기 등급 배치고사 보기
  • 집중 강의 시간 Day7 - 인터페이스 / 추상클래스


인터페이스에 대해서 (인터페이스)

인터페이스란?

인터페이스 - learn.microsoft에서의 설명

  • 인터페이스는 계약(contract)을 정의합니다. 해당 인터페이스를 상속 받은 클래스, 레코드(record), 구조체는 인터페이스에 정의된 멤버들의 구현을 해야합니다. 인터페이스는 멤버에 대한 기본 구현을 정의할 수 있습니다. 공통적인 기능에 대해 단일 구현을 제공하기 위해 static(정적) 멤버를 정의할 수도 있습니다. C# 11부터는 static abstruct 또는 static virtual 멤버를 정의할 수 있습니다.
    살짝 입맛대로 해석했다
    (! 참고로 유니티 2022.3.24f1의 버전은 C# 9이다 !)
    유니티 C# 컴파일러와 C# 9 일부 미지원 기능에 대한 설명

  • 인터페이스의 선언과 사용하는 기본적인 방법

// 인터페이스 선언 방법
Interface ISampelInterface // 앞에 I를 붙이는 게 기본적인 컨벤션이다.
{
	void SampleMethod();
}

// 인터페이스 상속 받기
class ImplementationClass : ISampleInterface
{
    // 반드시 인터페이스의 멤버들을 구현해야한다!
    void ISampleInterface.SampleMethod()
    {
        // 메소드 구현
    }

    static void Main()
    {
        // 인터페이스 인스턴스 선언
        ISampleInterface obj = new ImplementationClass();

        // 멤버호출 
        obj.SampleMethod();
    }
}

  • 인터페이스에서 정의가능한 멤버 목록

    • 상수
    • 연산자
    • 정적 생성자 (static 생성자)
    • 중첩 형식 (인터페이스 내에 클래스)
    • 정적 필드, 메서드, 속성(프로퍼티), 인덱서, 이벤트
    • 명시적 인터페이스 구현 구문을 사용한 멤버 선언
    • 명시적 액세스 한정자(기본 액세스는 public)
    • 컴파일러가 허락하는 선까지 (사실 이 지경까지 가면 이상한 거다..)

  • 정의불가능한 멤버 목록

  • 필드 (CS0525)
  • 소멸자 (CS0575)

인터페이스의 특징들

  • 기본 액세스는 public이다
  internal class Program
  {
      static void Main(string[] args)
      {
          IInterface iInterface = new Class();
          iInterface.HelloWorld();
      }
  }

  interface IInterface
  {
      void HelloWorld()
      {
          Console.WriteLine("Hello World!");
      }
  }

  public class Class : IInterface // 이미 본문 선언이 된 메서드는 강제구현 사항이 아니다.
  {	
  }
  • 출력 결과
    • 위와 같이 아무런 키워드를 붙이지 않아도 인터페이스 외부에서 꺼낼 수 있다!

클래스가 인터페이스를 상속 받을 때 지켜야하는 것

  • 인터페이스로부터 상속 받은 멤버들을 구현할 때에는 public을 붙여야 원활하게 작동된다! (interface의 기본은 public 클래스의 기본은 private)
  • 출력 결과 (상속 받은 클래스에 public을 안 붙인 경우)
  • public을 붙인경우

추가적으로 발견한 사실

  • 인터페이스에서 메서드를 정의할 때 ()에서 ; 끝맺음을 하지 않고 {}만 추가해버리면 그것도 본문 선언으로 인식하게 된다. (인터페이스에서 본문 선언이 된 메서드는 강제 구현할 필요가 없다.)
    • 보면은 HelloWorld 메서드에 대한 오류가 발생하지 않았다..
  • 본문 선언 기준을 알게 된 이유..

  • 인터페이스를 상속받은 클래스들은 인터페이스의 멤버에 override 사용이 불가능하다 (정확하게 말해서는 인식을 못한다.)
    • 인터페이스의 메서드를 구체화하는 과정의 '재정의'와
      상속 받은 클래스가 부모 클래스의 메서드를 재정의하는 '오버라이딩'과는 약간 다르다고 한다.
    • 단순히 생각해보면 인터페이스에서 본문 선언된 메서드는 virtaul의 특징을 가지고 본문이 선언되지 않은인 메서드는 abstract의 특징을 가지는 것이다. 이미 인터페이스만의 특징을 가진 상태에서 추상 및 가상 클래스에 사용되는 override를 가져다대면 컴파일러가 못 찾는 것 같다.

인터페이스의 주 사용처

  • 동일한 기능이나 특징을 가진 애들을 하나로 묶어서 특정 메서드에 확정성 있게 사용하기 위해서 사용된다.

이를 바탕으로 시험삼아 만들어본 코드 (물약얻기 및 사용 코드입니다.)

 internal class Program
 {
     static void Main(string[] args)
     {
         Player player = new Player();

         bool playing = true;
         while (playing)
         {
             Console.Clear();
             Console.WriteLine("테스트용 CS파일입니다.");
             Console.WriteLine("원하는 것을 고르세요");

             Console.WriteLine();
             Console.WriteLine("1. 스텟 보기");
             Console.WriteLine("2. 인벤토리 보기 및 사용");
             Console.WriteLine("3. 아이템 얻기");
             Console.WriteLine("0. 종료하기");

             bool isInt = int.TryParse(Console.ReadLine(), out int select);
             if (!isInt) { Console.WriteLine($"제대로 입력하세요"); continue; }
             switch (select)
             {
                 case 0:
                     Console.WriteLine("그럼 안녕히...");
                     playing = false;
                     break;
                 case 1:
                     player.ShowStatus();
                     break;
                 case 2:
                     player.UseItem();
                     break;
                 case 3:
                     player.GetItem();
                     break;
                 default:
                     Console.WriteLine($"제대로 입력하세요");
                     break;
             }
         }
     }
 }
 // 플레이어 정보
 public class Player
 {
	//인터페이스의 주요 활용처 1
     List<IItem> inventory = new List<IItem>(); // 플레이어의 가방
	
     int hp;
     public int Hp
     {
         get { return hp; }
         set { hp = value; }
     }

     int atk;
     public int Atk
     {
         get { return atk; }
         set { atk = value; }
     }

     public Player()
     {
         hp = 100;
         atk = 10;
     }

     public void ShowStatus()
     {
         Console.Clear();

         Console.WriteLine("[ 플레이어의 스텟 ]");
         Console.WriteLine($"현재 체력 : {Hp}");
         Console.WriteLine($"현재 공격력 : {Atk}");

         Console.WriteLine();
         Console.WriteLine("돌아가려면 아무키나 입력하세요");
         Console.ReadKey();
     }

     public void UseItem()
     {
         Console.Clear();

         if (inventory.Count == 0)
         { 
             Console.WriteLine(" 보유 중인 아이템이 없습니다! ");
             Console.WriteLine();
             Console.WriteLine("계속하려면 아무키나 입력하세요");
             Console.ReadKey();
         }
         else
         {
             bool selecting = true;
             while (selecting)
             {
                 Console.Clear();

                 Console.WriteLine(" 보유 중인 아이템 ");
                 for (int i = 0; i < inventory.Count; i++)
                 {
                     Console.Write(i + 1);
                     inventory[i].ShowName();
                 }

                 Console.WriteLine();
                 Console.WriteLine("사용하고 싶은 포션을 고르세요.");

                 Console.WriteLine();
                 Console.WriteLine("0. 나가기");

                 bool isInt = int.TryParse(Console.ReadLine(), out int select);
                 if (!isInt) { Console.WriteLine($"제대로 입력하세요"); continue; }
                 else if (select == 0) { break; }
                 else if (select <= inventory.Count) // 이와 같이 짜면 스위치 문처럼 case를 무진장 넓히지 않아도 되서 좋다.
                 {
                     inventory[select - 1].UseItem(this);
                     inventory.RemoveAt(select - 1);
                     Console.WriteLine("계속할려면 아무키나 입력하세요");
                     Console.ReadKey();
                 }
                 else
                 {
                     Console.WriteLine("잘못된 입력입니다!");
                     Console.WriteLine("계속할려면 아무키나 입력하세요");
                     Console.ReadKey();
                 }
             }
         }
     }

     public void GetItem()
     {
         Console.Clear();

         Console.WriteLine("# 가져가고 싶은 아이템을 고르세요! #");
         Console.WriteLine($"1. 체력 포션 - 체력을 회복합니다.");
         Console.WriteLine($"2. 근력 포션 - 공격력을 올려줍니다.");

         Console.WriteLine();
         Console.WriteLine($"0. 됐어요.");

         bool selecting = true;
         while (selecting)
         {
             bool isInt = int.TryParse(Console.ReadLine(), out int select);
             if (!isInt) { Console.WriteLine($"제대로 입력하세요"); continue; }
             switch (select)
             {
                 case 0:
                     selecting = false;
                     break;
                 case 1:
                     inventory.Add(new HealthPotion());
                     Console.WriteLine($"체력 포션 획득!");
                     break;
                 case 2:
                     inventory.Add(new StrengthPotion());
                     Console.WriteLine($"근력 포션 획득!");
                     break;
                 default:
                     Console.WriteLine($"제대로 입력하세요");
                     break;
             }
         }
     }
 }

// 아이템 인터페이스
 interface IItem
 {
     void UseItem(Player player); // 인터페이스 주요 활용처 2
     void ShowName();
 }

// 아이템을 상속받은 체력 포션
 public class HealthPotion : IItem
 {
     public string name = "체력 포션";
     public void UseItem(Player player)
     {
         player.Hp += 30;
         Console.WriteLine("30의 체력이 회복되었습니다.");
     }
     public void ShowName()
     {
         Console.WriteLine($"{name}입니다. 체력을 올려줍니다.");
     }
 }

// 아이템을 상속받은 근력 포션
 public class StrengthPotion : IItem
 {
     public string name = "근력 포션";
     public void UseItem(Player player)
     {
         player.Atk += 10;
         Console.WriteLine("10의 공격력이 올랐습니다.");
     }
     public void ShowName()
     {
         Console.WriteLine($"{name}입니다. 공격력을 올려줍니다.");
     }
 }

코드 리뷰하면 진짜 지적하고 싶은 부분이 많긴 한데..

  • 어쨌든 핵심만 짚어주자면 인벤토리아이템 사용에 대한 이 둘의 측면을 생각해보면 된다.

만약에 인벤토리에 담을 때 인터페이스를 활용하지 않고, 필드나 Array를 활용한다면 새로운 아이템이 생길 때마다 또 다시 그 부분에다가 추가적인 업데이트 사항을 수정해줘야한다.

// arr를 이용한 방식
int[] consumableItems = { 0, 0 } // 0번이 체력 포션 1번이 근력 포션일 때

만약에 인벤토리가 배열과 같은 방식인 상황에서 여기서 소모품 하나가 추가된다면
일일이 여기를 들어와서 또 추가시켜야된다. 
만약에 한 두 개 정도의 소규모 종류 추가면 몰라도 
10개 이상의 종류가 추가되버리면 이 방식의 확장성은 좋지 않는 방법이다.
(장점이라면 소모품 종류 하나의 갯수를 리스트보다는 많이 받을 수 있지만,
종류에 확장성에 한해서는 리스트 인벤토리 방식을 사용하는 것이 좋다.)
- 물론 제일 좋은 건 게임 기획의 의도에 따라서 더 간편하고 확실한 걸 선택해주면 된다.
  • 아이템 사용 면에서는 서로 같은 상속을 받지 않은 다른 클래스일 경우에는 그때마다 메서드를 추가시키고 조건문으로 분류해줘야 하는 등 뭔가의 과정을 거쳐줘야하지만, 인터페이스를 활용한다면 UseItem()으로 하나로 통일시켜서 굳이 추가적인 메서드를 만들지 않아도 바로 사용이 가능하고, 효과나 출력과 같은 구체적인 사항만 신경써주면 된다.

물론 내가 사용하는 방식보다 더 좋은 방식이 있으니 인터페이스를 잘 활용할 수 있다면 무궁무진한 시스템들을 만들어낼 수 있을 것이다.




그 외에 알게된 점

선택적 매개변수

  • void Method(int i = 0) {} 이와 같은 방식으로 매개변수에 기본값을 지정해줄 수 있는데 이러면 매개변수 전달을 선택적으로 해줄 수 있다. 지켜야할 사항은 모든 필수 매개변수 이후에다가 적어줘야하는 것이다. (CS1737)
    • 오류 사진

빅오 표기법 계산법

  • 그냥 무식하게 n이 늘어날 때마다 그만큼 실행 횟수(시간 복잡도)연산에 활용되는 메모리 사용량(공간 복잡도)이 어떻게 변화하게 되는지를 계산해주면 된다. for(int i = 0; i < n; i++) 라는 for문을 보면 n이 늘어날 때마다 for문의 실행 횟수가 똑같이 늘어나므로 시간 복잡도는 O(n)에 해당하고, 공간 복잡도를 계산할 때는 int[] arr = new int[n];와 같이 로직에서 새로운 공간을 사용하게 되면은 n이 늘어날 때마다 arr의 크기도 늘어나므로 공간 복잡도는 O(n) 그러니까 n의 요소라는 것부터 잘 찾고 그게 늘어날 때마다 어떻게 변화하는 지를 잘 보면 빅오 계산법의 원리는 이해가 될 것이다. (재귀 호출의 호출 스택 등등 이런 요소들도 초기에는 놓치기 쉽다.)



작성하면서 느낀 점

TIL을 작성하는데 너무 시간을 과하게 쓴 것 같다. 물론 아는 게 아직 부족한만큼 학습 방식이 효율적일 수 있을거라 생각은 안하지만, 차라리 새벽에 집중할 시간에 아침 시간대에 더 집중했으면 하는게 내 스스로에게 하는 바램이다.
아이템 코드를 작성해볼 때 밑바닥부터 깔면서 아.. 술술 적어지기는 하는데 막상 완성했을 때 지적할 게 한 두 가지가 아니라는 점에서 난 아직 많은 부족함을 느낀다. 뭔가 좋은 학습 방법이라는 건 밑바닥에서부터 시작하는 코딩 방법이 아니라 기존의 좋은 코드를 가져와서 해석하고 나만의 방식으로 만드는 게 어쩌면 좋은 코드를 짤 수 있는 좋은 방법이라고 생각한다.
그리고 코드를 짤 때 절대로 기획의 힘을 무시하지 말자. (어떻게 기획을 시작하느냐에 따라서 코드의 퀄리티가 천차만별로 갈리는 것 같다.)

profile
게임 개발 꿈나무

0개의 댓글