NGMsoftware

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

    학습


    C# 18-2. 인터페이스와 추상 클래스. (Interface and Abstract class)

    페이지 정보

    본문

    안녕하세요. 소심비형입니다. 저번 글에 이어 오늘은 인터페이스에 대해 알아보겠습니다. 우선, 인터페이스에 대해 이해하기 위해서 상속이라는 개념에 대해 학습이 되어 있어야 합니다. 그동안 많은 예제를 통해 무의식적으로 상속을 사용해왔고, 바로 이전 글인 추상 클래스도 상속을 통해 다형성을 구현했었습니다. 물론, 인터페이스도 상속을 통해 구현되어야 하죠. 이미 상속에 대한 내용은 글을 적었으므로, 여기에서 자세히 다루지는 않겠습니다. 혹시라도 제 홈페이지에 있는 상속에 대한 글을 아직 읽어보지 않았다면 지금이라도 읽어보고 이 글을 보시는게 좋을거 같네요.

    [ 클릭: 17-6. C# 상속. (Inheritance) ]

    UHfFiLx.png

     

     

    자 이제 인터페이스만의 특징들에 대해서 알아봅시다. 일반적인 상속은 클래스 또는 추상 클래스를 상속 받는것을 말합니다. 클래스 상속은 배이스(기본, 부모 또는 슈퍼) 클래스의 기능을 디라이브(파생, 자식 또는 서브) 클래스에서 사용할 수 있습니다. 하지만, 인터페이스는 구현하는 것을 의미합니다. 인터페이스를 상속 받은 클래스는 인터페이스에 정의된 속성과 메서드를 무조건 구현해야 하는 의무가 생깁니다. 의무는 강제적입니다. 우리가 성인이 되면 무조건 군대를 가야 하는것처럼 말이죠^^;

    아래는 인터페이스의 특징입니다.

    • 인터페이스는 추상 기본 클래스와 같습니다. 인터페이스를 구현하는 클래스 또는 구조체는 모든 멤버를 구현해야 합니다.
    • 인터페이스는 직접 인스턴스화할 수 없습니다. 해당 멤버는 인터페이스를 구현하는 클래스 또는 구조체에 의해 구현됩니다.
    • 인터페이스는 이벤트, 인덱서, 메서드 및 속성을 포함할 수 있습니다.
    • 인터페이스에는 메서드의 구현이 포함되지 않습니다.
    • 클래스 또는 구조체는 여러 인터페이스를 구현할 수 있습니다.클래스는 기본 클래스를 상속할 수 있으며 하나 이상의 인터페이스를 제공할 수도 있습니다.

     

     

    프로그래밍에서 인터페이스(접점)는 규칙입니다. 프로그램 또는 모듈, 네트워크등등... 규칙을 정해두고 이 규칙을 사용하면 원하는 기능을 사용할 수 있게 되는 방식입니다. 우리 일상 생활에서 상당히 많은 인터페이스들을 볼 수 있습니다. 우리가 느끼지 못하고 있을뿐이죠^^; 아주 간단한 예로 USB가 있죠. 규칙에 맞게 만들어진 USB는 어떤 컴퓨터에 꽂아도 데이타를 읽거나 쓸 수 있습니다. 이외에도 키보드의 키들과 모니터의 HDMI등등... 많습니다. 이런 물리적인 장치들이 아닌 프로그래밍 방식은 대부분 API(Application Programming Interface)라는 이름으로 외부에 접점을 노출하고 있습니다.

    이렇게 어떤 것(예: USB 메모리)과 어떤 것(예: 노트북)이 연결되기 위한 접점을 통일된 규칙으로 정리한 것이 인터페이스입니다.

     

    그렇다면, 왜 인터페이스를 사용할까요?

    위에서 설명한것과 같이 어떤 규칙이 없다면 많은 불편함을 초래하게 됩니다. 콘센트를 예로들어보면, 현재 우리나라는 220볼트를 사용하고 있죠. 하지만 미국과 일본등 선진국에서는 110볼트를 씁니다. 한국에서 구입한 노트북을 가지고 미국에 가면 충전할 수 없게됩니다. 노트북은 110/220 모두 사용 가능하지만 콘센트 모양이 다르기 때문에 꽂을수가 없는거죠. 만약, 110볼트와 220볼트의 콘센트 모양을 통일했다면 해외 출장갈 때 굳이 전파사에 들러서 돼지코를 1,000원에 구입하지 않아도 되었을겁니다. 레고 블럭도 접점의 모양과 간격이 어떤 규칙을 가지고 있기 때문에 모양이 다른 블럭들을 조합해서 다양하게 사용 가능하도록 하고 있습니다.

     

    또, 한가지 예는 협업과 관련되어 있습니다. 자동차를 예로 들어볼까요? A 개발자는 100마력 엔진을 만들고, B 개발자는 세단 프레임을, C 개발자는 타이어 휠을 만든다고 가정해 보세요. 이 때 가장 먼저 작업이 완료 되어야 하는 사람은 아마도 프레임을 개발하는 B일겁니다. 뼈대가 만들어진 후 엔진 공간에 맞게 엔진을 제작하고, 뼈대에 맞게 휠도 만들어야 할겁니다. 여기에서 중요한 점은 A와 C는 병행 개발이 가능하지만 B는 불가능합니다. B가 뼈대를 다 만들기 전까지 A와 C는 놀아야 하는 상황이 발생되죠. 그래서 개발전에 A, B, C가 모여서 회의를 합니다. 엔진이 들어갈 자리는 이러저러 해야 하고, 휠이 들어갈 자리는 이러저러 해야 한다라고 말입니다. 그러면, 각자 작업 후 나중에 조립만 하면 되기에 좀 더 빠르고 효율적인 작업 생산성이 확보되게 됩니다.

    ge1JgOl.jpg

     

     

    이미 정의된 인터페이스가 있으므로 100마력짜리 엔진과 동일한 모양을 가진 200마력짜리 엔진을 만들수 있게되고 튜닝도 가능하게됩니다. 물론, 인터페이스만 같다면 300마력짜리 엔진을 올릴수도 있겠죠. 우리가 흔히 사용하고 있는 체크카드도 규칙만 지킨다면 누가 만들어도 ATM에서 사용할 수 있을겁니다.

     

    그렇다면 인터페이스를 어떻게 사용하는지 예제를 통해 알아보겠습니다.

    • 우선, 동영상 재생기를 만든다고 가정합니다.

    • 동영상 재생기는 동영상 코덱으로 인코딩된 모든 동영상을 재상할 수 있습니다.

    • 컨텐츠를 제작하는 업체들은 나름데로 자신들이 좋아하는(라이센스가 없거나 저렴한...) 코덱을 이용하여 컨텐츠를 생산하게 됩니다.

     

     

    이걸 코딩으로 옴겨보면 아래와 같습니다.

    Program.cs

    using System;
     
    namespace MultiInterfaceInheritance
    {
        public class Codec
        {
            public string Name
            {
                get; protected set;
            }
        }
     
        public class MP4 : Codec
        {
            public MP4()
            {
                Name = this.GetType().Name;
            }
        }
     
        public class AVI : Codec
        {
            public AVI()
            {
                Name = this.GetType().Name;
            }
        }
     
        public class MoviePlayer
        {
            public void Play(MP4 mp4)
            {
                Console.WriteLine($"{mp4.Name} 동영상을 재생하고 있습니다.");
            }
     
            public void Play(AVI avi)
            {
                Console.WriteLine($"{avi.Name} 동영상을 재생하고 있습니다.");
            }
        }
     
        class Program
        {
            static void Main(string[] args)
            {
                var moviePlayer = new MoviePlayer();
                moviePlayer.Play(new MP4());
                moviePlayer.Play(new AVI());
                Console.ReadKey();
            }
        }
    }

     

     

    실행하여 결과를 확인 해보세요. 아래 그림처럼 동영상이 잘 재생되고 있습니다.

    Hw4WWvY.png

     

     

    위 코드의 문제점이 보이시나요? 그렇습니다. 확장성이 전혀 고려되지 않은 코드입니다. 만약, 새로운 코덱이 추가된다면 MoviePlayer를 제공하는 업체로써는 상당히 귀찮은 일이 됩니다. 아래와 같이 말이죠.

    Program.cs

    using System;
    namespace MultiInterfaceInheritance
    {
        public class Codec { public string Name { get; protected set; } }
     
        public class MP4 : Codec { public MP4() { Name = this.GetType().Name; } }
     
        public class AVI : Codec { public AVI() { Name = this.GetType().Name; } }
     
        public class WMV : Codec { public WMV() { Name = this.GetType().Name; } }
     
        public class MoviePlayer
        {
            public void Play(MP4 mp4)
            {
                Console.WriteLine($"{mp4.Name} 동영상을 재생하고 있습니다.");
            }
     
            public void Play(AVI avi)
            {
                Console.WriteLine($"{avi.Name} 동영상을 재생하고 있습니다.");
            }
     
            public void Play(WMV wmv)
            {
                Console.WriteLine($"{wmv.Name} 동영상을 재생하고 있습니다.");
            }
        }
     
        class Program
        {
            static void Main(string[] args)
            {
                var moviePlayer = new MoviePlayer();
                moviePlayer.Play(new MP4());
                moviePlayer.Play(new AVI());
                Console.ReadKey();
            }
        }
    }

     

     

    물론, 이 코드도 잘 동작합니다. 하지만, 코덱이 새로 등장할 때마다 무비플레이어를 수정하고 다시 배포하는 작업은 상당히 고단하고 힘든일이 될겁니다. 아래와 같이 코드를 변경해봅시다.

    Program.cs

        public class MovieCodec { public string Name { get; protected set; } }
     
        public class SoundCodec { public string Name { get; protected set; } }

     

     

    위와 같이 사운드 코덱을 하나 추가하고, Codec의 이름을 MovieCodec으로 변경합니다. 동영상을 제작하는 업체에서 반드시 구현해야 하는 IPlayable을 추가합니다. 이로인해 동영상 컨텐츠를 제작하는 업체들은 이 규칙에 맞게 컨텐츠를 생산해야 합니다. 그렇지 않다면 최종 사용자인 시청자들에게 서비스 할 수 없게됩니다.

    Program.cs

    public interface IPlayable { bool IsConverter { get; } string Converter(); }

     

     

    위 코드를 보면 플렛폼 제공 업체인 무비플레이어가 재생 여부를 체크하기 위해 IsConverter를 확인하고, Converter를 통해 동영상을 재생하게 됩니다. 그러면 컨텐츠 제작 업체인 동영상을 만드는 회사들은 인터페이스를 명시적으로 구현해야 하겠죠. 아래 코드를 통해 어떻게 구현되는지 확인 해봅시다.

    Program.cs

        public class MP4 : MovieCodec, IPlayable
        {
            public MP4() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class MP3 : SoundCodec, IPlayable
        {
            public MP3() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class AVI : MovieCodec, IPlayable
        {
            public AVI() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class WMV : MovieCodec, IPlayable
        {
            public WMV() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
    

     

     

    위 코드의 36라인은 동영상 코덱이 아닌 사운드 코덱을 상속 받아서 구현되고 있습니다. 그렇다면, 동영상 플레이어에서 재생이 되지 않아야 하겠죠? 하지만, 인터페이스는 누구나 상속 받아서 구현이 가능하므로, 재생 가능 여부를 체크한 후 처리되어야 합니다.

    Program.cs

        public class MoviePlayer
        {
            public void Play(IPlayable movie)
            {
                if (movie.IsConverter)
                {
                    Console.WriteLine($"{movie.Converter()} 동영상을 재생하고 있습니다.");
                }
                else
                {
                    Console.WriteLine($"{movie.Converter()} 코덱은 동영상을 재상할 수 없습니다.");
                }
            }
        }

     

     

    아래는 전체 소스입니다.

    Program.cs

    using System;
     
    namespace MultiInterfaceInheritance
    {
        public interface IPlayable
        {
            bool IsConverter { get; }
            string Converter();
        }
     
        public class MovieCodec { public string Name { get; protected set; } }
     
        public class SoundCodec { public string Name { get; protected set; } }
     
        public class MP4 : MovieCodec, IPlayable
        {
            public MP4() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class MP3 : SoundCodec, IPlayable
        {
            public MP3() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class AVI : MovieCodec, IPlayable
        {
            public AVI() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class WMV : MovieCodec, IPlayable
        {
            public WMV() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class MoviePlayer
        {
            public void Play(IPlayable movie)
            {
                if (movie.IsConverter)
                {
                    Console.WriteLine($"{movie.Converter()} 동영상을 재생하고 있습니다.");
                }
                else
                {
                    Console.WriteLine($"{movie.Converter()} 코덱은 동영상을 재상할 수 없습니다.");
                }
            }
     
            class Program
            {
                static void Main(string[] args)
                {
                    var moviePlayer = new MoviePlayer();
                    moviePlayer.Play(new MP4());
                    moviePlayer.Play(new MP3());
                    moviePlayer.Play(new AVI());
                    moviePlayer.Play(new WMV());
                    Console.ReadKey();
                }
            }
        }
    }

     

     

    실행한 후 결과를 확인 해보세요. 아래 그림처럼 MP3는 동영상 플레이어에서 재생이 불가능합니다. (현실 세계에서는 재생이 가능하지만-_-;)

    y97ptzb.png

     

     

    한가지 의문이 들지 않나요? 이전 시간에 이야기한 추상 클래스와 별반 다르지 않다고 느끼실 겁니다. 그렇죠? 내부 구현만 없다뿐이지 구조는 유사합니다. 하지만, 추상 클래스보다 더 귀찮아지기만 한거 같습니다. 예를 들어 추상 클래스를 제공한 후 각각 벤더(컨텐츠 제작사들...)가 이를 상속 받아서 처리하면 벤더들도 좋겠죠? 특별히 구현해야 할 필요가 없으니 말입니다.

     

    하지만, 여기에서도 확장성이 문제가 됩니다. 만약, 동영상을 정지하기 위한 기능이 필요하다면? 또, 빨리 재생, 느리게 재생등등... 여러가지 기능이 추가될 가능성이 많습니다. 이럴때마다 플랫폼 제공 업체인 무비플레이어가 Stoppable 추상 클래스를 제공할 수는 없습니다. C#은 클래스의 경우 하나만 상속이 가능하기 때문입니다. 그렇다면, 기존의 추상 클래스를 수정하여 다시 배포해야 하는데, 이렇게되면 각각의 벤더들의 불만이 높아지겠죠. 이전 버전과의 호환성에 문제를 발생시키게 되니까요.

     

    인터페이스는 멀티 상속이 가능하며 벤더의 역량에 맞게 기능 구현이 가능합니다. 무비플레이어 입장에서는 더이상 신경쓸 필요가 없게 되는거죠. 아래처럼 IStoppable를 추가하세요.

    Program.cs

        public interface IStoppable
        {
            bool IsConverter { get; }
            string Converter();
        }

     

     

    MP4만 동영상을 재생중에 중지할 수 있는 기능이 있다고 가정합니다.

    Program.cs

        public class MP4 : MovieCodec, IPlayable, IStoppable
        {
            public MP4() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }

     

     

    MP4로 제작된 동영상 컨텐츠를 만드는 회사는 IStoppable 인터페이스만 추가로 구현하면 자신들이 원하는 서비스를 제공할 수 있게됩니다.

    Program.cs

        public class MoviePlayer
        {
            public void Play(IPlayable movie)
            {
                if (movie.IsConverter)
                {
                    Console.WriteLine($"{movie.Converter()} 동영상을 재생하고 있습니다.");
                }
                else
                {
                    Console.WriteLine($"{movie.Converter()} 코덱은 동영상을 재상할 수 없습니다.");
                }
            }
            public void Stop(IStoppable movie)
            {
                if (movie.IsConverter)
                {
                    Console.WriteLine($"{movie.Converter()} 동영상이 중지 중입니다.");
                }
                else
                {
                    Console.WriteLine($"{movie.Converter()} 코덱은 동영상 재생을 중지할 수 없습니다.");
                }
            }
        }

     

     

    이제 시청자는 무비플레이어에서 컨텐츠를 볼 수 있습니다. 또한, 어떤 컨텐츠 제공 업체냐에 따라 다양한 기능을 서비스 받을 수 있게 되죠. 아래는 전체 소스입니다.

    Program.cs

    using System;
     
    namespace MultiInterfaceInheritance
    {
        public interface IPlayable { bool IsConverter { get; } string Converter(); }
     
        public interface IStoppable { bool IsConverter { get; } string Converter(); }
     
        public class MovieCodec { public string Name { get; protected set; } }
     
        public class SoundCodec { public string Name { get; protected set; } }
     
        public class MP4 : MovieCodec, IPlayable, IStoppable
        {
            public MP4() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class MP3 : SoundCodec, IPlayable
        {
            public MP3() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class AVI : MovieCodec, IPlayable
        {
            public AVI() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class WMV : MovieCodec, IPlayable
        {
            public WMV() { Name = this.GetType().Name; }
            public bool IsConverter { get { return this is MovieCodec; } }
            public string Converter()
            {
                return this.Name;
            }
        }
     
        public class MoviePlayer
        {
            public void Play(IPlayable movie)
            {
                if (movie.IsConverter)
                {
                    Console.WriteLine($"{movie.Converter()} 동영상을 재생하고 있습니다.");
                }
                else
                {
                    Console.WriteLine($"{movie.Converter()} 코덱은 동영상을 재상할 수 없습니다.");
                }
            }
     
            public void Stop(IStoppable movie)
            {
                if (movie.IsConverter)
                {
                    Console.WriteLine($"{movie.Converter()} 동영상이 중지 중입니다.");
                }
                else
                {
                    Console.WriteLine($"{movie.Converter()} 코덱은 동영상 재생을 중지할 수 없습니다.");
                }
            }
        }
     
        class Program
        {
            static void Main(string[] args)
            {
                var moviePlayer = new MoviePlayer();
                moviePlayer.Play(new MP4());
                moviePlayer.Play(new MP3());
                moviePlayer.Play(new AVI());
                moviePlayer.Play(new WMV());
                moviePlayer.Stop(new MP4());
                Console.ReadKey();
            }
        }
    }

     

     

    실행한 후 결과를 확인 해보세요. 아래와 같이 동작하게 됩니다.

    DyNkfN6.png

     

     

    추상 클래스와 인터페이스는 어려운 개념중에 하나입니다. 왜 이렇게 해야 하는지 이해하기도 쉽지 않구요. 그렇더라도 자주 접하고 생각하고 고민하면서 코딩하다보면 어느순간 좀 더 OOP에 가까운 코딩을 하고 있는 자신을 보게 될겁니다. 위의 예제가 적절하지 못할수도 있고, 잘못된 방향으로 인도할지도 모릅니다. 그렇더라도 스스로 탐구하고 공부하다보면 실무에서 어떻게 업무 로직에 적용시킬지 감이 오는 날이 있을겁니다.

    이로써, 추상 클래스와 인터페이스에 대한 내용은 마무리하고 다음부터는 속성에 대한 내용으로 글을 적도록 하겠습니다. 사실 이 홈페이지의 많은(?) 예제속에서 속성을 사용해 왔지만, 제대로 다루지 않았기 때문이죠.

     

    다음 시간에...

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

    댓글목록

    등록된 댓글이 없습니다.