추상 클래스란 무엇인가?
추상 클래스는 인스턴스를 만들 수 없는 클래스이다. 즉, new 키워드를 사용하여 객체를 생성할 수 없으며, 상속을 통해서만 사용될 수 있다. 추상 클래스는 하나 이상의 추상 메소드를 포함할 수 있으며, 이 메소드는 구현되지 않고 서브클래스에서 반드시 구현해야 한다. 이를 통해 공통적인 기능을 정의하면서도 세부 구현은 서브클래스에 맡기는 방식으로 유연하게 동작할 수 있다.
예시:
abstract class Animal {
void makeSound(); // 추상 메소드
}
추상 클래스의 역할
추상 클래스는 객체 지향 프로그래밍에서 중요한 역할을 한다. 특히, 다양한 클래스들이 공통적으로 가져야 할 속성이나 메소드를 정의하는 데 유용하다. 추상 클래스는 설계 단계에서 클래스들의 구조를 정의하는 데 도움을 준다.
인터페이스란 무엇인가?
Dart에서는 별도의 interface 키워드를 제공하지 않는다. 대신, 모든 클래스는 암묵적으로 인터페이스 역할을 할 수 있다. implements 키워드를 사용하여 클래스를 구현할 때, 해당 클래스의 모든 메소드를 구현해야 한다. 인터페이스는 특정 기능을 다중 상속하고자 할 때 유용하게 사용된다.
예시:
class Dog implements Animal {
@override
void makeSound() {
print("Bark");
}
}
추상 클래스와 인터페이스의 차이점
추상 클래스와 인터페이스는 모두 객체 지향 설계에서 중요한 역할을 하지만, 그 목적과 사용 방식에는 차이가 있다.
-
추상 클래스는 상속을 통해 기능을 공유하며, 공통된 코드도 포함할 수 있다. 반면, 인터페이스는 클래스가 구현해야 할 메소드의 계약을 정의한다.
-
추상 클래스는 하나만 상속받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있다.
-
추상 클래스는 메소드와 필드를 가질 수 있지만, 인터페이스는 오직 메소드 선언만 갖는다.
추상 클래스 예시:
abstract class Vehicle {
void drive();
void stop();
}
인터페이스 예시:
class Car implements Vehicle {
@override
void drive() {
print("Driving the car");
}
@override
void stop() {
print("Car stopped");
}
}
추상 클래스와 인터페이스의 사용 사례
추상 클래스의 사용 사례
추상 클래스는 계층 구조에서 공통적인 동작이나 속성을 정의하고, 이를 서브클래스에서 확장하거나 재정의할 때 유용하다. 특히, 코드 재사용성이 필요한 경우에 효과적이다. 예를 들어, 동물 클래스를 상속받는 다양한 동물들이 공통적으로 가질 수 있는 행동이나 속성을 추상 클래스에 정의하고, 각 동물이 해당 행동을 어떻게 구현할지를 서브클래스에서 결정할 수 있다.
예시:
abstract class Animal {
String name;
Animal(this.name);
void eat() {
print('$name is eating.');
}
void makeSound(); // 추상 메소드
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
void makeSound() {
print('Bark');
}
}
class Cat extends Animal {
Cat(String name) : super(name);
@override
void makeSound() {
print('Meow');
}
}
이 경우, Animal 클래스는 추상 클래스이며, 공통적으로 동물들이 가진 eat() 메소드를 구현하고 있다. 하지만 makeSound() 메소드는 추상 메소드로, 각각의 동물들이 다르게 구현해야 한다.
인터페이스의 사용 사례
인터페이스는 서로 다른 클래스들에 공통된 기능을 강제할 때 유용하다. 예를 들어, 여러 장치들이 서로 다른 방식으로 작동하지만, start와 stop 같은 공통 기능을 가져야 한다고 가정할 수 있다. 이때 인터페이스를 통해 이 두 메소드를 강제할 수 있다.
예시:
class Device {
void start() {
print('Device started');
}
void stop() {
print('Device stopped');
}
}
class Printer implements Device {
@override
void start() {
print('Printer is warming up');
}
@override
void stop() {
print('Printer is cooling down');
}
}
class Scanner implements Device {
@override
void start() {
print('Scanner is initializing');
}
@override
void stop() {
print('Scanner is shutting down');
}
}
이 예시에서는 Device 클래스가 인터페이스 역할을 하고 있으며, 각각의 장치인 Printer와 Scanner가 이를 구현하여 start()와 stop() 메소드를 각기 다르게 정의하고 있다.
인터페이스와 추상 클래스의 선택 기준
인터페이스와 추상 클래스는 각각의 상황에 맞게 선택적으로 사용된다. 두 방법 모두 클래스 간의 관계를 정의하고, 코드의 재사용성을 높일 수 있지만, 특정 상황에서 더 적합한 방법을 선택해야 한다.
- 추상 클래스는 공통된 속성과 동작을 미리 정의해 놓고, 이를 확장하여 사용하는 경우 적합한다.
- 인터페이스는 다중 구현이 필요한 경우 적합하며, 클래스 간 결합도를 낮출 수 있다.
다중 상속 문제 해결
Dart는 클래스의 다중 상속을 지원하지 않지만, 여러 인터페이스를 구현할 수 있다. 이 방법을 통해 클래스 간의 유연한 관계를 설정하고, 다중 상속의 문제를 우회할 수 있다.
예시:
class Flyable {
void fly();
}
class Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
@override
void fly() {
print('Duck is flying');
}
@override
void swim() {
print('Duck is swimming');
}
}
이처럼 Duck 클래스는 Flyable과 Swimmable 인터페이스를 모두 구현할 수 있다.
추상 클래스와 인터페이스의 혼합 사용
때로는 추상 클래스와 인터페이스를 함께 사용하는 것이 유용할 수 있다. 예를 들어, 특정 클래스는 공통적인 동작을 상속받으면서도 여러 기능 인터페이스를 구현해야 할 경우이다. 이렇게 하면 코드의 유연성과 재사용성을 높일 수 있다.
예시: 추상 클래스와 인터페이스의 혼합
abstract class Machine {
void start();
void stop();
}
class Maintenance {
void service() {
print('Performing maintenance.');
}
}
class Car extends Machine implements Maintenance {
@override
void start() {
print('Car is starting.');
}
@override
void stop() {
print('Car is stopping.');
}
@override
void service() {
print('Car is being serviced.');
}
}
이 예시에서 Car 클래스는 Machine이라는 추상 클래스를 상속받아 공통 기능인 start()와 stop()을 구현한다. 동시에 Maintenance 인터페이스를 구현하여 유지 보수 기능을 포함할 수 있다.
인터페이스의 상속
인터페이스 역시 상속을 통해 확장될 수 있다. 이를 통해 인터페이스 간의 계층 구조를 만들고, 더 복잡한 설계를 할 수 있다.
인터페이스 상속 예시
class Drivable {
void drive();
}
class ElectricDrivable extends Drivable {
void charge();
}
class ElectricCar implements ElectricDrivable {
@override
void drive() {
print('Electric car is driving.');
}
@override
void charge() {
print('Electric car is charging.');
}
}
이 예시에서 ElectricDrivable 인터페이스는 Drivable 인터페이스를 상속하여 확장되었다. ElectricCar는 ElectricDrivable를 구현하여 drive()와 charge() 메소드를 모두 가져오게 된다.
Dart에서의 다중 인터페이스 구현
Dart에서 다중 상속을 허용하지 않기 때문에, 여러 인터페이스를 구현하여 다중 상속의 효과를 낼 수 있다. 이러한 기능은 특정 클래스가 여러 인터페이스의 역할을 동시에 수행해야 할 때 유용하다. 이를 통해 보다 복잡하고 확장 가능한 시스템을 설계할 수 있다.
다중 인터페이스 구현 예시
class Printable {
void printContent();
}
class Scannable {
void scanDocument();
}
class MultifunctionalPrinter implements Printable, Scannable {
@override
void printContent() {
print('Printing document.');
}
@override
void scanDocument() {
print('Scanning document.');
}
}
위 예시에서 MultifunctionalPrinter 클래스는 Printable과 Scannable 두 개의 인터페이스를 구현하여, 출력과 스캔 두 가지 기능을 모두 수행할 수 있다.