인터페이스와 추상클래스

상태
완료
담당자
날짜
숫자
0
저번에 우리는 클래스에 대한 개념과 상속, 오버라이드, 오버로드에 대해 학습했습니다. 이번에는 저번에 배운 내용에서 더 나아가 인터페이스와 추상 클래스에 대해 알아보도록 하겠습니다.

인터페이스란?

인터페이스는 객체 지향 프로그래밍에서 중요한 개념으로, 클래스가 특정 동작을 구현하도록 강제하는 계약을 정의합니다. 인터페이스는 메서드, 속성, 이벤트, 인덱서의 집합을 포함할 수 있으며, 이를 구현하는 클래스는 인터페이스에 정의된 모든 멤버를 반드시 구현해야 합니다.
그렇다면 어떤 경우에 인터페이스를 사용해야 할까요?

1. 여러 클래스가 동일한 동작을 구현해야 할 때

만약 여러분이 게임 캐릭터들이 공통적으로 이동하고 공격하는 동작을 가지고 있는 클래스를 만든다고 가정합시다.
// ICharacter 인터페이스 정의 public interface ICharacter { void Move(); // 이동 메서드 void Attack(); // 공격 메서드 } // 전사 클래스는 ICharacter 인터페이스를 구현합니다. public class Warrior : ICharacter { // Move 메서드 구현 public void Move() { Console.WriteLine("전사가 이동합니다."); } // Attack 메서드 구현 public void Attack() { Console.WriteLine("전사가 검으로 공격합니다."); } } // 마법사 클래스는 ICharacter 인터페이스를 구현합니다. public class Wizard : ICharacter { // Move 메서드 구현 public void Move() { Console.WriteLine("마법사가 이동합니다."); } // Attack 메서드 구현 public void Attack() { Console.WriteLine("마법사가 주문을 시전합니다."); } } class Program { static void Main() { // ICharacter 인터페이스 타입으로 객체 생성 ICharacter warrior = new Warrior(); ICharacter wizard = new Wizard(); // Move와 Attack 메서드 호출 warrior.Move(); warrior.Attack(); wizard.Move(); wizard.Attack(); } }
C#
복사
위 코드를 보면 ICharacter라는 인터페이스를 정의하고 그 인터페이스의 메서드는 Move(), Attack()이 있습니다. 그리고 Warrior, Wizard 클래스는 ICharacter 인터페이스를 구현하여 그 메서드인 Move()와 Attack() 메서드를 구현하도록 강제됩니다.
이처럼 여러 클래스가 동일한 메서드를 구현해야 하는 경우, 인터페이스를 사용하면 공통된 동작을 명확히 정의하고 강제할 수 있습니다.

2. 클래스가 여러 인터페이스를 구현해야 할 때

여러분이 게임 캐릭터를 만들 때, 이동, 공격 외에도 대화 기능을 구현하려 한다고 가정해봅시다.
// ICharacter 인터페이스 정의 public interface ICharacter { void Move(); // 이동 메서드 void Attack(); // 공격 메서드 } // ITalkable 인터페이스 정의 public interface ITalkable { void Talk(); // 대화 메서드 } // 전사 클래스는 ICharacter와 ITalkable 인터페이스를 구현합니다. public class Warrior : ICharacter, ITalkable { // Move 메서드 구현 public void Move() { Console.WriteLine("전사가 이동합니다."); } // Attack 메서드 구현 public void Attack() { Console.WriteLine("전사가 검으로 공격합니다."); } // Talk 메서드 구현 public void Talk() { Console.WriteLine("전사가 대화합니다."); } } // 마법사 클래스는 ICharacter와 ITalkable 인터페이스를 구현합니다. public class Wizard : ICharacter, ITalkable { // Move 메서드 구현 public void Move() { Console.WriteLine("마법사가 이동합니다."); } // Attack 메서드 구현 public void Attack() { Console.WriteLine("마법사가 주문을 시전합니다."); } // Talk 메서드 구현 public void Talk() { Console.WriteLine("마법사가 대화합니다."); } } class Program { static void Main() { // ICharacter 인터페이스 타입으로 객체 생성 ICharacter warrior = new Warrior(); ICharacter wizard = new Wizard(); // Move와 Attack 메서드 호출 warrior.Move(); warrior.Attack(); wizard.Move(); wizard.Attack(); // ITalkable 인터페이스 타입으로 객체 생성 ITalkable talkableWarrior = new Warrior(); ITalkable talkableWizard = new Wizard(); // Talk 메서드 호출 talkableWarrior.Talk(); talkableWizard.Talk(); } }
C#
복사
이 상황처럼 클래스가 여러 기능을 가져야 할 때, 인터페이스를 사용하면 각 기능을 독립적으로 정의할 수 있으며, 클래스는 필요한 인터페이스를 여러 개 구현하고 다양한 기능을 가질 수 있습니다. 또한 기능 확장이 필요한 경우에도 새로운 기능을 인터페이스로 정의하고 클래스에서 이를 구현하여 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있습니다. 이를 통해 코드의 재사용성과 확장성을 높일 수 있습니다.

