본문 바로가기

C# 일기

10. 추상 클래스(Abstract Class)

클래스 상속을 통해 여러 기능들을 상속시키면서 봉착하는 문제는 하나 더 있다.

 

9. 가상함수와 오버라이드를 다룰 때 만났던 문제는, 부모와 비슷하지만 달라질 필요가 있는 기능에 대해서 였고,

 

이를 가상함수와 오버라이드를 통해 선택적으로 부모의 특성을 그대로 물려받거나 재정의해서 사용할 수 있었다.

 

그렇다면 오늘 만나볼 문제는 무엇일까?

 

 

 

 

using System;

namespace Favor
{

   class Item
   {
      protected int index;
      protected float weight;
      public virtual void Use() { Console.WriteLine("아이템을 사용합니다."); }
   }
   
   class Portion : Item
   {
      public override void Use() { Console.WriteLine("물약을 사용해 체력이 회복됩니다."); }
   }
   
   class Elixer : Item
   {
      public override void Use() { Console.WriteLine("비약을 사용해 공격력이 상승합니다."); }
   }
   // ...
}

 

부모 클래스로 아이템을 선언했고, 아이템은 사용기능이 공통적으로 존재한다.

 

그러나 아이템이라는 클래스 자체가 굉장히 모호하고, 사용의 기능도 마찬가지로 어떤 아이템이냐에 따라 사용 효과가 크게 달라질 수 있기 때문에 가상함수로 설정하는 것이 힘들다.

 

위의 코드처럼 가상함수로 설정한다면, 어차피 부모의 가상함수 Use를 그대로 사용하는 자식 클래스는 하나도 없고, 모두가 일일히 가상 함수를 오버라이드 해야한다.

 

사용 기능이 없는 아이템도 만들 계획에 없기 때문에 영락없이 하나씩 오버라이딩 해줄 위기에 놓였다.

 

위와 같은 경우에도 유연하게 정보의 확장에 대응할 수 있는 방법은 없을까?

 

위처럼 추상적인 개념을 가상함수로 설정하는 것이 아니라, 실제로 추상적 개념을 선언하는 방법이 있는데, 추상 클래스와 추상 함수를 통해 구현할 수 있다.

 

 

 

 

먼저 추상 함수에 대해서 설명하겠다.

 

추상 함수란, 앞서 말했듯 부모가 갖고있으나, 부모 클래스에서 구체적으로 정의하기 어려운 기능을 추상화하여, 이를 자식 클래스를 통해 구체화 하여 사용할 수 있도록 정의하는 함수이다.

 

추상 클래스는, 추상 함수를 하나 이상 포함한 클래스를 의미한다.

 

추상 클래스와 추상 함수의 선언은 아래와 같다.

 

 

 

using System;

namespace Favor
{
   abstract class Item
   {
      public abstract void Use();
   }
   
   class Portion : Item
   {
      public override void Use() { Console.WriteLine("포션을 사용하여 체력이 회복됩니다."); }
   }
}

 

먼저 추상 클래스임을 알려주는 abstract 키워드를 사용하여 클래스를 선언한다.

 

abstract 키워드를 제외하면 기존의 클래스 선언 방법과 동일하다.

 

그 후, 추상 함수임을 알려주는 키워드 abstract를 통해 추상 함수를 선언한다.

 

이때는 이전에 배웠던 가상 함수의 선언에서 자료형 앞에 virtual을 붙였던 것과 유사하게, 자료형 앞에 abstract를 붙여 가상 함수로 선언한다.

 

이때 다른 점이 나타나게 된다. 추상 함수는 말 그대로 추상적인 기능을 담은 함수이기 때문에, 정의할 수 없다.

 

추상 함수는 추상 클래스에서 오직 선언만 하고, 정의는 자식 클래스에서 정의해주어야 한다.

 

이렇게 선언한 추상 함수를 자식 클래스에서 정의하는 방법은 가상 함수의 오버라이드와 동일하다.

 

