[Java] 상속 vs 합성
**합성(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. 상속 대신 합성: 상속의 복잡성을 줄이고, 더 유연하고 확장 가능한 코드를 작성할 수 있습니다.
합성은 특히 스프링 프레임워크와 같은 의존성 주입을 사용하는 시스템에서 매우 효과적으로 사용되며, 구성 요소 간의 관계를 더 느슨하게 관리하는 데 유용합니다.