추상 클래스와 인터페이스는 어떠한 객체가 공통된 기능을 중복없이 사용하게 해주고, 그로 인해서 확장성과 유연성이 증가하게 됩니다. 하지만 이 둘의 차이를 모를 경우 추상 클래스를 써야 할 상황에 인터페이스를 사용하게 되고, 인터페이스를 써야 할 상황에 추상 클래스를 사용하게 됩니다. 이럴 경우 가독성이 떨어지거나, 필요 없는 기능임에도 추가적으로 코드를 작성 해야 한다거나 하는 상황이 생길 수 있습니다. 이번 시간에는 추상 클래스와 인터페이스의 차이를 이해하고 적절하게 사용할 수 있도록 가이드를 제시하겠습니다. 또한 예시코드를 통해서 이해를 돕겠습니다.
목차
[추상 클래스의 개념]
추상 클래스는 설계도와 같습니다. 예를 들어, 동물을 다루는 프로그램을 만든다고 가정해보겠습니다. 여기서 추상 클래스는 ‘동물’에 대한 블루프린트(청사진) 역할을 합니다. 동물은 다리를 가지고 먹이를 먹는다는 공통 특성이 있지만, 구체적으로 어떤 종류의 동물인지에 따라 다양한 특성을 가질 수 있습니다.
[추상 클래스의 사용법과 예시코드]
- 추상 클래스는
abstract
키워드를 사용하여 선언됩니다. - 추상 메서드가 하나 이상 포함되어야 하며, 추상 메서드는 메서드의 시그니처만을 정의하고 구현은 하위 클래스에서 이루어집니다.
- 추상 클래스 내에서 구현된 메서드는 일반적인 메서드로 사용할 수 있습니다.
// 추상 클래스 정의
abstract class Animal
{
// 추상 메서드 선언
public abstract void MakeSound();
// 일반 메서드
public void Eat()
{
Console.WriteLine("먹이를 먹습니다.");
}
}
// 추상 클래스를 상속받는 구체적인 클래스 정의
class Dog : Animal
{
// 추상 메서드 구현
public override void MakeSound()
{
Console.WriteLine("멍멍");
}
}
class Cat : Animal
{
// 추상 메서드 구현
public override void MakeSound()
{
Console.WriteLine("야옹");
}
}
// 메인 메서드에서 사용 예시
class Program
{
static void Main(string[] args)
{
// 추상 클래스로 객체를 생성할 수 없습니다.
// Animal myAnimal = new Animal(); // 에러 발생
// 추상 클래스를 상속받은 클래스의 객체 생성
Animal myDog = new Dog();
myDog.MakeSound(); // 출력: 멍멍
myDog.Eat(); // 출력: 먹이를 먹습니다.
Animal myCat = new Cat();
myCat.MakeSound(); // 출력: 야옹
myCat.Eat(); // 출력: 먹이를 먹습니다.
}
}
이 예시 코드에서는 추상 클래스인 Animal
을 정의하고, 이를 상속받은 Dog
와 Cat
클래스를 구현했습니다. Animal
클래스에는 추상 메서드 MakeSound
와 일반 메서드 Eat
이 정의되어 있습니다. 이를 통해 각 동물의 소리를 출력하고 Eat
메서드를 사용할 수 있습니다.
[추상 클래스의 장점과 단점]
[장점]
- 구조화된 코드: 추상 클래스를 사용하면 여러 요소를 논리적으로 구조화할 수 있습니다. 예를 들어, 게임 캐릭터, 아이템, 적 등 각각을 추상 클래스로 정의하여 코드의 가독성과 유지보수성을 높일 수 있습니다.
- 재사용성: 여러 객체가 비슷한 특성을 가질 수 있습니다. 추상 클래스를 사용하여 공통된 기능을 하나의 클래스로 정의하면, 이를 상속받아 다양한 객체를 쉽게 만들 수 있습니다. 예를 들어, 여러 종류의 적 캐릭터가 있는 게임에서 공통된 움직임이나 공격을 추상 클래스로 정의할 수 있습니다.
- 유연성과 확장성: 추상 클래스를 사용하면 새로운 요소를 쉽게 추가하거나 기존 요소를 변경할 수 있습니다. 새로운 캐릭터나 아이템을 추가할 때 기존의 추상 클래스를 상속받아 필요한 기능을 구현하면 됩니다. 이는 게임의 확장성을 높이고 새로운 콘텐츠를 빠르게 개발할 수 있게 해줍니다.
using System;
// 추상 클래스 정의: 게임 요소를 나타내는 Entity
abstract class Entity
{
protected string name;
public Entity(string name)
{
this.name = name;
}
// 추상 메서드: 게임 요소의 행동을 정의
public abstract void Action();
}
// 캐릭터 클래스: Entity를 상속받음
class Character : Entity
{
public Character(string name) : base(name) { }
// 캐릭터의 행동을 구현
public override void Action()
{
Console.WriteLine($"{name}이(가) 이동합니다.");
}
}
// 아이템 클래스: Entity를 상속받음
class Item : Entity
{
public Item(string name) : base(name) { }
// 아이템의 행동을 구현
public override void Action()
{
Console.WriteLine($"{name}을(를) 획득합니다.");
}
}
class Program
{
static void Main(string[] args)
{
// 캐릭터와 아이템 객체 생성
Character player = new Character("플레이어");
Item potion = new Item("체력 포션");
// 각 객체의 행동 수행
player.Action(); // 출력: 플레이어이(가) 이동합니다.
potion.Action(); // 출력: 체력 포션을(를) 획득합니다.
}
}
이 코드에서는 Entity
추상 클래스를 정의하여 게임의 요소를 추상화합니다. Character
와 Item
클래스는 각각 캐릭터와 아이템을 나타내며, Entity
를 상속받아 특정한 행동을 구현합니다. 이를 통해 코드를 구조화하고 새로운 캐릭터나 아이템을 추가할 때 기존의 추상 클래스를 상속받아 쉽게 구현할 수 있습니다.
[단점]
- 강한 결합도:추상 클래스를 사용하면 하위 클래스가 추상 클래스에 강하게 의존할 수 있으므로, 추상 클래스의 변경이 여러 부분에 영향을 줄 수 있습니다. 이는 코드의 유연성을 저하시킬 수 있습니다.
- 복잡성: 추상 클래스를 사용하면 클래스 간의 관계가 복잡해질 수 있습니다. 특히 추상 클래스의 상속 구조가 복잡해지면 코드의 이해와 유지보수가 어려워질 수 있습니다.
using System;
using System.Collections.Generic;
// 추상 클래스 정의: 게임 캐릭터
abstract class Character
{
protected string name;
protected int health;
public Character(string name)
{
this.name = name;
this.health = 100;
}
// 추상 메서드: 공격을 수행하는 행동
public abstract void Attack();
}
// 플레이어 클래스
class Player : Character
{
public Player(string name) : base(name) { }
public override void Attack()
{
Console.WriteLine($"{name}이(가) 공격합니다!");
}
}
// 몬스터 클래스
class Monster : Character
{
public Monster(string name) : base(name) { }
public override void Attack()
{
Console.WriteLine($"{name}이(가) 플레이어를 공격합니다!");
}
}
// 고블린 클래스
class Goblin : Monster
{
public Goblin(string name) : base(name) { }
public override void Attack()
{
Console.WriteLine($"{name}이(가) 플레이어를 향해 작은 돌을 던집니다!");
}
}
// 트롤 클래스
class Troll : Monster
{
public Troll(string name) : base(name) { }
public override void Attack()
{
Console.WriteLine($"{name}이(가) 플레이어를 향해 거대한 망치를 휘두릅니다!");
}
}
// 드래곤 클래스
class Dragon : Monster
{
public Dragon(string name) : base(name) { }
public override void Attack()
{
Console.WriteLine($"{name}이(가) 플레이어를 향해 불을 내뿜습니다!");
}
}
class Program
{
static void Main(string[] args)
{
Player player = new Player("용사");
List<Character> enemies = new List<Character>();
enemies.Add(new Goblin("작은 고블린"));
enemies.Add(new Troll("거대한 트롤"));
enemies.Add(new Dragon("화염 용"));
player.Attack();
foreach (var enemy in enemies)
{
enemy.Attack();
}
}
}
이 코드에서는 Character
를 상속받은 다양한 몬스터 클래스를 만들었고, 이들을 리스트에 추가하여 플레이어와 상호작용합니다. 이렇게 되면 클래스 간의 강한 의존성과 복잡한 상속 구조가 형성되어 유지보수가 어려워집니다
[언제 사용해야 할까?]
- 유사한 기능을 가진 여러 클래스가 있을 때: 예를 들어, 게임에서 여러 종류의 적 캐릭터가 있을 때, 이들이 공통적으로 가지는 움직임이나 공격 메서드 등을 추상 클래스로 정의할 수 있습니다. 이렇게 하면 새로운 적을 추가할 때마다 반복적인 코드를 작성할 필요가 없으며, 코드의 재사용성을 높일 수 있습니다.
- 인터페이스보다 구현된 메서드가 필요할 때: 추상 클래스는 일부 메서드를 구현할 수 있습니다. 이는 인터페이스와 비교했을 때 구현된 메서드를 가질 수 있다는 장점이 있습니다. 따라서 일부 기능은 이미 구현되어야 하고, 다른 기능은 하위 클래스에서 구현되어야 하는 경우에 추상 클래스를 사용할 수 있습니다.
- 코드의 확장성을 고려할 때: 코드의 확장성을 고려할 때 추상 클래스를 사용하는 것이 유리할 수 있습니다. 추상 클래스는 하위 클래스에서 확장하고 재정의할 수 있는 메서드를 포함할 수 있으므로, 새로운 기능을 추가하거나 기존 기능을 변경할 때 유용합니다.
- 공통적인 기능을 추상화할 때: 추상 클래스는 공통된 특성이나 기능을 추상화하여 정의할 때 사용됩니다. 예를 들어, GUI 프레임워크에서 여러 컨트롤이 공통적으로 가져야 할 속성과 메서드를 추상 클래스로 정의할 수 있습니다.
마지막으로 추상 클래스는 추상클래스를 상속받는 모든 자식이 어떠한 메서드를 무조건 사용해야할 때 사용합니다. 예를들어서 동물은 모두 이동을한다고 가정하면, 이동이라는 메서드는 무조건 구현해야하기 때문에 추상클래스로 구현하여 이동을 구현하도록 강제하는 것 입니다.
[인터페이스의 개념]
인터페이스는 프로그래밍에서 객체들이 상호작용하는 방법을 정의하는 일종의 기능 조각입니다. 어떤 객체가 특정한 인터페이스를 구현한다는 것은 그 객체가 인터페이스에 명시된 기능이 추가되는 것 과 같습니다. 예를 들어, 자동차와 비행기가 있을 때, 운전과 비행이라는 두 가지 다른 인터페이스를 정의할 수 있습니다. 그리고 각각은 자신의 인터페이스를 따라야 합니다.
using System;
// 이동 가능한 능력을 가진 인터페이스 정의
interface IMovable
{
void Move(); // 이동 메서드 선언
}
// 자동차 클래스
class Car : IMovable
{
public void Move()
{
Console.WriteLine("자동차가 바퀴를 돌려 이동합니다.");
}
}
// 비행기 클래스
class Airplane : IMovable
{
public void Move()
{
Console.WriteLine("비행기가 날개를 펼쳐서 이동합니다.");
}
}
class Program
{
static void Main(string[] args)
{
// 각각의 객체 생성
Car myCar = new Car();
Airplane myAirplane = new Airplane();
// 인터페이스를 통해 객체의 메서드 호출
Console.WriteLine("자동차 이동:");
myCar.Move();
Console.WriteLine("비행기 이동:");
myAirplane.Move();
}
}
인터페이스를 사용 하기위해서 IMovable를 정의하고 이동 기능이 필요한 Car, Airplane에 인터페이스를 상속합니다. 이로써 자동차와 비행기는 각각 다른 이동기능을 사용할 수 있게 됩니다.
[인터페이스의 장점과 단점]
[장점]
- 유연성과 확장성: 인터페이스를 사용하면 클래스 간의 결합도를 낮추고 유연성을 높일 수 있습니다. 예를 들어, 여러 클래스가 동일한 인터페이스를 구현한다면, 해당 클래스들을 서로 교환하여 사용할 수 있습니다. 이는 코드의 재사용성과 확장성을 높여줍니다. 예를 들어, 다양한 데이터베이스를 사용하는 클래스가 있을 때, 인터페이스를 통해 데이터베이스 종류에 상관없이 동일한 방식으로 작업할 수 있습니다.
- 다형성 지원: 인터페이스를 사용하면 다형성을 지원할 수 있습니다. 즉, 여러 객체가 동일한 인터페이스를 구현하고 있을 때, 해당 인터페이스를 사용하여 다양한 객체를 동일한 방식으로 다룰 수 있습니다. 이는 코드의 유연성을 높여주고, 객체 간의 결합도를 낮출 수 있습니다.
using System;
using System.Collections.Generic;
// 데이터베이스 작업을 위한 인터페이스 정의
interface IDatabase
{
void Connect(); // 연결 메서드
void Query(string sql); // 쿼리 메서드
void Disconnect(); // 연결 해제 메서드
}
// MySQL 데이터베이스 클래스
class MySQLDatabase : IDatabase
{
public void Connect()
{
Console.WriteLine("MySQL 데이터베이스에 연결합니다.");
}
public void Query(string sql)
{
Console.WriteLine($"MySQL 데이터베이스에서 쿼리를 수행합니다: {sql}");
}
public void Disconnect()
{
Console.WriteLine("MySQL 데이터베이스 연결을 해제합니다.");
}
}
// PostgreSQL 데이터베이스 클래스
class PostgreSQLDatabase : IDatabase
{
public void Connect()
{
Console.WriteLine("PostgreSQL 데이터베이스에 연결합니다.");
}
public void Query(string sql)
{
Console.WriteLine($"PostgreSQL 데이터베이스에서 쿼리를 수행합니다: {sql}");
}
public void Disconnect()
{
Console.WriteLine("PostgreSQL 데이터베이스 연결을 해제합니다.");
}
}
class Program
{
static void Main(string[] args)
{
// 데이터베이스 객체 생성
IDatabase database;
// MySQL 데이터베이스 사용
database = new MySQLDatabase();
UseDatabase(database);
// PostgreSQL 데이터베이스 사용
database = new PostgreSQLDatabase();
UseDatabase(database);
}
// 인터페이스를 활용한 데이터베이스 사용 메서드
static void UseDatabase(IDatabase database)
{
database.Connect();
database.Query("SELECT * FROM table_name");
database.Disconnect();
}
}
이 코드에서는 IDatabase
인터페이스를 정의하고, 이를 MySQLDatabase
와 PostgreSQLDatabase
클래스가 구현합니다. 그리고 Main
메서드에서는 각각의 데이터베이스를 사용하는 예시를 보여줍니다. UseDatabase
메서드에서는 인터페이스를 매개변수로 받아 해당 인터페이스의 메서드를 호출하여 데이터베이스 작업을 수행합니다.
이렇게 인터페이스를 사용하면 다양한 데이터베이스를 사용하는 클래스를 동일한 방식으로 다룰 수 있으며, 객체 간의 결합도를 낮추고 코드의 재사용성과 확장성을 높일 수 있습니다.
[단점]
- 추가적인 코드 작성: 인터페이스를 사용하면 추가적인 코드 작성이 필요합니다. 클래스가 인터페이스를 구현하기 위해서는 인터페이스에 선언된 모든 멤버를 구현해야 합니다. 이는 개발 시간을 늘릴 수 있습니다.
- 이해와 유지보수의 어려움: 인터페이스를 사용하면 클래스와 클래스 간의 관계를 추상화하여 표현할 수 있습니다. 그러나 이러한 추상화가 지나치게 되면 코드의 복잡성을 증가시킬 수 있고, 이해와 유지보수가 어려워질 수 있습니다.
using System;
using System.Collections.Generic;
// 인터페이스 정의: 운송수단
interface ITransportation
{
void Move(); // 이동 메서드
}
// 자전거 클래스: ITransportation 인터페이스 구현
class Bicycle : ITransportation
{
public void Move()
{
Console.WriteLine("자전거가 페달을 밟아 이동합니다.");
}
}
// 자동차 클래스: ITransportation 인터페이스 구현
class Car : ITransportation
{
public void Move()
{
Console.WriteLine("자동차가 바퀴를 돌려 이동합니다.");
}
}
// 비행기 클래스: ITransportation 인터페이스 구현
class Airplane : ITransportation
{
public void Move()
{
Console.WriteLine("비행기가 날개를 펼쳐서 이동합니다.");
}
}
// 배 클래스: ITransportation 인터페이스 구현
class Ship : ITransportation
{
public void Move()
{
Console.WriteLine("배가 물결을 헤치며 이동합니다.");
}
}
// 기차 클래스: ITransportation 인터페이스 구현
class Train : ITransportation
{
public void Move()
{
Console.WriteLine("기차가 철길을 따라 이동합니다.");
}
}
class Program
{
static void Main(string[] args)
{
// 다양한 운송수단 객체 생성
List<ITransportation> vehicles = new List<ITransportation>
{
new Bicycle(),
new Car(),
new Airplane(),
new Ship(),
new Train()
};
// 각 운송수단의 이동 메서드 호출
foreach (var vehicle in vehicles)
{
vehicle.Move();
}
}
}
이 코드는 다양한 운송수단을 나타내는 클래스를 ITransportation
인터페이스로 구현합니다. 각 탈 것 클래스는 Move()
메서드를 구현하여 이동 방법을 나타냅니다.
하지만 운송수단이 더 많아질수록 인터페이스 구현 클래스도 더 많아지며, 새로운 운송수단을 추가할 때마다 해당 운송수단에 대한 새로운 클래스를 작성하고 인터페이스를 구현해야 합니다. 이는 코드의 복잡성을 증가시키고, 유지보수를 어렵게 만듭니다. 또한, 새로운 인터페이스 멤버가 추가될 때마다 모든 구현 클래스에서 해당 멤버를 구현해야 합니다. 이는 유연성과 확장성을 저해합니다.
[인터페이스는 언제사용해야 할까?]
- 객체 간의 결합도를 줄일 때: 여러 클래스가 동일한 동작을 수행하지만 구현이 다를 때, 인터페이스를 사용하여 각 클래스를 독립적으로 관리할 수 있습니다. 예를 들어, 여러 데이터베이스를 사용하는데, 각각의 데이터베이스에 대한 클래스를 따로 작성한다면 유지보수가 어려울 수 있습니다. 그러나 데이터베이스 연결 및 쿼리 작업과 같은 기능을 수행하는 인터페이스를 정의하고 각 데이터베이스에 대해 해당 인터페이스를 구현한다면, 어떤 데이터베이스를 사용하더라도 동일한 방식으로 코드를 작성할 수 있습니다.
- 다형성을 지원할 때: 여러 객체가 동일한 동작을 수행하지만 구현이 다를 때, 인터페이스를 사용하여 다형성을 지원할 수 있습니다. 이는 코드의 유연성을 높이고, 객체 간의 결합도를 줄여줍니다. 예를 들어, 여러 도형이 있고 각 도형에 대해 넓이를 계산하는 기능이 필요한 경우, 각 도형 클래스가 해당 인터페이스를 구현하면 동일한 방식으로 넓이를 계산할 수 있습니다.
- 클래스 간의 상호작용을 추상화할 때: 클래스 간의 관계를 추상화하여 표현할 필요가 있는 경우에도 인터페이스를 사용할 수 있습니다. 예를 들어, 여러 종류의 운송수단이 있고 이들을 모두 운송수단으로 다루어야 할 때, 각 운송수단에 대한 인터페이스를 정의하여 각각의 클래스가 해당 인터페이스를 구현하면 운송수단으로서의 공통된 특성을 추상화할 수 있습니다.
[추상클래스와 인터페이스 차이 비교]
- 추상 클래스 (Abstract Class):
- 추상 클래스는 일반적으로 추상 메서드(abstract method)를 포함하거나, 추상 메서드와 일반 메서드를 함께 가질 수 있습니다.
- 추상 클래스는 하나 이상의 추상 메서드를 포함하므로, 반드시 하위 클래스에서 이를 구현해야 합니다.
- 추상 클래스는 단일 상속만을 지원하므로, 하나의 클래스만을 상속받을 수 있습니다.
- 일반 메서드의 구현을 함께 제공할 수 있기 때문에, 코드의 재사용성을 높일 수 있습니다.
// 추상 클래스 정의
abstract class Shape
{
public abstract double CalculateArea(); // 추상 메서드
public void Display() // 일반 메서드
{
Console.WriteLine("도형을 그립니다.");
}
}
// 추상 클래스를 상속받은 구체 클래스
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
- 인터페이스 (Interface):
- 인터페이스는 메서드, 속성, 이벤트 및 인덱서를 정의할 수 있지만, 해당 멤버들은 구현되어 있지 않습니다.
- 클래스가 인터페이스를 구현할 때, 해당 인터페이스의 모든 멤버를 반드시 구현해야 합니다.
- 인터페이스는 다중 상속을 지원하므로, 하나의 클래스가 여러 인터페이스를 구현할 수 있습니다.
- 클래스 간의 관계를 추상화하여 표현할 때 주로 사용됩니다.
// 인터페이스 정의
interface IShape
{
double CalculateArea(); // 메서드 선언
}
// 인터페이스를 구현한 클래스
class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public double CalculateArea()
{
return Width * Height;
}
}
추상 클래스는 클래스의 일반적인 행동을 정의하고 일부 메서드를 구현할 때 사용되며, 인터페이스는 클래스 간의 상호작용을 추상화하고 일정한 행동을 강제할 때 사용됩니다. 따라서 추상 클래스는 “is-a” 관계를 표현하고, 인터페이스는 “can-do” 관계를 표현합니다.
이를 더 축약하면 추상클래스는 “무엇인가”를 나타낼 때 사용하며, 인터페이스는 “무엇을 할 수 있는지”를 나타 냅니다.
[마무리 글]
추상클래스와 인터페이스는 개념과 용도가 비슷하여 언제 무엇을 사용 해야할지 헷갈립니다. 그렇기에 장점과 단점을 잘 이해하시고 작성해 드린 예시코드를 읽어보시면, 언제 사용 해야하는지 이해가 되실 것 입니다. 요약하면 추상클래스는 무엇인가를 정의할 때 공통된 기능들이 있으면 그룹화하는 것이고, 인터페이스는 무엇을 할 수 있는지를 나타내고, 클래스에 할 수 있는 것들을 추가하는 개념입니다.