구성과 상속의 차이

구성상속의 차이

객체지향 프로그래밍에서 상속Inheritance과 구성Composition은 객체 간 관계를 설정하는 두 가지 주요 방식입니다. 두 가지 모두 객체 간 협력을 설계하는 강력한 방법이지만, 각각 장단점이 있으며, 상황에 따라 적절한 선택이 필요합니다.

상속의 특징과 단점

상속Inheritance은 객체 간 IS-A 관계를 표현하며, 하위 클래스가 상위 클래스의 속성과 메서드를 물려받아 재사용할 수 있는 특징을 가지고 있습니다. 이를 통해 코드 재사용성과 일관성을 높일 수 있지만, 강한 결합과 유연성 부족이라는 단점이 있습니다.

  • 강한 결합: 상위 클래스가 변경되면, 하위 클래스에 영향을 미치게 되어 유지보수성이 떨어집니다.
  • 유연성 부족: 상위 클래스의 모든 기능을 물려받아야 하므로, 필요 없는 기능도 상속되어 복잡성이 증가할 수 있습니다.
// 상위 클래스 Library
public class Library
{
    public virtual void AddBook() => Console.WriteLine("Book added to library.");
    public virtual void BorrowBook() => Console.WriteLine("Book borrowed from library.");
    // 모든 도서관이 전자책을 지원하지 않음에도 불구하고, 상속 구조 상 포함됨
    public virtual void DownloadEBook() => Console.WriteLine("E-Book downloaded from library.");
}
// 하위 클래스 PhysicalLibrary, 전자책 기능이 필요 없지만 상속 구조로 인해 물려받음
public class PhysicalLibrary : Library
{
    public override void AddBook() =>         Console.WriteLine("Physical book added to library.");
    public override void BorrowBook() => Console.WriteLine("Physical book borrowed from library.");
    
    // DownloadEBook 메서드는 필요 없지만 상속받아 물려받음
}

이 예제에서 PhysicalLibrary는 실제 도서만 취급하지만, 상위 클래스 Library에서 전자책 다운로드 메서드(DownloadEBook)를 상속받게 됩니다. 이는 상속의 강한 결합 문제를 보여주며, PhysicalLibrary가 필요하지 않은 기능을 물려받고, 불필요한 메서드가 포함되는 단점이 발생합니다.

구성이 상속의 단점을 해결하는 방식

구성Composition은 HAS-A 관계를 나타냅니다. 한 객체가 다른 객체를 포함하고, 그 객체의 기능을 활용하는 방식입니다. 상속에 비해 구성은 객체 간의 결합도를 낮추고, 더 유연한 설계를 가능하게 합니다.

  • 느슨한 결합: 구성된 객체는 서로 독립적이므로, 한 객체가 변경되더라도 다른 객체에 영향을 미치지 않습니다. 이는 시스템의 유지보수를 용이하게 만듭니다.
  • 유연성: 객체 간의 관계를 런타임에 동적으로 변경할 수 있으며, 객체가 다른 객체의 기능을 사용할 때 필요한 기능만 선택적으로 포함할 수 있습니다.
  • 재사용성: 구성은 객체가 서로 독립적으로 존재하므로, 같은 기능을 여러 객체에서 필요할 때 쉽게 재사용할 수 있습니다.
// 인터페이스 정의: 도서관 기능을 개별적으로 나누어 필요에 맞게 구성
public interface ILibraryAction
{
    void AddBook();
    void BorrowBook();
}
public interface IEBookSupport
{
    void DownloadEBook();
}
// 실물 도서관 기능
public class PhysicalLibraryAction : ILibraryAction
{
    public void AddBook() => Console.WriteLine("Physical book added to library.");
    public void BorrowBook() => Console.WriteLine("Physical book borrowed from library.");
}
// 전자책 지원 도서관 기능
public class EBookSupport : IEBookSupport
{
    public void DownloadEBook() => Console.WriteLine("E-Book downloaded from library.");
}
// 도서관 클래스는 구성된 객체에 기능을 위임
public class Library
{
    private ILibraryAction libraryAction;
    private IEBookSupport ebookSupport;
    // 도서관 구성은 기능에 따라 동적으로 선택 가능
    public Library(ILibraryAction action, IEBookSupport ebook = null)
    {
        libraryAction = action;
        ebookSupport = ebook;
    }
    public void AddBook() => libraryAction.AddBook();
    public void BorrowBook() => libraryAction.BorrowBook();
    public void DownloadEBook()
    {
        if (ebookSupport != null)
        {
            ebookSupport.DownloadEBook();
        }
        else
        {
            Console.WriteLine("This library does not support E-Books.");
        }
    }
}
// 사용 예시
var physicalLibrary = new Library(new PhysicalLibraryAction());
physicalLibrary.AddBook();       // "Physical book added to library."
physicalLibrary.BorrowBook();    // "Physical book borrowed from library."
physicalLibrary.DownloadEBook(); // "This library does not support E-Books."
var ebookLibrary = new Library(new PhysicalLibraryAction(), new EBookSupport());
ebookLibrary.DownloadEBook();    // "E-Book downloaded from library."

이 예제에서는 구성을 사용해 Library는 필요한 기능만 동적으로 주입받습니다. PhysicalLibrary는 전자책 기능이 필요 없으므로 EBookSupport를 주입하지 않고, 필요한 기능만 사용합니다. 이를 통해 상속의 강한 결합 문제를 해결하고, 유연성을 확보할 수 있습니다.

