Java

[Java] 상속 vs 합성

goblin- 2024. 9. 28. 20:20

**합성(Composition)**은 객체지향 프로그래밍에서 객체가 다른 객체를 포함하여 기능을 재사용하거나 확장하는 기법입니다. 합성은 상속과 달리 클래스 간의 강한 결합을 피하고, 더 유연하고 유지보수하기 쉬운 설계를 제공하는 데 목적이 있습니다.

 

1. 합성의 기본 개념

 

합성은 “has-a” 관계로 설명되며, 이는 한 객체가 다른 객체를 포함하고 있다는 의미입니다. 예를 들어, 자동차(Car) 객체는 엔진(Engine) 객체를 포함할 수 있습니다. 이때 자동차는 엔진을 사용하지만, 엔진과 강하게 결합되지 않고 서로 독립적입니다.

 

합성의 예시

public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

public class Car {
    private Engine engine;  // Car는 Engine을 포함 (has-a 관계)

    public Car() {
        this.engine = new Engine();  // Engine 객체 생성
    }

    public void drive() {
        engine.start();  // 엔진을 사용
        System.out.println("Car is driving");
    }
}

Car 클래스Engine 객체를 포함하여 자동차의 동작을 구현합니다. Car는 엔진의 내부 구현에 의존하지 않고, Engine 객체에 작업을 위임함으로써 기능을 재사용합니다.

 

2. 합성의 장점

 

2.1 유연성

 

합성은 객체 간의 결합도를 낮추어 클래스들이 더 독립적으로 동작할 수 있게 합니다. 합성을 통해 각 구성 요소가 독립적으로 변경될 수 있어, 유지보수확장이 용이합니다.

합성된 객체는 서로 다른 구현체를 쉽게 교체할 수 있습니다. 예를 들어, 자동차의 엔진이 가솔린 엔진에서 전기 엔진으로 교체되더라도 자동차 클래스는 수정되지 않습니다.

 

2.2 상속의 문제 해결

 

상속은 “is-a” 관계로 설계되며, 부모 클래스와 하위 클래스 간의 강한 결합이 발생할 수 있습니다. 상속 관계가 깊어질수록, 부모 클래스의 변경이 하위 클래스에 예기치 못한 영향을 미칠 수 있습니다.

반면 합성은 “has-a” 관계로, 객체가 구성 요소포함하면서도 서로 독립적으로 유지됩니다. 구성 요소가 변경되더라도 기능을 쉽게 교체할 수 있어, 더 유연한 구조를 제공합니다.

 

2.3 코드 재사용성

 

합성을 통해 구현을 재사용할 수 있으며, 구성 요소들을 모듈화하여 다른 클래스에서도 쉽게 사용할 수 있습니다. 재사용 가능한 컴포넌트로 코드를 모듈화할 수 있다는 점이 합성의 큰 장점입니다.

 

3. 상속과 합성의 비교

특징 상속(Ingeritence) 합성(Composition)
관계 "is-a" 관계 "has-a" 관계
결합도 부모와 자식 클래스 간의 강한 결합 포함된 객체를 독립적으로 변경할 수 있음
유연성 부모 클래스가 변경되면 하위 클래스에 영향을 미칠 수 있음 포함된 객체를 독립적으로 변경할 수 있음
다중 상속 자바는 다중 상속이 불가능함 다양한 객체를 포함하여 여러 기능을 동시에 사용할 수 있음
재사용성 부모 클래스의 구현을 재사용 다양한 구성 요소를 모듈화하여 재사용할 수 있음

 

4. 합성의 예시: 다양한 구현체 사용

 

합성의 장점은 구현체를 쉽게 교체할 수 있다는 것입니다. 다음 예시에서는 다양한 엔진 타입을 사용하여 자동차를 구현합니다.

 

다양한 엔진을 사용하는 자동차 예시

// 엔진 인터페이스
public interface Engine {
    void start();
}