가상함수 오버라이드와 동일하게 함수를 선언하고, 선언 후에 정의한다.

 

 

 

 

 

또 한 가지 다른 점이 나타나게 되는데, 가상 함수는 자식 클래스에서 재정의하지 않아도 기존의 부모 클래스의 가상 함수 기능을 그대로 사용하는 것이 가능했다.

 

그러나 추상 함수는 추상 클래스에서 오직 선언만 할 뿐, 정의하지 않기 때문에 부모 클래스에서 물려받아 사용할 기능이 존재하지 않는다.

 

따라서 추상 함수를 자식 클래스에서 정의하지 않으면, 런타임 에러가 생기게된다.

 

 

 

그렇다면 이해하기를, 추상 함수는 추상 클래스인 부모 클래스에 접근했을 때는 사용하지 않으면서, 자식 클래스에게만 존재하는 개념이나 기능을 선언하여 자식에게만 주는 것으로 이해하면 되는 것일까?

 

 

 

 

 

추상 클래스가 아닌 부모 클래스는 부모 클래스로서 객체로 존재할 수 있었다. 아래와 같이 말이다.

 

using System;

namespace Favor
{
   class Item
   {
      public void Use(){ Console.WriteLine("아이템을 사용합니다.");}
   }
   
   class Portion : Item
   {
      
   }
   
   internal class Program
   {
      static void Main(string[] args)
      {
         Item item = new Item();
         item.Use();
         // 출력값 : 아이템을 사용합니다.
      }
   }
}

 

추상 클래스가 아닌 일반 클래스는 다른 함수에서 인스턴스되어 객체로서 활동이 가능하다.

 

추상 클래스의 경우에는 어떨까?

 

 

 

 

using System;

namespace Favor
{
   abstract class Item
   {
      public abstract void Use(){ Console.WriteLine("아이템을 사용합니다.");}
   }
   
   class Portion : Item
   {
      
   }
   
   internal class Program
   {
      static void Main(string[] args)
      {
         Item item = new Item();	// 에러
         item.Use();
         // 출력값 : 없음. (Runtime Error)
      }
   }
}

 

추상 클래스는 객체가 될 수 없다. 말 그대로 추상 클래스이기 때문에 실체가 없는 것이다.

 

위에서 했던, 부모에게는 필요 없고, 자식에게만 필요한 기능들을 위해 추상 함수를 선언하는 것인가에 대한 질문은 이로서 종결될 것이다.

 

부모 클래스는 애초에 객체가 될 수 없기 때문에, 스스로 기능을 수행할 수 없기 때문이다.

 

 

 

 

 

추상 클래스는 객체가 아니라, 여러 공통된 요소들을 갖고있는 다양한 클래스를 만들기 위한 클래스 만들기 툴이라고 이해하면 될 것 같다.

 

전에 클래스를 이야기할 때 붕어빵 틀로 예시를 들었는데 이에 걸맞는 예시인 것 같다.

 

붕어빵 틀로 만든 붕어빵은 사람들에게 팔 수 있지만, 붕어빵 틀을 붕어빵 사러 온 사람들에게 팔 수는 없지 않은가?

 

 

 

 

 

 

실체도 없고, 물려주기만 할 수 있는 이 추상 클래스를 사용하는 이유는 무엇일까?

 

어차피 정의도 다 따로 달아줘야 하는거면, 그냥 일반 클래스를 부모로 지정하고 달라지는 부분을 애초에 부모 클래스에서 선언하지 않고, 맨 위의 코드처럼 포션사용함수, 비약사용함수, 붕대사용함수 등등을 개별로 선언 및 정의하면 되지 않을까? 

 

심지어 추상 클래스를 만들어버리면, 추상 클래스를 상속받는 자식 클래스들은 반드시 추상 함수를 재정의 하도록 되어있다.

 

상속받기 싫을 수도 있는데. 강제적으로 이 추상 함수를 상속시키는 추상 클래스의 존재 의의는 무엇일까?

 

 

 

 