상속과 구성이 적합한 경우

상속과 구성은 각기 다른 상황에서 사용될 수 있으며, 적절한 설계 선택이 필요합니다.

상속이 적합한 경우

  • IS-A 관계가 명확할 때: 하위 클래스가 상위 클래스의 모든 기능을 그대로 물려받아야 하고, IS-A 관계가 자연스러울 때 상속이 적합합니다. 예를 들어, Bird 클래스와 Sparrow 클래스의 관계는 상속으로 표현하는 것이 적합합니다.
  • 다형성을 활용할 때: 상속을 사용하여 공통 인터페이스를 구현하거나, 상위 클래스에서 정의된 메서드를 하위 클래스에서 재정의Override하여 다형성을 구현할 수 있습니다.

구성이 적합한 경우

  • HAS-A 관계가 더 적합할 때: 객체가 다른 객체의 기능을 사용할 필요가 있지만, 고정된 상속 구조가 아니라 필요에 따라 동적으로 포함할 수 있는 관계가 필요할 때 구성이 적합합니다. 예를 들어, 도서관(Library)이 여러 책(Book)을 포함하는 관계는 구성으로 표현할 수 있습니다.
  • 유연한 객체 교체: 런타임에 객체 간의 관계를 동적으로 변경하거나, 다른 객체로 교체할 필요가 있을 때 구성은 매우 유용합니다.
  • 강한 결합을 피하고자 할 때: 상속을 사용하면 상위 클래스에 대한 의존도가 높아지지만, 구성은 객체 간 결합을 느슨하게 유지하면서도 기능을 확장할 수 있습니다.

다형성의 역할

다형성Polymorphism은 객체가 동일한 인터페이스를 통해 다양한 방식으로 동작할 수 있는 객체지향의 중요한 원칙입니다. 다형성은 상속과 구성 모두에서 사용될 수 있으며, 시스템의 유연성을 극대화하는 데 기여합니다.

상속에서의 다형성

상속은 다형성을 자연스럽게 제공합니다. 하위 클래스는 상위 클래스의 메서드를 재정의override하여, 동일한 인터페이스를 통해 다른 방식으로 동작할 수 있습니다.

// 상위 클래스 Book
public class Book
{
    public virtual void PrintInfo() => Console.WriteLine("Book information.");
}
// 하위 클래스 PhysicalBook
public class PhysicalBook : Book
{
    public override void PrintInfo() => Console.WriteLine("Physical book information.");
}
// 하위 클래스 EBook
public class EBook : Book
{
    public override void PrintInfo() => Console.WriteLine("E-Book information.");
}
// 사용 예시
Book physicalBook = new PhysicalBook();
Book ebook = new EBook();
physicalBook.PrintInfo();  // "Physical book information."
ebook.PrintInfo();         // "E-Book information."

이 예제는 상속을 통해 다형성을 활용한 예시입니다. Book 클래스의 하위 클래스들은 각자 다른 방식으로 PrintInfo 메서드를 구현하고 있습니다. 이를 통해 동일한 인터페이스를 통해 다양한 객체가 다른 동작을 수행할 수 있습니다.

구성에서의 다형성

구성에서도 다형성을 활용할 수 있습니다. 구성된 객체가 특정 인터페이스를 구현하면, 상속 없이도 동일한 방식으로 동작을 변경할 수 있습니다. 이를 통해 구성된 객체의 동작을 쉽게 교체할 수 있습니다.

// 인터페이스 정의
public interface IBook
{
    void PrintInfo();
}
// 실물 책 클래스
public class PhysicalBook : IBook
{
    public void PrintInfo() => Console.WriteLine("Physical book information.");
}
// 전자책 클래스
public class EBook : IBook
{
    public void PrintInfo() => Console.WriteLine("E-Book information.");
}
// 도서관 클래스는 IBook 인터페이스를 구현한 객체에 의존
public class Library
{
    private List<IBook> books = new List<IBook>();
    public void AddBook(IBook book) => books.Add(book);
    public void PrintAllBooks()
    {
        foreach (var book in books)
        {
            book.PrintInfo();
        }
    }
}
// 사용 예시
Library library = new Library();
library.AddBook(new PhysicalBook());
library.AddBook(new EBook());
library.PrintAllBooks();
// 출력:
// "Physical book information."
// "E-Book information."

이 예제는 구성을 통한 다형성을 보여줍니다. Library 클래스는 IBook 인터페이스를 구현한 객체들과 협력하며, 상속이 아닌 구성을 통해 다양한 책 객체를 처리합니다. 런타임에 어떤 객체를 주입하느냐에 따라 다형성이 발휘됩니다.

맺음말

상속과 구성은 각각 장단점이 있으며, 상황에 따라 적절한 방식을 선택하는 것이 중요합니다. 상속은 코드의 재사용성을 극대화할 수 있지만, 강한 결합과 유연성 부족이라는 단점이 있을 수 있습니다. 반면, 구성은 느슨한 결합을 제공하고, 객체를 동적으로 변경할 수 있어 더 유연한 설계를 가능하게 합니다. 다형성은 두 방식 모두에서 중요한 역할을 하며, 상속과 구성에서 각각 어떻게 적용될 수 있는지 이해하는 것이 시스템 설계에 큰 도움이 됩니다. 실무에서는 상속과 구성을 적절히 조합하여 유지보수성과 확장성이 높은 설계를 목표로 해야 합니다.