안녕하세요! 객체 지향 프로그래밍의 핵심 원칙인 상속과 다형성에 대해 알아보면서, '부모 클래스 타입으로 자식 객체를 다룬다'는 개념의 중요성을 깨달으셨을 겁니다. 이번에는 이 다형성을 더욱 강력하게 만들고, 시스템의 유연성과 확장성을 극대화하는 두 가지 중요한 도구인 **추상 클래스(Abstract Class)**와 **인터페이스(Interface)**에 대해 자세히 살펴보겠습니다. 이 둘은 공통적으로 '추상화'를 목표로 하지만, 그 방식과 용도가 다릅니다.
1. 추상 클래스(Abstract Class): 미완성 설계도
추상 클래스는 이름 그대로 완전하지 않은, 추상적인 클래스를 의미합니다. 일반 클래스처럼 속성과 메서드를 가질 수 있지만, 다음과 같은 중요한 특징이 있습니다.
- 인스턴스화 불가: 추상 클래스는 그 자체로는 객체를 생성할 수 없습니다. (new AbstractClass() 불가능) 마치 설계도인데 중요한 부품이 빠져있어서 실제 제품을 만들 수 없는 것과 같습니다.
- 추상 메서드 포함 가능: abstract 키워드를 사용하여 **추상 메서드(Abstract Method)**를 가질 수 있습니다.
- 추상 메서드는 선언만 있고 구현(몸통)이 없는 메서드입니다. { } 부분이 없습니다.
- 추상 메서드는 반드시 자식 클래스에서 override 키워드를 사용하여 구현해야 합니다.
- 추상 메서드를 하나라도 가지고 있는 클래스는 반드시 abstract 클래스로 선언되어야 합니다.
- 일반 메서드 및 속성 포함 가능: 추상 클래스는 추상 메서드 외에 일반(구현된) 메서드, 속성, 생성자, 필드 등을 가질 수 있습니다.
- 상속 전용: 추상 클래스는 오직 다른 클래스에게 상속(inheritance)될 목적으로만 사용됩니다.
추상 클래스 사용 목적:
- 공통 기능 정의 및 강제 구현: 여러 자식 클래스들이 공유하는 공통적인 속성과 구현된 메서드를 제공하면서도, *반드시 구현해야 할 특정 행동(추상 메서드)*을 강제할 때 사용합니다.
- IS-A 관계의 계층 구조: 'A는 B이다'라는 논리적 관계를 명확히 하면서, 상위 개념이 하위 개념들의 공통적인 특성과 행동을 정의하는 데 적합합니다.
예시: Shape 추상 클래스
// 추상 클래스 Shape 정의
public abstract class Shape
{
public string Name { get; set; } // 일반 속성
public Shape(string name) // 생성자도 가질 수 있음
{
Name = name;
}
// 일반(구현된) 메서드
public void DisplayInfo()
{
Console.WriteLine($"이 도형은 {Name}입니다.");
}
// 추상 메서드: 구현부가 없고, 자식 클래스에서 반드시 재정의해야 함
public abstract double GetArea(); // 면적 계산은 각 도형마다 다름
public abstract double GetPerimeter(); // 둘레 계산도 각 도형마다 다름
}
// Circle 클래스: Shape 추상 클래스 상속
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(string name, double radius) : base(name)
{
Radius = radius;
}
// 추상 메서드 GetArea() 구현 (재정의)
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
// 추상 메서드 GetPerimeter() 구현 (재정의)
public override double GetPerimeter()
{
return 2 * Math.PI * Radius;
}
}
// Rectangle 클래스: Shape 추상 클래스 상속
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(string name, double width, double height) : base(name)
{
Width = width;
Height = height;
}
// 추상 메서드 GetArea() 구현 (재정의)
public override double GetArea()
{
return Width * Height;
}
// 추상 메서드 GetPerimeter() 구현 (재정의)
public override double GetPerimeter()
{
return 2 * (Width + Height);
}
}
추상 클래스 사용 예시:
public class Program
{
public static void Main(string[] args)
{
// Shape shape = new Shape("도형"); // 에러! 추상 클래스는 인스턴스화 불가
Shape circle = new Circle("원", 5.0);
Shape rectangle = new Rectangle("직사각형", 4.0, 6.0);
// 다형성 활용: 부모 타입(Shape)으로 자식 객체를 다룸
List<Shape> shapes = new List<Shape> { circle, rectangle };
foreach (Shape s in shapes)
{
s.DisplayInfo(); // 일반 메서드 호출
Console.WriteLine($"{s.Name}의 면적: {s.GetArea():F2}"); // 재정의된 추상 메서드 호출
Console.WriteLine($"{s.Name}의 둘레: {s.GetPerimeter():F2}"); // 재정의된 추상 메서드 호출
Console.WriteLine("--------------------");
}
/*
출력:
이 도형은 원입니다.
원의 면적: 78.54
원의 둘레: 31.42
--------------------
이 도형은 직사각형입니다.
직사각형의 면적: 24.00
직사각형의 둘레: 20.00
--------------------
*/
}
}
Shape 추상 클래스는 모든 도형이 공통적으로 가질 수 있는 Name 속성과 DisplayInfo() 메서드를 제공합니다. 하지만 도형마다 면적과 둘레를 계산하는 방식이 다르므로, GetArea()와 GetPerimeter()는 추상 메서드로 선언하여 자식 클래스들이 반드시 자신에게 맞는 방식으로 구현하도록 강제합니다.
2. 인터페이스(Interface): '무엇을 할 수 있는가'에 대한 계약서
인터페이스는 클래스가 특정 기능을 제공할 것임을 명시하는 '계약서' 또는 '규약'과 같은 역할을 합니다. 추상 클래스보다 더 높은 수준의 추상화를 제공하며, 다음과 같은 특징이 있습니다.
- 구현 없음: 인터페이스는 메서드의 선언(시그니처)만 포함하며, 어떠한 구현(몸통)도 가지지 않습니다. 속성도 get; set;처럼 선언만 가능하며, 필드를 가질 수 없습니다. (C# 8.0부터는 default 구현 가능하지만, 기본적인 개념은 '구현 없음'입니다.)
- 다중 구현 가능: 클래스는 하나의 추상 클래스만 상속받을 수 있지만, **여러 개의 인터페이스를 구현(Implement)**할 수 있습니다.
- IS-A 관계 (능력): 'A는 B이다'보다는 'A는 B를 할 수 있다' (HAS-A-CAPABILITY)는 관계를 표현하는 데 적합합니다. 예를 들어, '비행기는 날 수 있다(예시 인터페이스 이름 : IFlyable)', '강아지는 짖을 수 있다(예시 인터페이스 이름 : IBarkable)'.
- 접근 제한자: 인터페이스의 모든 멤버는 기본적으로 public이며, 명시적으로 public을 붙이지 않습니다.
인터페이스 사용 목적:
- 역할 정의 및 강제 구현: 특정 역할을 수행하는 데 필요한 행동들을 정의하고, 이 인터페이스를 구현하는 클래스들이 해당 행동들을 반드시 구현하도록 강제할 때 사용합니다.
- 느슨한 결합(Loose Coupling) 촉진: 클래스들 간의 의존성을 줄여 코드를 더 유연하고 확장 가능하게 만듭니다. 특정 구현에 묶이지 않고, 인터페이스라는 추상적인 계약에 의존하게 합니다.
- 다중 상속의 대안: C#의 단일 상속 제한을 극복하고, 여러 상위 타입의 '능력'을 한 클래스에 부여할 수 있도록 합니다.
예시: IPlayable 인터페이스
// 인터페이스 IPlayable 정의 (접근 제한자 public은 붙이지 않음)
public interface IPlayable
{
// 메서드 선언 (구현 없음)
void Play();
void Pause();
void Stop();
// 속성 선언 (구현 없음)
string CurrentTrack { get; set; }
}
// MusicPlayer 클래스: IPlayable 인터페이스 구현
public class MusicPlayer : IPlayable
{
public string CurrentTrack { get; set; }
public MusicPlayer(string track)
{
CurrentTrack = track;
}
public void Play()
{
Console.WriteLine($"{CurrentTrack}을(를) 재생합니다.");
}
public void Pause()
{
Console.WriteLine($"{CurrentTrack}을(를) 일시 정지합니다.");
}
public void Stop()
{
Console.WriteLine($"{CurrentTrack}을(를) 정지합니다.");
}
}
// VideoPlayer 클래스: IPlayable 인터페이스 구현
public class VideoPlayer : IPlayable
{
public string CurrentTrack { get; set; } // 비디오 파일 이름 등
public VideoPlayer(string videoName)
{
CurrentTrack = videoName;
}
public void Play()
{
Console.WriteLine($"비디오 {CurrentTrack}을(를) 재생합니다.");
}
public void Pause()
{
Console.WriteLine($"비디오 {CurrentTrack}을(를) 일시 정지합니다.");
}
public void Stop()
{
Console.WriteLine($"비디오 {CurrentTrack}을(를) 정지합니다.");
}
}
인터페이스 사용 예시:
public class Program
{
public static void Main(string[] args)
{
// 인터페이스 타입으로 객체를 다룸 (다형성)
IPlayable player1 = new MusicPlayer("봄날의 곰");
IPlayable player2 = new VideoPlayer("바다 탐험");
// 인터페이스를 구현하는 모든 객체는 Play, Pause, Stop을 호출할 수 있음
player1.Play();
player1.Pause();
Console.WriteLine($"현재 재생 중: {player1.CurrentTrack}");
player1.Stop();
Console.WriteLine("--------------------");
player2.Play();
player2.Stop();
Console.WriteLine($"현재 재생 중: {player2.CurrentTrack}");
player2.Pause(); // 비디오 플레이어의 일시 정지
Console.WriteLine("\n--- 여러 플레이어를 한 번에 제어 ---");
List<IPlayable> playableItems = new List<IPlayable> { player1, player2 };
foreach (IPlayable item in playableItems)
{
item.Play();
}
// 출력:
// 봄날의 곰을(를) 재생합니다.
// 비디오 바다 탐험을(를) 재생합니다.
}
}
IPlayable 인터페이스는 '재생 가능한' 객체가 가져야 할 공통적인 행동(Play(), Pause(), Stop())을 정의합니다. MusicPlayer와 VideoPlayer는 서로 다른 종류의 객체이지만, IPlayable을 구현함으로써 모두 '재생 가능한' 객체로 취급될 수 있습니다. 이를 통해 서로 다른 구현체를 동일한 인터페이스로 다루는 강력한 다형성을 얻을 수 있습니다.
3. 추상 클래스와 인터페이스 비교
| 특징 | 추상 클래스(Abstract Class) | 인터페이스(Interface) |
| 인스턴스화 | 불가능 | 불가능 |
| 구현 포함 | 가능 (일반 메서드/속성) + 추상 메서드 | 기본적으로 불가능 (선언만 가능), C# 8.0+ 기본 구현 가능 |
| 필드 | 가질 수 있음 | 가질 수 없음 |
| 생성자 | 가질 수 있음 | 가질 수 없음 |
| 상속/구현 | 단일 상속만 가능 (클래스 -> 추상 클래스) | 다중 구현 가능 (클래스 -> 여러 인터페이스) |
| 접근 제한자 | 일반 클래스와 동일 (public, private, protected 등) | 모든 멤버는 기본적으로 public |
| 목적 | IS-A 관계 (종류): 공통 구현과 강제 구현의 균형 | HAS-A-CAPABILITY 관계 (능력): 역할 정의 및 강제 구현 |
| 예시 | Animal, Shape, Vehicle | IComparable, IEnumerable, IDisposable, IPlayable |
언제 무엇을 사용할까?
- 추상 클래스:
- 여러 클래스들이 공통된 기반(base)을 공유하며, 일부 기능은 공통적으로 구현하고 일부는 각자 다르게 구현해야 할 때 사용합니다. (예: Animal의 Sleep()은 공통, MakeSound()는 각자 다르게)
- 'A는 B이다'라는 강한 계층적 관계가 있을 때 적합합니다.
- 버전 변경 시 유연성이 필요한 경우 (새로운 일반 메서드를 추가해도 기존 구현체에 영향 X)
- 인터페이스:
- 클래스들이 서로 다른 상속 계층에 속하더라도 특정 기능을 반드시 수행해야 한다고 규정하고 싶을 때 사용합니다.
- 'A는 B를 할 수 있다'라는 능력을 부여하고 싶을 때 적합합니다.
- 다중 상속의 대안으로 여러 '능력'을 한 클래스에 부여해야 할 때 사용합니다.
- 느슨한 결합을 통해 시스템의 유연성과 확장성을 극대화할 때 중요합니다.
마무리하며: 추상 클래스와 인터페이스로 완성하는 추상화! ✨
추상 클래스와 인터페이스는 C#에서 추상화를 구현하고 다형성을 활용하는 데 필수적인 도구입니다. 둘 다 코드의 유연성과 확장성을 높이고, 특정 기능을 강제하여 견고한 시스템을 만드는 데 기여하지만, 그 접근 방식과 목적에 차이가 있습니다.
- 추상 클래스는 공통된 특징을 가진 '그룹'에 대한 미완성 설계도이며,
- 인터페이스는 다양한 객체들이 공통적으로 가질 수 있는 '능력'에 대한 계약서입니다.
이 두 가지를 적절하게 사용하여 여러분의 C# 프로그램을 더욱 객체 지향적으로, 그리고 더욱 강력하게 만들어 보세요!
추상 클래스와 인터페이스의 특정 구현 방식이나 더 깊은 개념에 대해 궁금한 점이 있으신가요? 언제든지 질문해주세요!
'C# > C# 문법' 카테고리의 다른 글
| C# 중급 문법 (6) : Static에 대해서 (1) | 2025.08.20 |
|---|---|
| C# 중급 문법 (5) : 구조체 (3) | 2025.08.17 |
| C# 중급 문법 (3) : 상속과 다형성 (4) | 2025.08.09 |
| C# 중급 문법 (2) : 생성자와 캡슐화 (2) | 2025.08.05 |
| C# 중급 문법 (1) : 클래스, 추상화, 인스턴스화 (3) | 2025.07.31 |
