1. 추상 메서드
이전 강의에서 우리는 기초를 다졌어: 추상 클래스가 뭔지, 추상 메서드와 프로퍼티를 담을 수 있다는 걸 봤지. 이건 "계약"이야. 상속받는 클래스가 반드시 직접 구현해야 한다는 약속이지.
이제 좀 더 깊게 들어가서 실전에서 자주 쓰는 복잡한 시나리오를 볼 거야. 이번 강의에선 문법의 디테일, 추상이 다단계 계층에서 어떻게 동작하는지, 그리고 abstract랑 virtual을 언제 써야 하는지 확실히 구분해볼 거야.
추상 메서드는 마치 오래된 요리책에 나오는 비밀 레시피 같아: "비밀 재료를 넣으세요" — 뭔진 안 알려주지만, 다음 요리사는 무조건 뭔가를 넣어야 해!
추상 메서드는 항상 추상 클래스 안에 있어야 해
일반(비-추상) 클래스에 추상 메서드를 선언하려고 하면 컴파일 에러가 나. 왜냐면 일반 클래스는 바로 인스턴스화할 수 있는데, 구현 안 된 메서드를 호출하면 뭐가 나올까? IDE가 어리둥절한 표정만 지을걸.
// 이건 컴파일 에러야:
public class WrongClass
{
public abstract void Oops(); // 이렇게 하면 안 돼!
}
클래스에 추상 메서드가 하나라도 있으면, 그 클래스는 abstract로 표시해야 해!
파생 클래스에서 추상 메서드 구현하기
구현은 override 키워드로 해. 추상 클래스를 상속받은 클래스가 모든 추상 메서드를 구현하지 않으면, 그 클래스도 추상이어야 해(아니면 컴파일러가 화낼 거야).
우리 앱 예시 계속해볼게:
이전 강의에서 Shape (도형) 클래스를 만들고, 면적 계산용 추상 메서드를 선언했지. 이제 구체적인 도형을 만들어보자:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
왜 추상 메서드가 그렇게 중요할까?
추상 메서드는 이렇게 말하는 거야: "야, 이 클래스 상속받을 거면, 이건 니가 직접 구현해!" 이러면 프로그램 구조가 더 명확해지고, 실수로 빼먹는 걸 막아주고, 무엇보다 폴리모피즘을 제대로 쓸 수 있게 해줘.
2. 추상 프로퍼티 (properties)
추상 프로퍼티란?
추상 프로퍼티도 추상 메서드처럼 "계약"이야, 근데 프로퍼티(property)용이지. C#에선 프로퍼티가 데이터 캡슐화의 핵심이라서, 추상 프로퍼티는 "값을 어디서(그리고 어떻게) 가져올지는 자식이 알아서 해"라는 뜻이야.
예시:
도형에 Name 프로퍼티를 추가해보자 — 모든 자식이 이걸 가져야 하지만, 뭘 반환할지는 각자 정하면 돼.
public abstract class Shape
{
public abstract string Name { get; }
public abstract double CalculateArea();
}
이제 모든 자식은 이 프로퍼티를 꼭 구현해야 해:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public override string Name => "직사각형";
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public override string Name => "원";
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
문법 특징
추상 프로퍼티는 인터페이스 프로퍼티랑 비슷해: 구현 없이 선언만 하고, getter만 있을지, setter도 있을지 정할 수 있어.
public abstract class Creature
{
// 읽기 전용 프로퍼티
public abstract string Species { get; }
// 읽기/쓰기 프로퍼티
public abstract int Age { get; set; }
}
구현 클래스에서 값을 가져오거나(필요하면) 바꾸는 로직을 직접 만들 수 있어.
왜 추상 프로퍼티가 필요할까?
추상 프로퍼티는 자식이 어떻게 값을 가져오거나 저장할지 직접 정하게 하고 싶을 때 딱이야. 모델링, 비즈니스 모델, 뷰 모델 등에서, 단순히 필드 반환이 아니라 복잡한 로직이 필요할 때 자주 써.
예를 들어, 어떤 도형은 이름을 계산해서 반환하고, 어떤 건 그냥 상수로 반환할 수도 있지. 이런 걸 추상 프로퍼티로 깔끔하게 "숨길" 수 있어.
3. 다이어그램: 추상 클래스, 메서드, 프로퍼티 관계
아래 다이어그램을 보면 어떻게 동작하는지 한눈에 보여:
┌─────────────┐
│ abstract │
│ Shape │
│-------------│
│ +Name: str │ <-- 추상 프로퍼티
│ +Area(): dbl│ <-- 추상 메서드
└─────┬───────┘
│
┌────▼────┐ ┌───────┐
│Rectangle│ │ Circle│
│........ │ .... │ ......│
│+Name │ │+Name │
│+Area() │ │+Area()│
└─────────┘ └───────┘
이렇게 하면 Shape 클래스의 어떤 자식이든 똑같은 인터페이스로 다룰 수 있고, 내부 구현은 각자 자유롭게 할 수 있어.
추상 메서드와 프로퍼티용 UML 다이어그램:
┌────────────────────────────┐
│ abstract class │
│ Animal │
│────────────────────────────│
│+ Name: string {abstract} │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
│
┌────────┴──────────┐
│ │
┌────────────┐ ┌─────────────┐
│ Cat │ │ Dog │
│────────────│ │─────────────│
│+ Name │ │+ Name │
│+ MakeSound()│ │+ MakeSound()│
└────────────┘ └─────────────┘
4. 실전 시나리오와 실제 프로젝트에서의 장점
추상 메서드와 프로퍼티는 클래스 계층을 확장 가능하게 설계할 때 필수야. 대형 비즈니스 앱, 복잡한 도메인 모델, 플러그인 시스템, UI 프레임워크 등에서, 시스템의 "법칙"을 정해두고 모든 자식이 반드시 따르게 할 수 있지.
실제 프로젝트에선 이런 "깔끔한" 구조 덕분에 몇 달, 몇 년이 지나도 새 기능이나 엔티티를 쉽게 추가할 수 있어. 예전 코드가 이미 모든 동작 케이스를 고려하고 있으니까.
"왜?"라는 질문과 폴리모피즘으로 돌아가기
폴리모피즘은 마법처럼 보이지만, 사실 그냥 계약이야: "이 계열 객체라면 어떤 메서드를 호출해도 항상 맞는 결과가 나온다"는 거지. 추상 메서드와 프로퍼티는 계층 전체가 같은 언어를 쓰게 만들지만, "기본 구현"은 강요하지 않아.
이런 방식은 플러그인 시스템(IDE, 그래픽 에디터, CRM 등)에서 많이 써. 외부 개발자가 확장 기능을 만들 때, 플랫폼이 요구하는 최소 기능만 구현하면 되니까. 예를 들어, 파일 처리 모듈을 만든다면, 기본 클래스에 추상 프로퍼티 FileExtension과 추상 메서드 Open()을 넣어서, 어떤 플러그인이든 자기 파일 타입을 제대로 처리할 수 있게 할 수 있어.
추상, virtual, 일반 멤버의 차이
| 특징 | 일반 메서드/프로퍼티 | virtual | abstract |
|---|---|---|---|
| 구현 있음? | 있음 | 있음 | 없음 |
| override 필수? | 아님 | 아님(원하면 오버라이드 가능) | 필수(자식에서 꼭 구현) |
| 직접 호출 가능? | 가능 | 가능 | 불가 |
| 일반 클래스에 가능? | 가능 | 가능 | 불가 |
| sealed로 표시 가능? | 불가 | 가능 | 불가 |
5. 우리 학습 앱에서의 적용
이제 새로 배운 걸로 도형 앱으로 돌아가서, 추상 프로퍼티와 메서드가 어떻게 유연하고 이해하기 쉬운 시스템을 만드는지 보자.
// 예를 들어, 메인 메서드에서
List<Shape> shapes = new List<Shape>
{
new Rectangle(4, 5),
new Circle(2.5)
};
foreach (Shape shape in shapes)
{
// 추상 프로퍼티 Name과 메서드 CalculateArea 덕분에,
// 도형 타입이 뭔지 신경 안 써도 돼
Console.WriteLine($"{shape.Name}, 면적: {shape.CalculateArea():F2}");
}
결과:
직사각형, 면적: 20.00
원, 면적: 19.63
깔끔하지? 코드가 도형이 뭔지 몰라도, 필요한 프로퍼티와 메서드만 호출하면 .NET CLR이 알아서 제대로 실행해줘!
6. 자주 하는 실수와 구현 팁
실수 1: 부모의 추상 메서드를 구현 안 함.
파생 클래스에서 추상 메서드나 프로퍼티를 하나라도 구현 안 했는데, 그 클래스가 abstract가 아니면, 컴파일러가 빌드 못 하게 막아. 이건 좋은 보호장치야: "구멍 난" 로직(예: CalculateArea() 없는 도형)으로 객체를 만들 수 없으니까.
실수 2: 메서드는 abstract인데 클래스는 아님.
이런 코드도 컴파일 안 돼. 클래스에 abstract 메서드를 추가했다면, 클래스도 반드시 추상이어야 해:
public abstract class Polygon : Shape
{
// CalculateArea() 구현 안 함, 그래서 클래스는 abstract
}
실수 3: 추상 메서드를 virtual로 구현하려고 함.
추상 메서드는 override로 구현해야 해, virtual이 아니라. 근데 만약 더 아래 계층에서 또 오버라이드할 수 있게 하고 싶으면, 먼저 override로 구현한 다음, 자식 클래스에서 virtual로 선언할 수 있어:
public override double CalculateArea()
{
// 기본 구현...
}
더 하위 클래스에서 이 메서드를 다시 오버라이드할 수 있어. 하지만 다시 abstract로 만들 순 없어, 이미 구현이 있으니까.
GO TO FULL VERSION