Dart에서 추상 클래스(Abstract Class)는 객체 지향 프로그래밍에서 중요한 개념 중 하나로, 클래스 설계의 유연성과 확장성을 높이는 데 사용됩니다. 추상 클래스는 여러 클래스가 상속할 수 있지만, 직접 인스턴스화(객체 생성)는 할 수 없습니다. 이는 추상 클래스가 기본적인 인터페이스 또는 행동을 정의하지만, 구체적인 구현은 서브 클래스에서 제공해야 하기 때문입니다. 이를 통해 프로그램의 유지보수성과 코드 재사용성을 높일 수 있으며, 다형성(Polymorphism)과 같은 개념을 활용하여 더욱 유연한 설계가 가능합니다.
1. 추상 클래스란?
추상 클래스는 다른 클래스가 상속하여 확장할 수 있는 클래스입니다. 하지만 추상 클래스 자체는 인스턴스화할 수 없습니다. 즉, new 키워드를 사용해 직접 객체를 생성하려고 하면 컴파일 오류가 발생합니다. 추상 클래스는 공통된 속성이나 메서드를 정의하고, 이를 상속받은 클래스들이 그 구체적인 동작을 정의하도록 강제할 수 있습니다.
추상 클래스 정의 방법
Dart에서 추상 클래스는 abstract 키워드를 사용하여 정의됩니다. 추상 클래스는 하나 이상의 추상 메서드를 가질 수 있으며, 이러한 메서드는 구현이 없는 메서드로, 단순히 메서드의 시그니처만 정의하고 실제 구현은 서브 클래스에서 하게 됩니다.
abstract class Animal {
void makeSound(); // 추상 메서드: 구현이 없음
}
위의 코드에서 Animal은 추상 클래스입니다. 이 클래스는 makeSound()라는 메서드를 정의하고 있지만, 그 메서드는 구현되지 않았습니다. 이를 상속받는 클래스는 반드시 이 메서드를 구현해야 합니다.
2. 추상 클래스 상속
추상 클래스를 상속받는 클래스는 추상 클래스의 모든 추상 메서드를 반드시 구현해야 합니다. 상속을 통해 자식 클래스는 부모 클래스의 속성과 메서드를 물려받아 사용할 수 있으며, 추상 클래스에서 정의된 기본 메서드들을 재정의(override)할 수 있습니다.
추상 클래스 상속 예시
다음은 추상 클래스 Animal을 상속받아 구체적인 동작을 구현한 예시입니다:
class Dog extends Animal {
@override
void makeSound() {
print('Woof Woof'); // 구체적인 구현
}
}
class Cat extends Animal {
@override
void makeSound() {
print('Meow'); // 구체적인 구현
}
}
위 코드에서 Dog와 Cat 클래스는 Animal 추상 클래스를 상속받았으며, makeSound() 메서드를 구체적으로 구현하였습니다. 이처럼 상속을 통해 각 동물 클래스는 고유의 소리를 출력할 수 있게 됩니다.
3. 추상 클래스의 인스턴스화 제한
추상 클래스는 직접 인스턴스화할 수 없습니다. 즉, 추상 클래스는 기본 설계 원칙으로 정의된 템플릿일 뿐, 실제 동작을 담당하지 않으므로 이를 직접 사용할 수 없습니다.
인스턴스화 시도 시 오류 예시
void main() {
Animal myAnimal = Animal(); // 오류 발생: 추상 클래스는 인스턴스화할 수 없음
}
위의 코드는 컴파일 오류를 발생시킵니다. 추상 클래스는 상속을 통해 구체적인 클래스를 만들고, 그 클래스를 인스턴스화해야 합니다. 예를 들어, Dog 클래스는 인스턴스화할 수 있습니다.
void main() {
Animal myDog = Dog();
myDog.makeSound(); // Output: Woof Woof
}
이처럼 추상 클래스는 상속받은 서브 클래스의 구체적인 동작을 통해 사용됩니다.
4. 추상 클래스의 역할
1) 코드 재사용성 향상
추상 클래스는 상속을 통해 코드를 재사용할 수 있는 기초를 제공합니다. 여러 서브 클래스가 공통으로 사용하는 로직을 부모 추상 클래스에 정의함으로써, 서브 클래스는 필요한 부분만 재정의하거나 확장할 수 있습니다.
abstract class Animal {
void breathe() {
print('Breathing');
}
void makeSound();
}
class Dog extends Animal {
@override
void makeSound() {
print('Woof Woof');
}
}
class Cat extends Animal {
@override
void makeSound() {
print('Meow');
}
}
위 코드에서 breathe() 메서드는 추상 클래스 Animal에 정의되어 있고, 모든 동물이 공유하는 동작이므로 서브 클래스에서 별도의 구현 없이 그대로 사용할 수 있습니다. 반면 makeSound()는 서브 클래스에서 각자의 방식으로 구현해야 합니다.
2) 강제 구현
추상 클래스는 서브 클래스에서 반드시 구현해야 하는 메서드를 정의할 수 있습니다. 이를 통해 특정 기능의 구현을 강제할 수 있으며, 이를 통해 애플리케이션의 일관성과 안정성을 보장할 수 있습니다.
5. 다형성(Polymorphism)과 추상 클래스
다형성(Polymorphism)은 객체 지향 프로그래밍에서 매우 중요한 개념으로, 하나의 인터페이스나 추상 클래스를 통해 여러 다른 클래스의 객체를 동일하게 다룰 수 있도록 합니다. 추상 클래스는 다형성을 구현하는 데 자주 사용되며, 코드의 유연성과 확장성을 높이는 데 기여합니다.
다형성 예시
abstract class Animal {
void makeSound();
}
class Dog extends Animal {
@override
void makeSound() {
print('Woof Woof');
}
}
class Cat extends Animal {
@override
void makeSound() {
print('Meow');
}
}
void animalSound(Animal animal) {
animal.makeSound();
}
void main() {
Animal myAnimal = Dog();
animalSound(myAnimal); // Output: Woof Woof
myAnimal = Cat();
animalSound(myAnimal); // Output: Meow
}
이 예시에서는 animalSound() 함수가 Animal 타입의 객체를 인자로 받아 해당 객체의 makeSound() 메서드를 호출합니다. 다형성 덕분에 Dog와 Cat 클래스의 객체를 모두 동일하게 다룰 수 있으며, 각각의 클래스에서 구현한 고유한 makeSound() 메서드가 실행됩니다.
6. 추상 클래스와 인터페이스의 차이
Dart에서 인터페이스는 특별한 키워드 없이도 구현될 수 있으며, Dart의 모든 클래스는 암묵적으로 인터페이스로 작동할 수 있습니다. 추상 클래스와 인터페이스는 비슷한 역할을 하지만, 추상 클래스는 일부 구현을 포함할 수 있다는 점에서 인터페이스와 차이가 있습니다. 인터페이스는 오직 메서드 시그니처만 제공하며, 구현을 포함하지 않습니다. 추상 클래스는 기본 동작을 포함할 수 있어 좀 더 유연한 구조를 제공합니다.
예시: 인터페이스와 추상 클래스의 차이
abstract class Animal {
void breathe() {
print('Breathing');
}
void makeSound();
}
class Bird implements Animal {
@override
void makeSound() {
print('Tweet');
}
@override
void breathe() {
print('Bird is breathing');
}
}
위 예시에서 Animal 추상 클래스는 Bird 클래스에 의해 구현되었습니다. breathe() 메서드는 추상 클래스에 기본 구현이 있지만, Bird 클래스에서 이를 재정의할 수 있습니다. 인터페이스와 달리 추상 클래스는 이처럼 기본 동작을 제공할 수 있습니다.
7. 결론
Dart에서 추상 클래스는 강력한 객체 지향 프로그래밍 도구로, 코드의 재사용성과 유지보수성을 크게 향상시킵니다. 추상 클래스는 공통 동작을 상속받는 여러 클래스가 공유할 수 있는 기초를 제공하며, 특정 메서드의 구현을 강제할 수 있습니다. 다형성을 통해 추상 클래스를 여러 방식으로 구현한 객체들을 동일하게 다룰 수 있어, 코드의 유연성과 확장성이 극대화됩니다. Dart에서 추상 클래스는 복잡한 애플리케이션의 구조를 효율적으로 관리할 수 있는 중요한 개념입니다.