인터페이스와 메서드 오버라이드의 차이점

저는 인터페이스에 대해 학습할 때, ‘인터페이스는 사실상 오버라이딩과 별 차이가 없는게 아닐까?’ 라고 생각했습니다. 왜냐하면 메서드 오버라이딩을 사용하여 이를 구현할 때, ICharacter라는 부모 클래스를 만들고 자식 클래스인 Warrior와 Wizard 클래스에서 재정의하면 되지 않을까 라는 생각을 했습니다.
인터페이스와 메서드 오버라이드는 개념적으로는 비슷해 보일 수 있지만, 목적과 사용 방법에 있어서 중요한 차이점이 있습니다. 위에서 나온 인터페이스를 통해 구현된 클래스를 아래의 코드를 알아보고 둘의 차이점을 명시해 보겠습니다.
메서드 오버라이딩을 사용한 코드
// 부모 클래스 정의 public class Character { public virtual void Move() { Console.WriteLine("캐릭터가 이동합니다."); } public virtual void Attack() { Console.WriteLine("캐릭터가 공격합니다."); } } // 전사 클래스는 Character 클래스를 상속받고 Move와 Attack 메서드를 오버라이드 public class Warrior : Character { public override void Move() { Console.WriteLine("전사가 이동합니다."); } public override void Attack() { Console.WriteLine("전사가 검으로 공격합니다."); } } // 마법사 클래스는 Character 클래스를 상속받고 Move와 Attack 메서드를 오버라이드 public class Wizard : Character { public override void Move() { Console.WriteLine("마법사가 이동합니다."); } public override void Attack() { Console.WriteLine("마법사가 주문을 시전합니다."); } } class Program { static void Main() { Character warrior = new Warrior(); Character wizard = new Wizard(); warrior.Move(); warrior.Attack(); wizard.Move(); wizard.Attack(); } }
C#
복사
먼저 인터페이스는 정의할 때 메서드의 구현 없이 메서드 시그니처(이름, 반환 타입, 매개 변수만)를 선언합니다. 이를 인터페이스 메서드라고 합니다. 이후 인터페이스의 인터페이스 메서드를 구현 클래스에서 구체적으로 구현하게 됩니다.
하지만 메서드 오버라이드는 부모 클래스에서 가상 메서드(virtual void Move()등)를 통해 기본 구현을 제공하고 자식 클래스는 부모 클래스를 상속받은 후 재정의합니다.
인터페이스는 주로 클래스 간의 공통된 동작을 정의할 때 사용되지만, 메서드 오버라이딩은 상속된 메서드의 동작을 변경하거나 확장할 때 사용됩니다.

인터페이스 사용 이유

