[이것이 C#이다] 8. 인터페이스와 추상 클래스

ssu_hyun·2022년 4월 13일
0

C#

목록 보기
12/22

Key point

  • 인터페이스
  • 인터페이스 상속
  • 추상 클래스

8.1 인터페이스(Interface)

  • 클래스의 청사진
    • 클래스가 해야하는 행동을 결정
      = 클래스가 어떤 메소드를 가질지 결정
  • 선언 : interface 키워드 이용
  • 대개 I로 시작하는 이름으로 명명
  • 메소드 구현, 필드 X
  • 메소드, 이벤트, 인덱서, 프로퍼티만을 가질 수 있다.
  • 접근 제한 한정자를 사용할 수 없고 모든 것이 public으로 선언됨
  • 인스턴스를 가질 수는 없지만, 인터페이스를 상속받는 클래스의 인스턴스를 만드는 것은 가능하다. 참조를 만들어 여기에 파생 클래스의 객체 위치를 담는 것
  • 파생 클래스는 기반 클래스와 같은 형식으로 간주한다
    = ConsoleLogger의 객체는 ILogger의 객체로 취급할 수 있다.

    예제 프로그램

   // 인터페이스
   interface ILogger
   {
       void WriteLog(string message);
   }
   
   // 파생 클래스
   class ConsoleLogger : ILogger  // 상속
   {
       public void WriteLog(string message)  // WriteLog() 메소드 구현 & public 한정자 수식 강제
       {
           Console.WriteLine(
                    "{0} {1}",
                    DateTime.Now.ToLocalTime(), message);
        }
    }
    
    // 파생 클래스의 인스턴스화
    ILogger logger = new ConsoleLogger();
    logger.WriteLog("Hello, World!");

8.2 인터페이스는 약속이다

  • 인터페이스는 클래스가 따라야 하는 약속
    • 인터페이스의 파생 클래스는 인터페이스에 선언된 ① 모든 메소드(및 프로퍼티)를 구현해줘야 하며 이 메소드들은 public 한정자로 수식해야 한다.
  • 상속을 통한 connect 역할
    • 어떤 클래스든 인터페이스를 위의 약속을 지켜 상속받아 구현하면 인터페이스 즉 기반클래스(부모 클래스)의 역할을 할 수 있다.
    • 기반 클래스(부모) ≒ 파생 클래스(자식)이 성립되는 것

예제 프로그램 - 콘솔에 로그 출력

using System;
using System.IO;

namespace Interface
{
	// 기반 클래스 : 인터페이스
   interface ILogger
   {
       void WriteLog(string message);
   }

   // 파생 클래스1
   class ConsoleLogger : ILogger
   {
       public void WriteLog(string message) // 약속 : WriteLog(), public 한정자 구현
       {
           Console.WriteLine(
                    "{0} {1}",
                    DateTime.Now.ToLocalTime(), message);
       }
   }

   class ClimateMonitor
   {
       private ILogger logger;  // ILogger 참조를 필드로 지님
       public ClimateMonitor(ILogger logger)  // 필드 통해 초기화
       {
           this.logger = logger;
       }

       public void start()
       {
           while (true)
           {
               Console.Write("온도를 입력해주세요. : ");
               string temperature = Console.ReadLine();  // 온도 입력받기
               if (temperature == "")
                   break;

               logger.WriteLog("현재 온도 : " + temperature);
           }
       }
   }

   class MainApp
   {
       static void Main(string[] args)
       {
           // ConsoleLogger 객체를 생성자 인수로 넘겨 "콘솔에 메시지 출력"
           ClimateMonitor monitor = new ClimateMonitor(new ConsoleLogger());
           monitor.start();
       }
   }
} 

예제 프로그램 - 텍스트파일에 로그 출력

using System;
using System.IO;

 // 기반 프로그램 : 인터페이스
 namespace Interface
{
    interface ILogger
    {
        void WriteLog(string message);
    }

    // 파생 클래스1
    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine(
                     "{0} {1}",
                     DateTime.Now.ToLocalTime(), message);
        }
    }

    // 파생 클래스2
    class FileLogger : ILogger
    {
        private StreamWriter writer;

        public FileLogger(string path)
        {
            writer = File.CreateText(path);
            writer.AutoFlush = true;
        }

        public void WriteLog(string message)
        {
             writer.WriteLine("{0} {1}", DateTime.Now.ToShortTimeString(), message);
        }
    }

    class ClimateMonitor
    {
        private ILogger logger;  // ILogger 참조를 필드로 지님
        public ClimateMonitor(ILogger logger)  // 필드 통해 초기화
        {
            this.logger = logger;
        }

        public void start()
        {
            while (true)
            {
                Console.Write("온도를 입력해주세요. : ");
                string temperature = Console.ReadLine();  // 온도 입력받기
                if (temperature == "")
                    break;

                logger.WriteLog("현재 온도 : " + temperature);
            }
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            // FileLogger 객체를 생성자 인수로 넘겨 "파일에 로그 기록"
            ClimateMonitor monitor = new ClimateMonitor(new FileLogger("MyLog.txt"));
            monitor.start();
        }
    }
}

  • cmd에서 txt파일 한글깨짐 없이 여는 법
    • type 명령어 통해 파일 읽기
    • chcp(CHange Code Pages) : cmd 상의 언어를 바꾸는 명령어
    • 한글코드인 949에서 한글이 깨지는 문제가 발생하므로 유니코드 65001 설정
      *유니코드 : 모든 언어를 표시할 수 있는 코드