// 가솔린 엔진 클래스
public class GasolineEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gasoline engine started");
    }
}

// 전기 엔진 클래스
public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric engine started");
    }
}

// 자동차 클래스는 엔진 객체를 포함함
public class Car {
    private final Engine engine;

    public Car(Engine engine) {  // 엔진 객체를 외부에서 주입받음 (의존성 주입)
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving");
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        Car gasolineCar = new Car(new GasolineEngine());  // 가솔린 엔진 사용
        gasolineCar.drive();  // 출력: Gasoline engine started, Car is driving

        Car electricCar = new Car(new ElectricEngine());  // 전기 엔진 사용
        electricCar.drive();  // 출력: Electric engine started, Car is driving
    }
}

합성을 통해 자동차는 다양한 엔진을 포함할 수 있으며, 객체의 내부 구현을 변경하지 않고 기능을 쉽게 확장할 수 있습니다.

이는 유연한 설계를 가능하게 하고, 새로운 엔진 타입을 추가할 때에도 자동차 클래스를 수정하지 않아도 됩니다.

 

5. 합성의 적용 예시: 스프링 프레임워크에서의 사용

 

스프링 프레임워크에서도 **의존성 주입(DI, Dependency Injection)**이라는 개념을 통해 합성을 구현합니다. 스프링에서는 객체 간의 느슨한 결합을 위해 합성이 많이 사용됩니다.

 

스프링에서의 합성 예시

// Payment 인터페이스
public interface Payment {
    void processPayment(double amount);
}

// CreditCard 결제 방식 구현
public class CreditCardPayment implements Payment {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment: " + amount);
    }
}

// PayPal 결제 방식 구현
public class PaypalPayment implements Payment {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment: " + amount);
    }
}

// 주문 클래스는 결제 방식을 합성하여 사용
public class Order {
    private final Payment payment;  // 결제 방식을 합성

    public Order(Payment payment) {  // 의존성 주입
        this.payment = payment;
    }

    public void placeOrder(double amount) {
        payment.processPayment(amount);
        System.out.println("Order placed successfully.");
    }
}

// 스프링 설정 파일이나 의존성 주입을 통해 합성된 객체 사용
public class Main {
    public static void main(String[] args) {
        Payment creditCard = new CreditCardPayment();
        Order order = new Order(creditCard);
        order.placeOrder(100.0);  // Credit Card 결제 방식으로 주문 처리

        Payment paypal = new PaypalPayment();
        Order anotherOrder = new Order(paypal);
        anotherOrder.placeOrder(200.0);  // PayPal 결제 방식으로 주문 처리
    }
}

스프링에서는 객체 간의 의존성을 생성자 주입 또는 세터 주입으로 해결하며, 이를 통해 합성의 장점을 극대화할 수 있습니다.

 

6. 결론

 

**합성(Composition)**은 객체지향 설계에서 매우 중요한 패턴으로, 유연한 구조재사용 가능한 코드를 작성하는 데 큰 도움이 됩니다. 상속의 남용으로 인한 문제를 해결하고, 구성 요소 간의 느슨한 결합을 유지하면서 기능을 쉽게 확장할 수 있습니다.

 

합성을 사용할 때의 핵심 포인트:

 

1. 유연한 설계: 합성을 통해 객체 간의 결합도를 낮추고, 다른 객체로 쉽게 교체할 수 있습니다.

2. 모듈화: 구성 요소를 모듈화하여 재사용 가능한 코드를 작성할 수 있습니다.

3. 상속 대신 합성: 상속의 복잡성을 줄이고, 더 유연하고 확장 가능한 코드를 작성할 수 있습니다.

 

합성은 특히 스프링 프레임워크와 같은 의존성 주입을 사용하는 시스템에서 매우 효과적으로 사용되며, 구성 요소 간의 관계를 더 느슨하게 관리하는 데 유용합니다.