위의 차이를 보셨을 때, 인터페이스와 메서드 오버라이드의 가장 큰 차이점이라 뭐라고 생각하시나요? 조금씩 차이가 있겠지만 가장 큰 차이는 상속 여부 입니다.
먼저, 이 상속 여부가 어째서 중요한지를 알기 위해서는 다중 상속을 먼저 이해하고 가야합니다.
C#, JAVA 등의 언어는 다중 상속을 지원하지 않습니다. 다중 상속이란 한 클래스가 두 개 이상의 부모 클래스를 상속받는 객체 지향 프로그래밍 개념입니다. 이를 통해 자식 클래스는 여러 부모 클래스의 속성과 메서드를 물려받을 수 있습니다.
그렇게 되면 어떤 점이 유리할까요?
다중 상속의 장점
1.
코드 재사용성 : 여러 부모 클래스의 기능을 재사용할 수 있어 코드 중복을 줄일 수 있습니다.
2.
다양한 기능 조합 : 여러 클래스로부터 기능을 조합하여 더 복잡한 기능을 가진 클래스를 쉽게 만들 수 있습니다.
하지만 이러한 장점이 있음에도 다중 상속을 지원하지 않는다는 건 그만한 단점이 있기도 하기 때문입니다.
다중 상속의 단점
1.
다이아몬드 문제(Diamond Problem) : 동일한 조상을 갖는 두 부모 클래스를 상속받을 때 발생하는 문제입니다. 자식 클래스가 조상 클래스의 어떤 메서드를 상속받을지 모호해질 수 있습니다. 다중 상속의 가장 큰 문제점 중 하나는 다이아몬드 문제입니다. 이를 알아보기 위해 다중 상속을 지원하는 C++을 통해 간단한 코드를 보며 알아보겠습니다.
class A { public: void show() { std::cout << "A::show" << std::endl; } }; class B : public A { }; class C : public A { }; class D : public B, public C { }; int main() { D obj; obj.show(); // 모호성 에러 발생 return 0; }
C++
복사
간단하게 이해해보자면 A 클래스를 B,C 클래스가 상속받고 D가 한번 더 B,C를 상속받은 코드입니다. 여기서 D는 show() 메서드를 실행하면 B와 C 클래스에서 상속받았기 때문에 부모 클래스 중 어떤 클래스의 메서드를 받아서 실행할지 애매해지기에 다이아몬드 문제가 발생하는 것입니다.
이러한 다이아몬드 문제를 해결하기 위해 C#, JAVA에서 사용하는 것이 인터페이스입니다. 다중 상속에서 발생하는 다이아몬드 문제는 동일한 상위 클래스에서 파생된 여러 경로로 인해 생기는 모호성 문제입니다. 인터페이스는 실제 구현을 포함하지 않고, 단지 구현해야 할 메서드를 정의하기 때문에 클래스는 각 인터페이스의 메서드를 구현하면서 충돌 없이 여러 인터페이스를 자유롭게 구현할 수 있습니다.
2.
복잡성 증가 : 클래스 계층 구조가 복잡해지면 유지보수와 디버깅이 어려워질 수 있습니다.
3.
이름 충돌 : 여러 부모 클래스에서 동일한 이름의 메서드나 속성을 상속받을 때 이름 충돌이 발생할 수 있습니다.
인터페이스는 이러한 다중 상속의 장점을 제공하면서도 단점을 피할 수 있습니다. 하지만 메서드 오버라이드만 사용하게 되면 다중 상속의 이점을 활용할 수 없습니다. 그렇기에 인터페이스와 메서드 오버라이드를 제대로 알고 상황에 맞게 사용해야합니다.

추상 클래스란?

추상 클래스는 일반 클래스와 비슷하지만 인스턴스를 직접 생성할 수 없는 클래스입니다. 주로 다른 클래스들이 상속받아 사용할 기반 클래스로 사용됩니다. 추상 클래스는 하나 이상의 추상 메서드를 포함할 수 있으며, 이 추상 메서드는 메서드의 시그니처만 정의하고 실제 구현은 제공하지 않습니다. 추상 메서드를 포함하는 클래스는 반드시 추상 클래스로 선언되어야 합니다.

추상 클래스의 특징

1.
인스턴스화 불가: 추상 클래스는 직접 인스턴스를 생성할 수 없습니다.
2.
추상 메서드 포함 가능: 추상 클래스는 하나 이상의 추상 메서드(구현 없이 선언만 되는 메서드)를 가질 수 있습니다.
3.
일반 메서드 포함 가능: 추상 클래스는 일반 메서드를 포함할 수 있으며, 이는 기본 동작을 제공할 수 있습니다.
4.
상속: 추상 클래스를 상속받는 클래스는 추상 메서드를 구현해야 합니다.
추상 클래스가 어떤 식으로 사용되는 밑의 코드를 보며 알아보겠습니다.
// 추상 클래스 정의 // abstract 키워드를 통해 추상 클래스 선언 public abstract class Character { // 추상 메서드: 하위 클래스가 반드시 구현해야 함 public abstract void Move(); public abstract void Attack(); // 가상 메서드: 기본 구현 제공 public virtual void Speak() { Console.WriteLine("캐릭터가 말합니다."); } } // 전사 클래스는 Character 클래스를 상속받고 추상 메서드를 구현 public class Warrior : Character { public override void Move() { Console.WriteLine("전사가 이동합니다."); } public override void Attack() { Console.WriteLine("전사가 검으로 공격합니다."); } } // 마법사 클래스는 Character 클래스를 상속받고 추상 메서드를 구현 public class Wizard : Character { public override void Move() { Console.WriteLine("마법사가 이동합니다."); } public override void Attack() { Console.WriteLine("마법사가 주문을 시전합니다."); } class Program { static void Main() { // Character 클래스는 추상 클래스이므로 인스턴스를 직접 생성할 수 없음 // Character character = new Character(); // 오류 발생 // 대신, 추상 클래스를 상속받은 클래스의 인스턴스를 생성할 수 있음 Character warrior = new Warrior(); Character wizard = new Wizard(); warrior.Move(); // "전사가 이동합니다." warrior.Attack(); // "전사가 검으로 공격합니다." warrior.Speak(); // "캐릭터가 말합니다." wizard.Move(); // "마법사가 이동합니다." wizard.Attack(); // "마법사가 주문을 시전합니다." wizard.Speak(); // "캐릭터가 말합니다." } }
C#
복사
추상 클래스나 추상 메서드를 선언할 때에는 abstract라는 키워드를 사용합니다. 추상 클래스는 public abstract void Move(); 와 같은 추상 메서드를 가질 수 있고, 이는 하위 클래스에서 구현하도록 강제합니다. 또한 추상 클래스는 public virtual void Speak(){}와 같은 가상 메서드를 가질 수 있습니다. 이는 기본 구현을 제공하며 자식 클래스의 필요에 따라 오버라이드할 수 있습니다. 결국 하위 클래스에서 추상 클래스를 기반으로 추상 메서드를 구현하게 강제하는 것입니다.

추상 클래스를 사용하는 이유

추상 클래스를 사용하는 이유는 특정 메서드를 자식 클래스에서 반드시 구현하도록 강제하고, 가상 메서드와 추상 메서드를 통해 기본 구현과 구현해야 할 메서드를 혼합하여 사용할 수 있습니다. 또한 클래스와 비슷하게 코드의 재사용성을 높이고 공통된 기능을 자식 클래스와 공유할 수 있습니다.
인터페이스와 유사한 부분이 많아보이지만 추상 클래스는 상태(필드)와 속성, 생성자를 정의할 수 있지만 인터페이스는 불가능합니다.
public interface ICharacter { // 인터페이스는 상태를 가질 수 없습니다. // public int Health; // 컴파일 오류 }
C#
복사
public interface ICharacter { // 인터페이스는 생성자를 정의할 수 없습니다. // public ICharacter() { } // 컴파일 오류 }
C#
복사

추상 클래스는 상속을 강제하는가?

’추상 클래스를 상속받는 클래스는 추상메서드를 구현해야한다는 것은 결국 상속이 되어야 오버라이드가 가능한데 결국 이는 상속을 강제하는 것이 아닌가?’라는 의문을 품으실 수 있습니다.
하지만 추상 클래스를 상속 받는 클래스가 메서드 오버라이드를 강제한다고 하여 상속 자체를 강제하는 것은 아닙니다.
추상 클래스는 상속받는 자식 클래스가 없어도 컴파일 에러가 발생하지 않지만, 상속받는 자식 클래스가 있을 때 오버라이드를 하지 않으면 에러가 발생합니다.
즉, 정리하자면 ‘추상 클래스(부모)가 추상 메서드를 가지고 있을 때, 그 클래스를 상속받는 클래스(자식)는 해당 추상 메서드를 오버라이드(재정의)해야 한다.’가 됩니다.

결론

지금까지 저희는 상속에서 더 나아가, 인터페이스와 추상 클래스에 대해 학습했습니다. 이는 객체지향 프로그래밍에서 굉장히 중요한 요소로, 클래스가 특정 동작을 구현하도록 강제합니다.
인터페이스다중 상속을 지원하고 유연한 설계를 가능하게 하며, 특정 동작을 강제하는 데 적합합니다. 반면, 추상 클래스공통된 동작과 상태를 공유하고, 기본 동작을 제공하는 데 유리합니다.
서로 보완적인 개념이므로 이를 잘 이해하고 상황에 맞게 사용한다면, 유지보수 시에 확장 가능한 시스템을 설계하는 데 도움이 될 것입니다.