NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# 17-6. C# 상속. (Inheritance)

    페이지 정보

    본문

    안녕하세요. 소심비형입니다. 오랫만에 C#에 대한 글을 작성하는군요-_-;

     

    오늘 알아볼 내용은 상속입니다. 상속은 캡슐화 및 다형성과 함께 OOP의 핵심 개념이기도 합니다. 상속을 사용하면 다른 클래스에 정의된 기능을 재사용할 수 있고, 이 클래스의 기능을 수정하거나 확장하는 새로운 클래스를 만들수도 있습니다. C#에서 상속되는 클래스를 배이스(Base) 클래스라 부르고 Java는 슈퍼(Super) 클래스라고 부릅니다. 상속 받는 클래스는 디라이브(Derived) 클래스라고 부릅니다. 보통은 파생 클래스라고 하죠. Java는 서브(Sub) 클래스라고 부릅니다.

     

    디라이브 클래스는 배이스 클래스를 하나만 가질 수 있지만 인터페이스는 제한없이 가질 수 있습니다. 상속은 가장 마지막에 위치하는 디라이브 클래스가 배이스 클래스의 기능을 사용할 수 있습니다. 만약, 이 배이스 클래스에게 배이스 클래스가 있다면 디라이브 클래스는 이 기능도 사용이 가능합니다. 좀 더 쉽게 이야기하면, C클래스가 B를 상속받고, B가 A를 상속 받았다면 C는 B와 A의 기능을 모두 사용할 수 있습니다.

     

    그림으로 그려보면 아래와 같이 표현할 수 있습니다.

    BaHlQie.png

     

     

    위 그림에서 동물과 조류는 abstract 키워드를 사용하여 추상 클래스임을 나타내고 있습니다. 추상 클래스는 구체화되지 않은 추상적인 개념을 나타내며 상속만을 위해 존재하는 클래스입니다. 이번 예제에서 사용되지는 않았지만, C#에서는 기능적으로 완벽하거나(더이상 확장이 필요없는) 또는 기능을 확장할 경우 문제가 예상될 때 상속을 금지하는 sealed(봉인) 키워드를 제공합니다. Java는 final(최종) 키워드를 제공하고 있습니다.

     

    위의 그림을 코드로 표현하면 아래와 같습니다.

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
     
    namespace ConsoleApplication1
    {
        class Inheritance { }
     
        enum Sex { 수컷, 암컷 }
     
        enum Species { 개과, 고양이과 }
     
        abstract class 동물
        {
            public Sex 성별 { get; set; }
            public Species 종 { get; set; }
            public string 이름 { get; set; }
     
            public abstract void 달리다(); public abstract void 짖다();
        }
     
        class 호랑이 : 동물
        {
            public Color 털색 = Color.Yellow;
     
            public Color 줄무늬색 = Color.Black;
     
            public override void 달리다() { }
     
            public override void 짖다()
            {
                Console.WriteLine("으르렁~");
            }
        }
        class 백호랑이 : 호랑이
        {
            public new Color 털색 = Color.White;
        }
     
        class 늑대 : 동물
        {
            public override void 달리다() { }
     
            public override void 짖다()
            {
                Console.WriteLine("아우~");
            }
        }
     
        abstract class 조류 : 동물 { }
     
        class 닭 : 조류
        {
            public Color 깃털색 = Color.White;
     
            public override void 달리다()
            {
                Console.WriteLine("두발로 달린다.");
            }
     
            public override void 짖다()
            {
                Console.WriteLine("꼬꼬댁~");
            }
        }
     
        class 병아리 : 닭
        {
            public new Color 깃털색 = Color.Yellow; public override void 짖다()
            {
                Console.WriteLine("삐약 삐약~");
            }
        }
    }

     

     

    조금 복잡해 보이기는 하나 상속 관계를 생각하면서 하나씩 보면 그리 어렵지 않게 이해할 수 있을거라 생각합니다. 위의 코드를 사용하여 응용 프로그램을 실행합니다. 아래는 "동물"로부터 파생된 클래스들의 사용 방법을 설명하고 있습니다.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
     
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main()
            {
                백호랑이 백호 = new 백호랑이();
                백호.털색 = System.Drawing.Color.White;
                백호.성별 = Sex.수컷;
                백호.짖다();
     
                Console.WriteLine(백호.털색.Name);
                Console.WriteLine(백호.줄무늬색.Name);
                Console.WriteLine();
     
                호랑이 백두산호랑이 = (호랑이)백호;
                백두산호랑이.털색 = System.Drawing.Color.Yellow;
                백두산호랑이.짖다();
     
                Console.WriteLine(백두산호랑이.털색.Name);
                Console.WriteLine(백두산호랑이.줄무늬색.Name);
                Console.WriteLine();
     
                닭 하림닭 = new 닭();
                하림닭.달리다();
                하림닭.짖다();
                병아리 얄리 = new 병아리();
                얄리.달리다();
                얄리.짖다();
     
                Console.WriteLine();
     
                List<동물> 동물들 = new List<동물>()
                {
                    백호, 백두산호랑이, 하림닭, 얄리
                };
     
                foreach (var 동물 in 동물들)
                {
                    동물.짖다();
                }
     
                Console.ReadKey();
            }
        }
    }

     

     

    배이스 클래스로부터 상속받은 속성 및 기능과 디라이브 클래스에서 재정의한 속성과 기능을 사용합니다. 모든 클래스는 "동물"에서 파생(Derived)되었으므로 44라인과 같이 배이스 클래스의 기능으로 사용할 수도 있습니다.

    qNbIOu4.png

     

     

    "조류" 클래스를 상속하여 "" 클래스를 정의하는 경우 이 클래스는 암시적으로 생성자와 소멸자를 제외한 배이스 클래스의 모든 맴버를 가집니다. 따라서 "" 클래스는 "조류" 클래스의 코드를 다시 구현하지 않고 재사용할 수 있게 됩니다. 물론, "" 클래스를 상속받아 구현된 "병아리"클래스는 "조류"와 ""의 모든 맴버를 가집니다. 또한, "병아리" 클래스는 "" 클래스의 기능을 변경하였습니다.

     

    여기에서 심도있게 다룰 내용은 아니지만, 위의 예제에서는 추상(Abstract) 및 가상(Virtual) 메소드를 이용하고 있습니다. 간단하게 설명하자면 배이스 클래스가 추상 클래스이거나 맴버가 추상 메소드라면 이를 상속받는 모든 디라이브 클래스들은 무조건 구현해야 합니다. 만약, 가상 메소드라면 재정의(Override)할 수도 있고 명시적으로 구현하지 않아도 됩니다. 추상 및 가상 맴버는 OOP의 주요 특징인 다형성의 기반이 되는 중요한 개념입니다.

     

    "동물" 클래스는 추상 클래스입니다. new 키워드를 사용하여 직접 인스턴스화하지 않으려는 의도로 추상 클래스를 선언합니다. 이렇게하면 "동물" 클래스는 파생되는 클래스에서만 사용할 수 있게됩니다. 추상 클래스를 만드는 이유는 직접 사용할 수 없게 하기 위함이며, 추상적인 개념을 사용하는 것은 비지니스가 명확하지 않다는 의미로 반드시 이 클래스를 상속하여 기능을 명확하게 구현하여 사용해야 하기 때문입니다.

     

    아래는 상속 관계에 있는 클래스들의 생성(public 클래스 이름: 생성자)과 소멸(~: 소멸자)의 과정을 최대한 단순화하여 보여주고 있습니다.

    using System;
     
    namespace Inheritance
    {
        class Base
        {
            protected string Name; public Base(string Name)
            {
                this.Name = Name;
                Console.WriteLine("{0}.Base()", this.Name);
            }
     
            ~Base()
            {
                Console.WriteLine("{0}.~Base()", this.Name);
            }
     
            public void BaseMethod()
            {
                Console.WriteLine("{0}.BaseMethod()", Name);
            }
        }
     
        class Derived : Base
        {
            public Derived(string Name) : base(Name)
            {
                Console.WriteLine("{0}.Derived()", this.Name);
            }
     
            ~Derived()
            {
                Console.WriteLine("{0}.~Derived()", this.Name);
            }
     
            public void DerivedMethod()
            {
                Console.WriteLine("{0}.DerivedMethod()", Name);
            }
        }
     
        class MainApp
        {
            static void Main(string[] args)
            {
                Base a = new Base("a");
                a.BaseMethod();
                Derived b = new Derived("b");
                b.BaseMethod();
                b.DerivedMethod();
            }
        }
    }

     

     

    위 코드를 실행한 결과 화면으로 생성자와 소멸자가 실행되는 순서를 확인할 수 있습니다. C#에서는 C++과 달리 다중 상속을 허용하지 않습니다. 또한, 생성자와 소멸자는 상속되지 않습니다. 배이스 클래스의 기본 생성자가 묵시적으로 수행될 뿐입니다.

    2r2IO3e.png

     

     

    만약, 배이스 클래스의 특정 생성자를 호출하려면 명시적으로 호출해야 합니다. 여기서 자신은 this를 사용하며, 배이스 클래스는 base로 지칭합니다. 아래 예제의 31라인에서 명시적으로 배이스 클래스의 생성자를 호출하고 있습니다.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
     
    namespace NestedClass
    {
        public class Base
        {
            protected string id; public string FirstName { get; set; }
     
            public string LastName { get; set; }
     
            // 기본 생성자.        
            public Base() { }
     
            // 베이스 클래스의 생성자.        
            public Base(string id, string firstName, string lastName)
            {
                this.id = id;
                this.FirstName = firstName;
                this.LastName = lastName;
            }
        }
        public class Derived : Base
        {
            public string FullName { get; set; }
     
            // 베이스 클래스의 생성자 호출.        
            public Derived(string id, string firstName, string lastName) : base(id, firstName, lastName)
            {
                this.FullName = string.Format("{0}{1}", firstName, lastName);
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                Derived 파생클래스 = new Derived("ngmaster", "소심", "비형");
                Console.WriteLine($"FirstName: {파생클래스.FirstName}");
                Console.WriteLine($"LastName: {파생클래스.LastName}");
                Console.WriteLine($"FullName: {파생클래스.FullName}");
                Console.ReadKey();
            }
        }
    }

     

     

    C#은 파생되는 클래스 뒤에 콜론(:)으로 배이스 클래스와 인터페이스를 상속 받습니다. Java는 클래스 뒤에 extends 키워드를 사용하여 클래스를 상속 받고, implements 키워드로 인터페이스를 상속 받습니다. 사실, 상속에 대해 이야기를 한다면, 곁다리로 짚고 넘어가야 할 수많은 내용들이 있습니다. 상속은 추상화와 다형성처럼 개체 지향 프로그래밍의 개념적인 내용 및 SOLID의 LSP(리스코프 치환의 원칙: The Liskov Substitution Principle)와 DIP(의존성 역전의 원칙: Dependency Inversion Principle)와도 관계가 있습니다. 언젠가는 하나씩 알아볼 내용이지만, 상속을 하면서 이런 주제들을 모두 다루기에는 배보다 배꼽이 더 커질 우려가 있으므로 다음 시간에 따로 알아보도록 하겠습니다.

     

    궁금하신분들은 인터넷에 검색해보면 정말 많은 자료들과 좋은 설명들이 많으므로 찾아보시는것도 좋겠네요^^;

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.