객체지향 프로그래밍에서 상속(
Inheritance
)은 클래스(Class
) 간에 관계를 형성하는 기능입니다.
상속을 사용하면 기존 클래스를 기반으로 새로운 클래스를 작성할 수 있습니다.
이를 통해 중복 코드를 줄이고, 코드의 재사용성을 높일 수 있습니다.
상속을 사용하면 상위 클래스(Superclass
)의 속성과 메서드를 하위 클래스(Subclass
)가 물려받아 사용할 수 있습니다.
상위 클래스에서 정의된 속성과 메서드는 하위 클래스에서 다시 정의하지 않아도 사용할 수 있으며, 상위 클래스에서 정의된 속성과 메서드를 재사용하여 하위 클래스를 작성할 수 있습니다.
상속을 사용하는 가장 일반적인 예시는 extends
키워드를 사용하여 하위 클래스를 정의하는 것입니다.
예를 들어, 다음은 TypeScript
로 작성된 Animal
클래스와 Dog
클래스입니다.
class Animal {
private name: string;
constructor(name: string) {
this.name = name;
}
public speak(): void {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
public speak(): void {
console.log(`${this.name} barks.`);
}
}
const dog1 = new Dog("Rufus");
dog1.speak(); // Rufus barks.
위 예시에서 Animal
클래스는 name
속성을 가지며, speak
메서드를 가지고 있습니다.
Dog
클래스는 Animal
클래스를 상속하며, speak
메서드를 다시 정의합니다.
dog1
은 Dog
클래스의 인스턴스로, speak
메서드를 사용하여 객체의 동작을 수행합니다.
상속을 사용하면 상위 클래스에서 정의된 속성과 메서드를 하위 클래스에서 재사용하여 코드를 간결하게 작성할 수 있습니다.
하지만 상속은 상위 클래스와 하위 클래스 간의 결합도(Coupling)를 높이는 결과를 가져올 수 있으므로, 상속을 남발하지 않고 적절한 상황에서 사용하는 것이 좋습니다.
상속은 부모 클래스와 자식 클래스 간의 결합도(Coupling)를 높일 수 있습니다.
결합도는 두 모듈 간의 상호 의존도를 나타내는 개념으로, 결합도가 높을수록 모듈 간의 의존성이 강해지며, 하나의 모듈을 변경할 때 다른 모듈에도 영향을 미칠 가능성이 높아집니다.
예를 들어, 다음은 TypeScript
로 작성된 Person
클래스와 Employee
클래스입니다.
class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public getName(): string {
return this.name;
}
public setName(name: string): void {
this.name = name;
}
public getAge(): number {
return this.age;
}
public setAge(age: number): void {
this.age = age;
}
}
class Employee extends Person {
private employeeId: number;
constructor(name: string, age: number, employeeId: number) {
super(name, age);
this.employeeId = employeeId;
}
public getEmployeeId(): number {
return this.employeeId;
}
public setEmployeeId(employeeId: number): void {
this.employeeId = employeeId;
}
}
위 예시에서 Person
클래스는 name
과 age
속성을 가지며, getName
, setName
, getAge
, setAge
메서드를 가지고 있습니다.
Employee
클래스는 Person
클래스를 상속하며, employeeId
속성과 getEmployeeId
, setEmployeeId
메서드를 가지고 있습니다.
이 예시에서 Employee
클래스는 Person
클래스를 상속받았기 때문에, Employee
클래스는 Person
클래스의 속성과 메서드를 사용할 수 있습니다.
하지만 Employee
클래스는 Person
클래스에 강하게 결합되어 있습니다.
만약 Person
클래스를 수정하면, Employee
클래스도 영향을 받을 수 있으며, 이를 방지하기 위해서는 Person
클래스와 Employee
클래스를 느슨하게 결합해야 합니다.
상속을 사용할 때는 결합도가 높아지지 않도록 주의해야 하며, 상속보다는 인터페이스(Interface
)나 믹스인(Mixin
) 등의 기술을 사용하여 느슨한 결합을 유지하는 것이 좋습니다.
인터페이스는 클래스가 구현해야 하는 메서드의 이름과 타입을 정의합니다.
이를 통해 여러 클래스에서 같은 인터페이스를 구현하도록 하여 코드의 재사용성을 높일 수 있습니다.
다음은 TypeScript
로 작성된 Person
클래스와 Employee
클래스를 인터페이스를 사용하여 수정한 예시입니다.
interface IPerson {
getName(): string;
setName(name: string): void;
getAge(): number;
setAge(age: number): void;
}
class Person implements IPerson {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public getName(): string {
return this.name;
}
public setName(name: string): void {
this.name = name;
}
public getAge(): number {
return this.age;
}
public setAge(age: number): void {
this.age = age;
}
}
class Employee implements IPerson {
private employeeId: number;
private person: Person;
constructor(name: string, age: number, employeeId: number) {
this.person = new Person(name, age);
this.employeeId = employeeId;
}
public getName(): string {
return this.person.getName();
}
public setName(name: string): void {
this.person.setName(name);
}
public getAge(): number {
return this.person.getAge();
}
public setAge(age: number): void {
this.person.setAge(age);
}
public getEmployeeId(): number {
return this.employeeId;
}
public setEmployeeId(employeeId: number): void {
this.employeeId = employeeId;
}
}
위 예시에서 IPerson
인터페이스는 Person
클래스와 Employee
클래스에서 구현해야 하는 메서드를 정의합니다.
Person
클래스는 IPerson
인터페이스를 구현하며, Employee
클래스는 IPerson
인터페이스와 Person
클래스를 사용하여 구현됩니다.
이를 통해 Employee
클래스는 Person
클래스와 느슨하게 결합되며, Person
클래스의 변경이 Employee
클래스에 영향을 미치지 않도록 할 수 있습니다.
믹스인(Mixin)은 객체 지향 프로그래밍에서 다중 상속을 구현하는 방법 중 하나입니다.
다중 상속이란, 하나의 클래스가 여러 개의 클래스로부터 상속받는 것을 말합니다.
다중 상속은 다양한 문제점을 가지고 있기 때문에 일부 언어에서는 지원하지 않거나 제한적으로 지원합니다.
믹스인은 이러한 문제점을 해결하기 위한 방법 중 하나로, 다중 상속의 일부 기능만을 제공하면서도 코드의 재사용성을 높이는 방법입니다.
믹스인은 클래스가 아니라 재사용 가능한 메소드와 프로퍼티들의 집합이며, 다른 클래스에서 이를 재사용할 수 있습니다.
믹스인을 사용하면 코드 중복을 줄이고, 클래스 간의 의존성을 낮출 수 있습니다.
믹스인은 보통 인터페이스를 사용하여 정의하며, 인터페이스를 상속받는 클래스에서 믹스인의 메소드와 프로퍼티를 구현함으로써 사용합니다.
JavaScript
와TypeScript
언어에서는 믹스인을 지원하지 않습니다.
하지만, 이를 구현하기 위한 다양한 방법이 있습니다.
JavaScript에서는 객체 병합(Object merging)
을 이용하여 믹스인을 구현할 수 있습니다.
예를 들어, 다음과 같은 두 개의 객체가 있다고 가정해보겠습니다.
const user = {
name: 'Alice',
age: 30
};
const logger = {
log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
};
이제, 이 두 개의 객체를 병합하여 새로운 객체를 생성하는 함수를 작성해보겠습니다.
function mixin(target, ...sources) {
Object.assign(target, ...sources);
}
위 함수는 첫 번째 인수로 타겟 객체를 받고, 나머지 인수로 병합할 소스 객체들을 받습니다. 이후, Object.assign
메소드를 사용하여 타겟 객체에 소스 객체들을 병합합니다.
이제, mixin 함수를 사용하여 믹스인을 구현할 수 있습니다.
const myClass = {};
mixin(myClass, user, logger);
console.log(myClass); // { name: 'Alice', age: 30, log: [Function: log] }
위 코드에서는 mixin
함수를 사용하여 user
와 logger
객체를 myClass
객체에 병합하였습니다. 이를 통해 myClass
객체는 name
, age
, log
메소드를 모두 갖게 됩니다.
TypeScript
에서는 mixin
을 구현하기 위해 클래스를 사용할 수 있습니다.
TypeScript
에서는 인터페이스와 클래스를 함께 사용하여 mixin을 구현할 수 있습니다.
예를 들어, 다음과 같은 인터페이스와 클래스가 있다고 가정해보겠습니다.
interface User {
name: string;
age: number;
}
class Logger {
log(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
이제, 이 두 개의 클래스와 인터페이스를 함께 사용하여 믹스인을 구현하는 예시를 보겠습니다.
class MyClass implements User {
name = 'Alice';
age = 30;
}
interface MyClass extends Logger {}
const myClass = new MyClass();
myClass.log('Hello, world!'); // [2022-02-28T08:55:21.479Z] Hello, world!
위 코드에서는 MyClass
클래스가 User
인터페이스를 구현하며, MyClass
클래스에 Logger
클래스를 믹스인합니다. 이를 통해 myClass
객체는 name
, age
, log
메소드를 모두 갖게 됩니다.
하지만, 이러한 방법은 TypeScript
의 클래스 데코레이터를 이용하면 더욱 효율적으로 구현할 수 있습니다. 클래스 데코레이터를 사용하면 코드의 가독성과 유지보수성이 향상될 수 있습니다.
TypeScript
에서 클래스 데코레이터를 이용해 믹스인을 구현하는 방법은 다음과 같습니다.
abstract class Animal {
abstract makeSound(): void;
}
function withSleep<T extends { new(...args: any[]): {} }>(Base: T) {
return class extends Base {
sleep() {
console.log('zzz...');
}
}
}
위 코드에서는 withSleep
데코레이터 함수를 정의하였습니다. 이 함수는 제네릭 타입 T
를 인수로 받습니다. T
는 클래스 타입이어야 하며, 생성자 함수의 시그니처를 나타내는 { new(...args: any[]): {} }
타입입니다. 이 함수는 Base
클래스를 상속받은 새로운 클래스를 반환합니다. 반환된 클래스에는 sleep
메소드가 추가됩니다.
데코레이터를 적용할 클래스를 정의합니다.
@withSleep
class Dog extends Animal {
makeSound() {
console.log('woof');
}
}
위 코드에서는 Dog
클래스에 withSleep
데코레이터를 적용하였습니다. 이를 통해 Dog
클래스는 Animal
클래스와 sleep
메소드를 모두 상속받게 됩니다.
데코레이터를 적용한 클래스를 사용합니다.
const dog = new Dog();
dog.makeSound(); // 'woof'
dog.sleep(); // 'zzz...'
위 코드에서는 새로운 Dog
객체를 생성하고, makeSound
와 sleep
메소드를 호출하였습니다.
이와 같이, TypeScript
에서 클래스 데코레이터를 이용하여 믹스인을 구현할 수 있습니다. 이를 통해 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.
믹스인은 상속보다는 구성을 사용해야 한다.
믹스인은 클래스 간의 상속 대신 구성(composition)을 사용하여 구현해야 합니다. 이는 클래스 간의 의존성을 낮추고, 코드의 재사용성을 높이는 데 도움이 됩니다.
예를 들어, 쇼핑몰 웹 애플리케이션을 만든다고 가정해보겠습니다. 이 애플리케이션에서는 상품을 나타내는 Product
클래스와 그 하위 클래스인 Book
클래스와 Electronic
클래스가 필요합니다. 이때, Book
과 Electronic
클래스는 Product
클래스에서 상속받을 수 있습니다.
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
class Book extends Product {
author: string;
constructor(name: string, price: number, author: string) {
super(name, price);
this.author = author;
}
}
class Electronic extends Product {
manufacturer: string;
constructor(name: string, price: number, manufacturer: string) {
super(name, price);
this.manufacturer = manufacturer;
}
}
위 코드에서 Book
과 Electronic
클래스는 Product
클래스를 상속받아 생성되었습니다. 이를 상속을 이용한 코드 재사용성이라고 할 수 있습니다.
하지만, 이러한 상속 구조는 각 클래스 간의 의존성을 높이고, 유지보수가 어려워질 수 있습니다.
예를 들어, Product
클래스에서 변경이 일어날 경우, 그 변경이 하위 클래스인 Book
과 Electronic
클래스에도 영향을 미치게 됩니다.
이를 해결하기 위해, 구성(composition)을 사용하여 코드를 재구성할 수 있습니다.
즉, Product
클래스가 Book
과 Electronic
클래스를 내부적으로 포함하도록 변경하는 것입니다.
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
class Book {
product: Product;
author: string;
constructor(name: string, price: number, author: string) {
this.product = new Product(name, price);
this.author = author;
}
}
class Electronic {
product: Product;
manufacturer: string;
constructor(name: string, price: number, manufacturer: string) {
this.product = new Product(name, price);
this.manufacturer = manufacturer;
}
}
위 코드에서 Product
클래스는 그대로 유지되었고, Book
과 Electronic
클래스는 Product
클래스를 내부적으로 포함하도록 변경되었습니다.
이를 구성을 이용한 코드 재사용성이라고 할 수 있습니다.
이제, Product
클래스에서 변경이 일어나더라도 Book
과 Electronic
클래스에는 영향을 미치지 않습니다.
또한, Book
과 Electronic
클래스는 각각 Product
클래스를 포함하기 때문에, 각 클래스 간의 의존성이 낮아지고, 유지보수가 용이해집니다.
또한, 각 클래스의 역할이 분리되어 있어서 코드의 가독성이 높아집니다.
상속과 구성은 각각 장단점이 있기 때문에, 어떤 방법을 선택할지는 상황에 따라 다릅니다.
일반적으로는 구성을 사용하는 것이 상속보다 더 안전하고 유연한 방법이라고 할 수 있습니다.
하지만, 구성을 사용하면 코드의 복잡성이 높아질 수 있으며, 일부 상황에서는 상속을 사용하는 것이 더 적합할 수도 있습니다.
믹스인 간에 이름 충돌이 일어날 수 있다.
믹스인은 여러 개의 클래스에서 재사용될 수 있기 때문에, 다른 믹스인에서 이미 사용한 이름과 충돌할 수 있습니다.
이러한 경우에는 이름 충돌을 방지하기 위해 네임스페이스를 사용하거나, 믹스인에서 이름을 변경해야 할 수 있습니다.
예를 들어, TypeScript
에서 다음과 같이 두 개의 믹스인을 정의해보겠습니다.
interface User {
name: string;
age: number;
}
interface Logger {
log(message: string): void;
}
class MyClass implements User, Logger {
name = 'Alice';
age = 30;
log(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
위 코드에서 User
와 Logger
믹스인은 각각 이름과 나이, 그리고 로깅 기능을 제공합니다.
MyClass
클래스는 User
와 Logger
믹스인을 구현한 클래스이며, 이를 통해 이름, 나이, 로깅 기능을 모두 갖게 됩니다.
하지만, 이 코드에서는 User
와 Logger
믹스인에서 동일한 이름을 가진 메소드가 존재할 때, 이름 충돌이 일어날 수 있습니다.
예를 들어, Logger
믹스인에서 log
메소드를 다시 정의한다면 다음과 같은 코드가 될 수 있습니다.
interface User {
name: string;
age: number;
}
interface Logger {
log(message: string): void;
}
interface Logger {
logError(error: Error): void;
}
class MyClass implements User, Logger {
name = 'Alice';
age = 30;
log(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
logError(error: Error) {
console.log(`[${new Date().toISOString()}] ${error.message}`);
}
}
위 코드에서 Logger
믹스인에서 log
메소드를 다시 정의하면, MyClass
클래스에서는 어떤 log
메소드를 사용해야 할지 알 수 없게 됩니다.
이러한 경우에는 이름 충돌을 방지하기 위해 네임스페이스를 사용하거나, 메소드의 이름을 변경하는 등의 방법을 사용해야 합니다.
예를 들어, Logger
믹스인에서 log
메소드를 logMessage
로 변경한다면, 다음과 같이 코드를 수정할 수 있습니다.
interface User {
name: string;
age: number;
}
interface Logger {
logMessage(message: string): void;
}
interface Logger {
logError(error: Error): void;
}
class MyClass implements User, Logger {
name = 'Alice';
age = 30;
logMessage(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
logError(error: Error) {
console.log(`[${new Date().toISOString()}] ${error.message}`);
}
}
위 코드에서 Logger
믹스인에서 log
메소드를 logMessage
로 변경하면, MyClass
클래스에서는 각각의 메소드에 대해 이름 충돌이 일어나지 않게 됩니다.
믹스인 간에 상속 관계가 생길 수 있다.
믹스인을 여러 개 사용하다 보면, 믹스인 간에도 상속 관계가 생길 수 있습니다.
이러한 경우에는 상속 관계를 만들지 않도록 주의해야 합니다.
또한, 믹스인 간의 순서에 따라 동작이 달라질 수 있기 때문에, 순서를 고려하여 사용해야 합니다.
믹스인을 과도하게 사용하지 말아야 한다.
믹스인을 과도하게 사용하면, 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.
믹스인은 코드의 재사용성을 높이기 위한 방법으로 사용해야 하며, 필요한 경우에만 사용해야 합니다.
믹스인이 사용되는 언어에서 지원하지 않는 경우가 있다.(JavaScript 와 TypeScript 도 지원하지 않습니다.)
몇몇 언어에서는 믹스인을 지원하지 않거나, 제한적으로 지원하는 경우가 있습니다.
이 경우에는 다른 방법으로 코드의 재사용성을 높일 수 있습니다.
상속과 구성은 각각 장단점이 있기 때문에, 어떤 방법이 적합한지는 상황에 따라 다르기 때문에, 코드의 특성과 요구사항에 따라 선택해야 합니다.
그러나 일반적으로는 구성이 상속보다 더 안전하고 유연한 방법이라고 할 수 있습니다.
상속을 사용하면 부모 클래스와 하위 클래스 간에 강한 의존성이 생기기 때문에, 부모 클래스에서 변경이 일어날 경우, 하위 클래스에도 영향을 미치게 됩니다.
또한, 상속을 남발하면 클래스 간의 계층 구조가 복잡해지고 유지보수가 어려워질 수 있습니다.
반면에 구성은 부품을 조합하는 방식으로 코드를 작성하기 때문에, 클래스 간의 의존성이 낮아지며, 유지보수가 용이해집니다.
또한, 각 클래스의 역할이 분리되어 있어서 코드의 가독성이 높아지는 장점이 있습니다.
하지만, 구성을 사용하면 코드의 복잡성이 높아질 수 있으며, 클래스 간의 상호작용이 많아져서 디버깅이 어려워질 수도 있습니다.
또한, 구성을 사용하면 객체를 생성하는 비용이 높아질 수 있습니다.
따라서, 상속과 구성을 사용할 때는 코드의 특성과 요구사항에 따라 선택해야 합니다.
일반적으로는 구성을 사용하는 것이 좀 더 안전하고 유연한 방법이라고 할 수 있지만, 간혹 상속을 사용하는 것이 더 적합한 경우도 있습니다.
상속은 일반적으로 다음과 같은 경우에 사용됩니다.
반면에 구성은 다음과 같은 경우에 사용됩니다.
따라서, 코드의 특성과 요구사항에 따라 상속과 구성을 선택해야 합니다.
일반적으로는 상속은 계층 구조가 간단하고 공통된 속성과 기능이 많은 경우에 사용하는 것이 적합하며,
구성은 클래스 간의 관계가 복잡하거나 런타임에 동적으로 객체를 생성해야 하는 경우에 사용하는 것이 적합합니다.