예제 프로그램 - 상속을 통한 class connect 역할

using System;
using System.IO;

namespace Interface
{
    interface ILogger
    {
        void WriteLog(string message);
    }


    /* ConsoleLogger와 FileLogger는 ILogger를 상속하며, WriteLog()메소드를 구현함으로써
       기반클래스(부모) ≒ 파생클래스(자식)로 간주되어 
       ILogger 인터페이스를 통해 다른 클래스(ClimateMonitor)와 연결되어 소통한다. */


    // 파생 클래스1
    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine(
                     "{0} {1}",
                     DateTime.Now.ToLocalTime(), message);
        }
    }

    // 파생 클래스2
    class FileLogger : ILogger
    {
        private StreamWriter writer;

        public FileLogger(string path)
        {
            writer = File.CreateText(path);
            writer.AutoFlush = true;
        }

        public void WriteLog(string message)
        {
             writer.WriteLine("{0} {1}", DateTime.Now.ToShortTimeString(), message);
        }
    }

    class ClimateMonitor
    {
        private ILogger logger; 
        public ClimateMonitor(ILogger logger)  
        {
            this.logger = logger;
        }

        public void start()
        {
            while (true)
            {
                Console.Write("온도를 입력해주세요. : ");
                string temperature = Console.ReadLine();  
                if (temperature == "")
                    break;

                logger.WriteLog("현재 온도 : " + temperature);
            }
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            /* [ class connect ]
               monitor 객체는 인터페이스를 통해 연결된 FileLogger를 통해
               애플리케이션이 시작된 디렉터리에 MyLog.txt 파일을 만들고 여기에 로그를 남긴다. */

            ClimateMonitor monitor = new ClimateMonitor(new FileLogger("MyLog.txt"));

            monitor.start();
        }
    }
}

[비타민 퀴즈]
ClimateMonitor의 logger가 ConsoleLogger의 객체를 가리킬 경우 실행 결과

ClimateMonitor monitor = new ClimateMonitor(new ConsoleLogger());


8.3 인터페이스를 상속하는 인터페이스

  • 인터페이스를 상속할 수 있는 것
    • 클래스
    • 구조체
    • 인터페이스
  • 인터페이스를 수정할 수 없어 반드시 상속해야 하는 경우
    • 상속하려는 인터페이스가 소스 코드가 아닌 어셈블리로만 제공되는 경우 (.NET SDK에서 제공하는 인터페이스들)
    • 상속하려는 인터페이스의 소스 코드를 갖고 있어도 이미 인터페이스를 상속하는 클래스들이 존재하는 경우
      • 이미 파생 클래스를 만든 경우 인터페이스를 수정하면 나머지 파생 클래스가 구현한 기반 클래스의 메소드와 프로퍼티를 수정해야하므로 상속을 하는 것이 낫다.
  • 선언 : 상속 문법과 동일
    interface 파생 인터페이스 : 부모 인터페이스
    {
        // ... 추가할 메소드 목록
    }

    파생 인터페이스는 기반 인터페이스에 선언된 모든 것을 그대로 물려받는다는 점을 잊지 말것

