자바칩

[Java] 상속과 조합 본문

Study/Java

[Java] 상속과 조합

아기제이 2024. 5. 20. 17:54
728x90

상속 조합을 사용하는 상황 구분

상속(Inheritance)

  • 상속은 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 관계를 의미한다.
  • 상속은 확장을 고려하고 설계가 확실한 "is-a" 관계일 때와, API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파되어도 괜찮은 경우에 사용한다.
  • 예: Bird 클래스는 Animal 클래스를 상속받을 수 있다. 이는 "새는 동물이다"라는 관계를 표현한다.

조합(Composition)

  • 조합은 새로운 클래스가 기존 클래스를 포함하는 관계를 의미한다. 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조한다.
  • 조합은 "has-a" 관계를 나타낼 때 사용한다.
  • 예: Car 클래스가 Engine 클래스를 포함할 수 있다. 이는 "자동차는 엔진을 가지고 있다"라는 관계를 표현한다.

상속을 사용할 때 발생할 수 있는 문제점과 해결을 위한 디자인 패턴

문제점

  1. 강한 결합: 부모 클래스와 자식 클래스 간의 강한 결합으로 인해 부모 클래스의 변경이 자식 클래스에 영향을 미친다.
  2. 확장성 제한: 새로운 기능 추가 시 상속 구조가 복잡해질 수 있다.
  3. 다중 상속 문제: 자바는 다중 상속을 지원하지 않으므로, 여러 클래스를 상속받아야 하는 경우 곤란할 수 있다.

해결을 위한 디자인 패턴

 

   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);
   

 

선호 기준

  • 상속: 클래스 간 강한 관계가 필요하고, 상속받는 모든 클래스가 동일한 기본 동작을 공유할 때 사용한다.
  • 조합: 유연성이 필요하고, 객체의 동작을 동적으로 변경하거나 확장해야 할 때 사용한다.

상속의 깊이가 깊어질 때 발생할 수 있는 문제와 해결을 위한 리팩토링 전략

문제점

  1. 복잡성 증가: 상속 계층이 깊어질수록 코드가 복잡해지고 이해하기 어려워진다.
  2. 유지보수 어려움: 상속 계층의 변경이 상위 클래스와 하위 클래스에 모두 영향을 미쳐 유지보수가 어려워진다.
  3. 다중 상속 문제: 다중 상속이 필요한 상황에서는 상속 계층 구조가 이를 지원하지 못한다.

리팩토링 전략

  1. 상속을 조합으로 변경: 상속 대신 조합을 사용하여 기능을 분리하고, 객체 간의 의존성을 줄인다.
  2. 인터페이스 도입: 공통된 기능을 인터페이스로 분리하고, 해당 인터페이스를 구현하는 클래스를 작성한다.
  3. 데코레이터 패턴 사용: 객체의 기능을 동적으로 확장할 때 데코레이터 패턴을 사용한다.

예시: 상속 계층이 깊은 경우, 다음과 같은 조합과 인터페이스를 사용하여 리팩토링한다.


    // 기존 상속 구조
    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