상속 조합을 사용하는 상황 구분
상속(Inheritance)
- 상속은 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 관계를 의미한다.
- 상속은 확장을 고려하고 설계가 확실한 "is-a" 관계일 때와, API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파되어도 괜찮은 경우에 사용한다.
- 예: Bird 클래스는 Animal 클래스를 상속받을 수 있다. 이는 "새는 동물이다"라는 관계를 표현한다.
조합(Composition)
- 조합은 새로운 클래스가 기존 클래스를 포함하는 관계를 의미한다. 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조한다.
- 조합은 "has-a" 관계를 나타낼 때 사용한다.
- 예: Car 클래스가 Engine 클래스를 포함할 수 있다. 이는 "자동차는 엔진을 가지고 있다"라는 관계를 표현한다.
상속을 사용할 때 발생할 수 있는 문제점과 해결을 위한 디자인 패턴
문제점
- 강한 결합: 부모 클래스와 자식 클래스 간의 강한 결합으로 인해 부모 클래스의 변경이 자식 클래스에 영향을 미친다.
- 확장성 제한: 새로운 기능 추가 시 상속 구조가 복잡해질 수 있다.
- 다중 상속 문제: 자바는 다중 상속을 지원하지 않으므로, 여러 클래스를 상속받아야 하는 경우 곤란할 수 있다.
해결을 위한 디자인 패턴
1. 템플릿 메소드 패턴: 상속을 통해 알고리즘의 구조를 정의하고, 세부 구현은 자식 클래스에서 수행하도록 한다.
abstract class Animal {
// 템플릿 메서드
public final void dailyRoutine() {
wakeUp();
eat();
move();
sleep();
}
abstract void wakeUp();
abstract void eat();
abstract void move();
abstract void sleep();
}
class Bird extends Animal {
@Override
void wakeUp() { System.out.println("Bird wakes up."); }
@Override
void eat() { System.out.println("Bird eats."); }
@Override
void move() { System.out.println("Bird files."); }
@Override
void sleep() { System.out.println("Bird sleeps."); }
}
2. 전략 패턴: 상속 대신 조합을 통해 행위를 캡슐화하고, 런타임에 행위를 변경할 수 있도록 한다.
interface MoveStrategy {
void move();
}
class FlyStrategy implements MoveStrategy {
public void move() { System.out.println("Flies."); }
}
class WalkStrategy implements MoveStrategy {
public void move() { System.out.println("Walks."); }
}
class Bird {
private MoveStrategy moveStrategy;
public Bird(MoveStrategy moveStrategy) {
this.moveStrategy = moveStrategy;
}
public void performMove() {
moveStrategy.move();
}
}
public class StrategyMain {
public static void main(String[] args) {
Bird bird = new Bird(new FlyStrategy());
bird.performMove(); // Output: Files
}
}
조합을 통해 유연성을 높이는 방법과 그 예시
유연성: 조합을 사용하면 객체의 기능을 동적으로 변경하거나 확장할 수 있어 유연성이 높아진다.
예시: 전략 패턴을 사용하여 Payment 클래스가 다양한 결제 방법을 지원하도록 한다.
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card.");
}
}
class PaypalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Paypal.");
}
}
class Payment {
private PaymentStrategy paymentStrategy;
public Payment(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void executePayment(int amount) {
paymentStrategy.pay(amount);
}
}
public class PaymentMain {
public static void main(String[] args) {
Payment payment = new Payment(new CreditCardPayment());
payment.executePayment(100); // Output: Paid 100 using Credit Card.
payment = new Payment(new PaypalPayment());
payment.executePayment(200); // Output: Paid 200 using Paypal.
}
}
다형성을 실현하기 위한 상속과 조합의 차이점
상속을 통한 다형성: 상속을 사용하면 부모 클래스를 참조하여 자식 클래스의 객체를 생성할 수 있다.
Animal animal = new Bird();
animal.move(); // Bird 클래스의 move() 메서드가 호출됨
조합을 통한 다형성: 조합을 사용하면 인터페이스를 통해 다양한 구현체를 동적으로 변경할 수 있다.
PaymentStrategy strategy = new CreditCardPayment();
Payment payment = new Payment(strategy);
payment.executePayment(100);
선호 기준
- 상속: 클래스 간 강한 관계가 필요하고, 상속받는 모든 클래스가 동일한 기본 동작을 공유할 때 사용한다.
- 조합: 유연성이 필요하고, 객체의 동작을 동적으로 변경하거나 확장해야 할 때 사용한다.
상속의 깊이가 깊어질 때 발생할 수 있는 문제와 해결을 위한 리팩토링 전략
문제점
- 복잡성 증가: 상속 계층이 깊어질수록 코드가 복잡해지고 이해하기 어려워진다.
- 유지보수 어려움: 상속 계층의 변경이 상위 클래스와 하위 클래스에 모두 영향을 미쳐 유지보수가 어려워진다.
- 다중 상속 문제: 다중 상속이 필요한 상황에서는 상속 계층 구조가 이를 지원하지 못한다.
리팩토링 전략
- 상속을 조합으로 변경: 상속 대신 조합을 사용하여 기능을 분리하고, 객체 간의 의존성을 줄인다.
- 인터페이스 도입: 공통된 기능을 인터페이스로 분리하고, 해당 인터페이스를 구현하는 클래스를 작성한다.
- 데코레이터 패턴 사용: 객체의 기능을 동적으로 확장할 때 데코레이터 패턴을 사용한다.
예시: 상속 계층이 깊은 경우, 다음과 같은 조합과 인터페이스를 사용하여 리팩토링한다.
// 기존 상속 구조
class A {
void methodA() { System.out.println("A"); }
}
class B extends A {
void methodB() { System.out.println("B"); }
}
class C extends B {
void methodC() { System.out.println("C"); }
}
// 리팩토링 후
interface MethodA {
void methodA();
}
class AImpl implements MethodA {
public void methodA() { System.out.println("A"); }
}
interface MethodB {
void methodB();
}
class BImpl implements MethodB {
public void methodB() { System.out.println("B"); }
}
interface MethodC {
void methodC();
}
class CImpl implements MethodC {
public void methodC() { System.out.println("C"); }
}
class Combined {
private MethodA methodA;
private MethodB methodB;
private MethodC methodC;
public Combined(MethodA methodA, MethodB methodB, MethodC methodC) {
this.methodA = methodA;
this.methodB = methodB;
this.methodC = methodC;
}
void methodA() { methodA.methodA(); }
void methodB() { methodB.methodB(); }
void methodC() { methodC.methodC(); }
}
public class RefactoringMain {
public static void main(String[] args) {
Combined combined = new Combined(new AImpl(), new BImpl(), new CImpl());
combined.methodA();
combined.methodB();
combined.methodC();
}
}
이러한 리팩토링을 통해 상속 깊이에 따른 복잡성을 줄이고, 코드의 유연성과 유지보수성을 높일 수 있다.
아래 링크의 블로그를 보고 상속과 조합을 공부하는 데에 도움이 많이 되었다.
https://velog.io/@jsj3282/%EC%83%81%EC%86%8D%EA%B3%BC-%EC%A1%B0%ED%95%A9Inheritance-vs-Composition
상속과 조합(Inheritance vs Composition)
우리는 다양한 이유로 상속을 사용한다. 코드를 재사용함으로써 중복을 줄일 수 있다. 변화에 대한 유연성 및 확장성이 증가한다. 개발 시간이 단축된다. 하지만, 상속의 장점들은 상속을 적절
velog.io