예제 프로그램

using System;

namespace DerivedInterface
{	
	// 기반 인터페이스
    interface ILogger
    {
        void WriteLog(string message);
    }
	
    // 상속1 : 파생 인터페이스
    interface IFormattableLogger : ILogger
    {
        void WriteLog(string format, params Object[] args);
    }
	
    // 상속2 : 파생 클래스
    class ConsoleLogger2 : IFormattableLogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine(
                $"{DateTime.Now.ToLocalTime()}, {message}");
        }

        public void WriteLog(string format, params Object[] args)
        {
            String message = String.Format(format, args);
            Console.WriteLine(
                $"{DateTime.Now.ToLocalTime()}, {message}");
        }
    }
	
    // 실행
    class MainApp
    {
        static void Main(string[] args)
        {
            IFormattableLogger logger = new ConsoleLogger2();
            logger.WriteLog("The world is not flat.");
            logger.WriteLog("{0} + {1} = {2}", 1, 1, 2);
        }
    }
}


8.4 여러 개의 인터페이스, 한꺼번에 상속하기

  • 클래스는 프로그램의 모호성을 초래하는 "죽음의 다이아몬드"문제로 인해 C#에서는 클래스의 다중 상속을 허용하지 않는다.
  • 인터페이스의 다중 상속은 허용 (내용이 아닌 외형을 물려줌)

예제 프로그램

using System;

namespace MultiInterfaceInheritance
{
	// 인터페이스1
    interface IRunnable
    {
        void Run();
    }

	// 인터페이스2
    interface IFlyable
    {
        void Fly();
    }

	// 인터페이스 다중 상속 클래스
    class FlyingCar : IRunnable, IFlyable
    {
        public void Run()
        {
            Console.WriteLine("Run! Run!");
        }

        public void Fly()
        {
            Console.WriteLine("Fly! Fly!");
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            FlyingCar car = new FlyingCar();
            car.Run();
            car.Fly();

            IRunnable runnable = car as IRunnable;  // 기반 인터페이스로 형식 변환
            runnable.Run();

            IFlyable flyable = car as IFlyable;  // 기반 인터페이스로 형식 변환
            flyable.Fly();
        }
    }
}

그래도 여러 클래스로부터 구현을 물려받고 싶다면?

  • 포함(Containment)기법
    : 클래스 안에 물려받고 싶은 기능을 가진 클래스들을 필드로 선언해 넣는 것
MyVehicle()
{
    Car car = new Car();
    Plane plane = new Plane();    
    public void Fly() { plane.Ride(); }
    public void Run() { car.Ride(); }
}

8.5 인터페이스의 기본 구현 메소드

  • 기본 구현 메소드 : 구현부를 가지는 메소드

    • 인터페이스에 새로운 메소드를 추가한 뒤 기본적인 구현체를 갖도록 하면 파생 클래스에서의 컴파일 에러를 막을 수 있다.
    // ILogger의 모든 파생 클래스에 대해 컴파일 에러를 발생시키는 경우 //
    indterface ILogger
    {
    	void WriteLog(string message);
        void WriteError(string error);  // 새로운 메소드 추가 : 컴파일 에러 발생
    }
    
    
    // 컴파일 에러 막는 법 : 기본 구현 메소드 //
    indterface ILogger
    {
    	void WriteLog(string message);
        void WriteError(string error)  // 다른 기존 코드에 아무런 영향 끼치지 않음
        {
            WriteLog($"Error : {error}");  // WriteError()에 기본 구현 제공
        }
    }

예제 프로그램

using System;

namespace DefaultImplementation
{
	// 기반 인터페이스
    interface ILogger
    {
        void WriteLog(string message);

        void WriteError(string error) // 새로운 메소드 추가
        {
            WriteLog($"Error: {error}");  // 기본 구현
        }    
    }
	
    // 파생 인터페이스
    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine(
                $"{DateTime.Now.ToLocalTime()}, {message}");
        }
    }
	
    // 실행
    class MainApp
    {
        static void Main(string[] args)
        {
            ILogger logger = new ConsoleLogger();
            logger.WriteLog("System Up"); //  OK
            logger.WriteError("System Fail");  // 기본 구현 메소드로 인해 OK

            ConsoleLogger clogger = new ConsoleLogger();
            clogger.WriteLog("System Up"); //  OK
            // clogger.WriteError("System Fail"); 
            // 컴파일 에러 : ConsoleLogger가 Writerror() 오버라이딩X
        }
    }
}
  • 인터페이스에 선언된 기본 구현 인터페이스는 파생 클래스의 참조로 호출할 수 없다.