이 질문은 질문부터 잘못되어있다. 왜냐하면, 추상 클래스를 사용하는 이유를 맨 처음부터 다루지 않았기 때문에 발생한 실수이다.

 

추상 클래스를 사용하는 목적은. 하나의 클래스를 상속받는 여러 자식 클래스들이 모두 이 메소드를 사용하게 하기 위함이다.

 

가상 함수에서는, 자식 클래스들이 가상 함수를 사용하지 않을 수도, 부모의 함수를 그대로 사용할 수도 있었다.

 

그러나 클래스를 상속시키면서 자식들이 가져야할 필수적인 요소들이 있을 것이다.

 

 

 

 

예를들어, 여러 종류의 차량을 Car라는 클래스를 상속시킨다고 해보자

 

차량이라면 가져야할 필수적인 기능은 무엇이 있을까?

 

여러가지 차량이 있을 수 있고, 또 여러가지 기능들이 존재하겠지만, 우선 차량이라면 움직여야 할 것이다.

 

차량이라는 기준으로 모두 묶었는데, 세상에 만들어졌을 때 부터 움직이지 못할 것을 가정하고 만들어진 차량은 없지 않은가?

 

따라서, 상속받는 모든 자식 클래스들이 공통적으로 가져야 할 기능들.

 

상속받은 자식 클래스들이 어떤 기능을 가지지 않으면 부모에 속해있을 수 없는 기능들을 추상 함수로 정의하여

(Car의 예에선, 움직이는 기능. 움직이지 않으면 차량이라고 볼 수 없기 때문에 Car의 자식이 아닌 것이다.)

 

모든 자식 클래스에게 상속해주기 위해 추상 클래스와 추상 함수는 존재하는 것이다. 

 

 

 

 

이 처럼, 추상 클래스의 본질에 대한 이해가 부족하다면, 추상 클래스는 왜 있는건지, 추상 함수는 왜 사용해야하는 건지에 대한 의문이 생길 수 있기 때문에, 근본부터 확실히 이해해야한다.

 

 

 

 

 

 

프로그래밍을 공부하면서, 기저에 깔아둔다면 이해에 도움이 되는 좋은 마음가짐이 있는데,

 

"내가 새롭게 배우는 개념들은 분명 기존에 사용하던 것 보다 더 유용한 기능이기에 탄생한 것이다." 라는 마음가짐이다.

 

C부터 시작하여 C++, C#으로 거쳐가면서 C에는 없던 새로운 것들이 C++에 등장하고, C++에 없던 새로운 것들이 C#에 등장하곤 했다.

 

이럴 때 마다 도대체 내가 이걸 왜 배우는건가. 기존에 사용하던 것과 이것의 차이는 무엇이며, 무엇이 도움이 되는가 라는 의문들이 반복해서 들었었다.

 

그러나 분명 쓸데없는 기능을 구태여 새로 만들지 않았을 것이며, 배워두면 분명 지금은 이해하기 힘들어도, 언젠가 이것을 사용했을 때엔 몰랐을 때와 확연히 다른 차이점을 보일 것이다 라고 생각하면, 억지로라도 그 개념의 장점을 찾을 수 있게된다.

 

이렇게 장점을 찾아나가면, 찾아낸 장점들이 "아 이래서 이걸 쓰는거구나" 라는 이해로 바뀌게 되고, 이러한 이해가 바탕이 되어 처음에는 이해하기 어렵고 사용하기 힘든 것들이 점차 익숙해져가는 것을 느낄 수 있다.

 

프로그램을 공부하는 모든 사람들이 이러한 일을 겪었으며, 앞으로도 겪게될테지만, 공부하는데 도움이 되어 나눠보았다.

 

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

12. 객체지향의 특징  (0) 2024.03.06
11. 인터페이스 (Interface)  (2) 2024.03.06
9. 가상 함수와 오버라이드 (Virtual & Override)  (0) 2024.03.06
8. 클래스 상속  (0) 2024.03.05
7. 프로퍼티(Property)  (0) 2024.03.05