Menu
Coddy logo textTech

상속보다 합성(Composition)

Coddy C# 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 70개 중 41번째.

상속은 클래스 간의 강력한 결합을 만듭니다. CarVehicle로부터 상속받을 때, Vehicle의 어떠한 변경 사항도 Car를 망가뜨릴 수 있습니다. 합성은 더 유연한 대안을 제공합니다: 동작을 상속받는 대신, 클래스는 해당 동작을 제공하는 객체들을 포함합니다.

이동하고 공격할 수 있는 게임 캐릭터를 생각해 봅시다. 상속을 사용하면 복잡한 계층 구조를 만들게 될 수도 있습니다. 합성을 사용하면 캐릭터에게 필요한 컴포넌트를 단순히 부여하기만 하면 됩니다:

public class MovementBehavior
{
    public void Move() => Console.WriteLine("Moving...");
}

public class AttackBehavior
{
    public void Attack() => Console.WriteLine("Attacking!");
}

public class Character
{
    private MovementBehavior movement = new MovementBehavior();
    private AttackBehavior attack = new AttackBehavior();
    
    public void Move() => movement.Move();
    public void Attack() => attack.Attack();
}

핵심적인 차이점은 관계에 있습니다. 상속은 "is-a" 관계(개는 동물이다)를 나타내는 반면, 구성(Composition)은 "has-a" 관계(캐릭터는 이동 동작을 가지고 있다)를 나타냅니다. 구성을 사용하면 런타임에 동작을 교체하고, 기능을 자유롭게 결합하며, 취약한 기본 클래스 문제(fragile base class problem)를 피할 수 있습니다.

진정한 계층 관계가 있을 때 상속을 사용하세요. 유연성이 필요하거나, 서로 관련 없는 클래스들 사이에 동작이 공유될 수 있거나, 동작을 동적으로 변경하고 싶을 때는 합성을 선택하세요.

challenge icon

챌린지

쉬움

상속보다 구성(composition)의 강력함을 보여주는 알림 시스템을 구축해 보겠습니다. 복잡한 알림 클래스 계층 구조를 만드는 대신, 교체 가능한 동작 컴포넌트들을 부여하여 NotificationSender를 구성하게 됩니다.

코드는 세 개의 파일로 구성됩니다:

  • Behaviors.cs: 알림 전송의 서로 다른 측면을 처리하는 두 개의 동작 클래스를 Notifications 네임스페이스에 생성합니다:
    • FormattingBehavior - 메시지 시작 부분에 [ALERT] 를, 끝 부분에 [END]를 붙여서 반환하는 Format(string message) 메서드를 가집니다.
    • DeliveryBehavior - Sending via {channel}: {message} 형식의 문자열을 반환하는 Deliver(string channel, string message) 메서드를 가집니다.
  • NotificationSender.cs: 구성을 사용하여 동작들을 결합하는 NotificationSender 클래스를 Notifications 네임스페이스에 생성합니다. 이 클래스는 다음을 수행해야 합니다:
    • FormattingBehaviorDeliveryBehavior의 인스턴스를 프라이빗 필드로 포함합니다.
    • 전송 채널 이름을 저장하는 Channel 속성(string)을 가집니다.
    • 채널 이름을 매개변수로 받는 생성자를 가집니다.
    • Send(string message) 메서드를 가집니다. 이 메서드는 먼저 formatting 동작을 사용하여 메시지 형식을 지정한 다음, delivery 동작을 사용하여 채널과 함께 전송하고 최종 결과를 반환합니다.
  • Program.cs: 메인 파일에서 입력받은 채널로 NotificationSender를 생성한 후, 입력받은 메시지를 전송합니다. 구성된 동작들이 어떻게 함께 작동하는지 보여주기 위해 결과를 출력합니다.

두 개의 입력을 받게 됩니다:

  • 전송 채널 이름 (예: Email, SMS)
  • 전송할 메시지

알림 전송 객체의 Send()를 호출한 결과를 출력하세요.

예를 들어, 입력이 EmailServer is down이라면, 출력은 다음과 같아야 합니다:

Sending via Email: [ALERT] Server is down [END]

NotificationSender가 아무것도 상속받지 않는다는 점에 주목하세요. 이 클래스는 단순히 포맷팅 및 전송 동작을 "가지고(has)" 있을 뿐입니다. 이러한 구성 방식은 전송 클래스를 변경하지 않고도 두 동작 중 하나를 쉽게 교체하거나, 이러한 동작들을 완전히 다른 클래스에서 재사용할 수 있음을 의미합니다!

치트 시트

컴포지션(Composition)은 상속의 대안으로, 부모 클래스로부터 동작을 상속받는 대신 클래스가 동작을 제공하는 객체를 포함하게 함으로써 더 유연한 코드를 생성합니다.

상속 vs 컴포지션:

  • 상속은 "is-a" 관계를 나타냅니다 (Dog는 Animal입니다)
  • 컴포지션은 "has-a" 관계를 나타냅니다 (Character는 이동 동작을 가지고 있습니다)

컴포지션의 장점:

  • 런타임에 동작을 교체할 수 있음
  • 기능을 자유롭게 조합할 수 있음
  • 강한 결합(tight coupling)과 취약한 기본 클래스(fragile base class) 문제를 방지함
  • 관련 없는 클래스 간에 동작을 공유할 수 있음

동작 컴포넌트를 사용한 예시:

public class MovementBehavior
{
    public void Move() => Console.WriteLine("Moving...");
}

public class AttackBehavior
{
    public void Attack() => Console.WriteLine("Attacking!");
}

public class Character
{
    private MovementBehavior movement = new MovementBehavior();
    private AttackBehavior attack = new AttackBehavior();
    
    public void Move() => movement.Move();
    public void Attack() => attack.Attack();
}

각 접근 방식을 사용하는 시기:

  • 진정한 계층적 관계에는 상속을 사용하세요
  • 유연성, 공유 동작 또는 동적인 동작 변경이 필요한 경우에는 컴포지션을 선택하세요

직접 해보기

using System;
using Notifications;

class Program
{
    public static void Main(string[] args)
    {
        // 입력 읽기
        string channel = Console.ReadLine();
        string message = Console.ReadLine();

        // TODO: channel을 사용하여 NotificationSender 생성
        
        // TODO: 메시지를 전송하고 결과 출력
        
    }
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