8.6 추상 클래스 : 인터페이스와 클래스 사이

  • 메소드의 구현 가질 수 있음(=클래스)

    • 추상 메소드(Abstract Method) : 추상 클래스에서 구현을 가지지 않는 메소드
    • 추상 클래스의 파생 클래스는 ① 추상 메소드를 반드시 구현해야하며 (인터페이스 역할) ② public, protected, internal, protected internal 한정자 중 하나로 수식될 것 강조
  • 객체 생성 불가(=인터페이스)

  • 인터페이스를 제공하되 기본적인 구현을 함께 제공하고 싶을 경우 사용

    • 파생 클래스가 구현해야할 메소드 정의 강제와 기본적인 구현 모두 하고 싶을 때
  • 추상 클래스는 또 다른 추상 클래스를 상속할 수 있다.

    • 이 경우 자식 추상 클래스는 부모 추상 클래스의 추상 메소드를 구현하지 않아도 된다.
  • 사용 목적 : 내가 만든 추상 클래스를 이용할 때 이에 대한 규칙/약속 강제

  • 선언

    // 문법 //
    abstract class 클래스이름
    {
        // 클래스와 동일하게 구현
    }
    
    
    // 추상 메소드 선언 예 //
    abstract class AbstractBase  // 추상 클래스
    {
        public abstract void SomeMethod();  // 추상 메소드 : abstract 한정자 이용
    }
    
    class Derived : AbstractBase
    {
        public override void SomeMethod()
        {
            // Something
        }
    }

예제 프로그램

using System;

namespace AbstractClass
{
    abstract class AbstractBase  // 추상 클래스
    {
        protected void PrivateMethodA()
        {
            Console.WriteLine("AbstractBase.PrivateMethodA()");
        }

        public void PublicMethodA()
        {
            Console.WriteLine("AbstractBase.PublicMethodA()");
        }

        public abstract void AbstractMethodA();  // 추상 메소드
    }

	// 추상 클래스 상속
    class Derived : AbstractBase
    {
    	// 추상 메소드 구현 & 한정자 
        public override void AbstractMethodA()
        {
            Console.WriteLine("Derived.AbstractMethodA()");
            PrivateMethodA();
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            AbstractBase obj = new Derived();
            obj.AbstractMethodA();
            obj.PublicMethodA();
        }
    }
}


연습 문제

  1. 인터페이스와 클래스가 다른 점은 무엇입니까?
    : 인터페이스는 클래스와 달리 내부에 메서드(함수)와 프로퍼티, 인덱서만 선언 가능하며, new 키워드를 통해 인스턴스화 할 수 없다. 또한 접근 지정자가 기본적으로 public으로 설정되어 있다. 따라서 해당 인터페이스를 상속받는 클래스를 만들어, 업 캐스팅 형식의 참조를 통해 인터페이스를 활용해야 한다.

    인터페이스는 하나의 약속이다. 인터페이스를 상속받는 모든 파생 클래스들은 인터페이스에 선언되어 있는 함수를 무조건 정의해야 한다. 따라서 어떤 프로그래머가 해당 클래스가 어떤 인터페이스를 상속받고 있는지만 알고 있어도, 해당 클래스가 어떤 기능을 하는지 유추할 수 있다.

  2. 인터페이스와 추상 클래스가 다른 점은 무엇입니까?
    : 추상클래스는 인터페이스와 기본적으로 역할(함수의 정의를 강요)은 비슷하지만, 인터페이스와 달리 데이터(변수)를 선언할 수 있다. 또한 필요에 의해 함수를 정의해도 괜찮다. 추상 클래스에는 해당 클래스에서만 사용 가능한 추상 메서드라는 것이 존재하는데, 이는 인터페이스의 메서드와 같은 역할을 한다. 하지만 모든 추상 클래스 내의 메소드와 데이터는 private으로 설정되어 있기 때문에, 추상 메서드의 접근 지정자를 public으로 설정하는 것을 추천한다.

0개의 댓글