[Java] 다형성

ref- inflearn 김영한, 실전 자바 - 기본편

📌다형성(Polymorphism)

✅다형성이란?

  • 다형성은 다양한 형태 를 뜻한다. 프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 보통 하나의 객체는 하나의 타입으로 고정되있지만 다형성을 이용하면 하나의 객체가 다른 타입으로 사용될수있다.

  • 부모는 자식을 담을수있다.


✅다형적 참조 예제

  • 부모 클래스
package poly.basic;
public class Parent {
 public void parentMethod() {
 System.out.println("Parent.parentMethod");
 }
}
  • 자식 클래스
package poly.basic;
public class Child extends Parent {
 public void childMethod() {
 System.out.println("Child.childMethod");
 }
}
  • 메인 클래스
package poly.basic;
/**
 * 다형적 참조: 부모는 자식을 품을 수 있다.
 */
public class PolyMain {
 public static void main(String[] args) {
 //부모 변수가 부모 인스턴스 참조
 System.out.println("Parent -> Parent");
 Parent parent = new Parent();
 parent.parentMethod();
 //자식 변수가 자식 인스턴스 참조
 System.out.println("Child -> Child");
 Child child = new Child();
 child.parentMethod();
 child.childMethod();
 //부모 변수가 자식 인스턴스 참조(다형적 참조)
 System.out.println("Parent -> Child");
 Parent poly = new Child();
 poly.parentMethod();
 //Child child1 = new Parent(); 자식은 부모를 담을 수 없다.
 //자식의 기능은 호출할 수 없다. 컴파일 오류 발생
 //poly.childMethod();
 }
}
  • 실행결과
Parent -> Parent
Parent.parentMethod
Child -> Child
Parent.parentMethod
Child.childMethod
Parent -> Child
Parent.parentMethod

✅해당 예제 메모리 구조 결과 확인

  • Parent - > Parent:parent.parentMethod() 부모타입의 변수가 부모 인스턴스 참조

image description

  1. Parent parent = new Parent()
  2. Parent 인스턴스를 만들고 , 부모 타입인 Parent를 생성했기떄문에 메모리 상에 Parent 만 생성이 된다( 자식은 생성이 되지않는다.)
  3. 생성된 참조값을 Parent 타입의 변수인 parent에 담아둔다.
  4. parent.parentMethod()를 호출하면 인스턴스의 Parent 클래스에 있는 parentMethod()가 호출된다.


  • Child - > Child:child.childMethod() 자식타입의 변수가 자식 인스턴스 참조

image description

  1. 자식 타입의 변수가 자식 인스턴스를 참조한다.
  2. Child child = new Child()
  3. Child 인스턴스를 만들었다. 이경우 자식 타입인 Child를 생성했기 떄문에 메모리 상에 Child와 Parent가 모두 생성된다.
  4. 생성된 참조값을 Child 타입의 변수인 child에 담아둔다.
  5. child.childMethod()를 호출하면 인스턴스의 Child 클래스에 있는 childMethod()가 호출된다.


  • Parent - > Child:poly.parentMethod() 다형적 참조: 부모타입의 변수가 자식 인스턴스 참조. (여기서 부터 중요)

image description

  1. 부모 타입의 변수가 자식 인스턴를 참조한다.
  2. Parent poly = new Child()
  3. Child 인스턴스를 만들었다. 이경우 자식 타입인 Child를 생성했기 떄문에 메모리 상에 Child와 Parent가 모두 생성된다
  4. 생성된 참조값을 Parent 타입의 변수인 poly에 담아둔다
  • 부모는 자식을 담을수 있다
    1. 부모 타입은 자식 타입을 담을 수 있다. Parent poly = new Child(); 성공
    2. 자식 타입은 부모 타입을 담을 수 없다. Child child1 = new Parent(); 컴파일 오류 발생

결론: 자바에서 부모 타입은 물론이고, 자신을 기준으로 모든 자식 타입을 참조 할 수있다. 이것이 바로 다양한 형태를 참조할수 있다고 해서 다형적 참조라고 한다.


✅다형적 참조의 한계

  • Parent - > Child:poly.childMethod()

image description

  1. Parent poly = new Child() 자식을 참조한 상황에서 poly가 자식 타입인 child에 있는 childMethod()를 호출할순없다.
  2. poly.childMethod()를 실행하면 참조값을 통해 인스턴스를 찾고, 호출하는 poly는 Parent 타입이기떄문에 Parent 클래스부터 시작해서 필요한 기능을 찾는다. 그런데 상속 관계에서는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈수는 없다.
    Parent는 부모 타입이고 상위에 부모가 없다. 따라서 childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다. (이런 경우 childMethod()를 호출하고 싶으면 캐스팅이 필요하다.)

✅다운캐스팅

  • 다운캐스팅을 이용해서 위예제 childMethod() 호출하기

image description

  1. Child child = (Child) poly 다운캐스팅이라는 기능을 사용해서 부모 타입을 잠깐 자식타입으로 변경
  2. 캐스팅을 한다고 해서 Parent poly 타입이 변하는 것은 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 child 타입이 되는 것이다. 따라서 poly의 타입은 Parent로 기존과 같이 유지된다.
  • 업캐스팅(upcasting): 부모 타입으로 변경 (업캐스팅은 생량 가능)
  • 다운캐스팅(downcasting): 자식 타입으로 변경

✅instanceof

변수가 참조하는 인스턴스의 타입을 확인하고 싶다면 instanceof 키워드를 사용하면된다.

new Parent() instanceof Parent
Parent p = new Parent() //같은 타입 true
new Child() instanceof Parent
Parent p = new Child() //부모는 자식을 담을 수 있다. true
new Parent() instanceof Child
Child c = new Parent() //자식은 부모를 담을 수 없다. false
new Child() instanceof Child
Child c = new Child() //같은 타입 true
  • 오른쪽에 있는 타입이 왼쪽에 인스턴스의 타입이 들어갈 수 있는지 대입해보면 된다. 대입이 가능하면 true, 불가능 하면 false가 된다.

✅메서드 오버라이딩

메서드 오버라이딩: 기존 기능을 하위 타입에서 새로운 기능으로 재정의 메서드 오버라이딩에서 꼭 기억해야 할점은 오버라이딩 된 메서드가 항상 우선권을 가진다.

✅추상 클래스1

  • 실제로 생성되면 안되는 클래스를 추상 클래스라고 한다. (추상적인 개념을 제공하는 클래스) 실체인 인스턴스가 존제하지 않고 상속을 목적으로 하는 부모 클래스 역활
abstract Class 클래스명
  • 추상 클래스는 클래스를 선언할떄 앞에 추상이라는 의미 abstract 키워드를 붙인다.

✅추상 메서드

부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있다.

public abstract void 메서드명()
  • 추상 메서드는 선언할 떄 메서드 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 된다.
  • 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다. (그렇지 않으면 컴파일 오류발생)
  • 추상 메서드는 바디가 없다.
  • 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 한다.

추상 클래스 추상 메서드 예제.

  • 동물이라는 추상 클스(부모 클래스)
    package poly.ex3;
    public abstract class AbstractAnimal {
     public abstract void sound();
     public void move() {
     System.out.println("동물이 움직입니다.");
     }
    }
    
  • AbstractAnimal 클래스는 abstract가 붙은 추상 클래스이다. 이 클래스는 직접 인스턴스를 생성할수없다.
  • sound()는 abstrac가 붙은 추상 메서드이다. 이 메서드는 자식이 반드시 오버라이딩 해서 사용해야한다.

  • 강이지 클래스 ( AbstracAnumal 자식 클래스)
    package poly.ex3;
    public class Dog extends AbstractAnimal {
     @Override
     public void sound() {
     System.out.println("멍멍");
     }
    }
    
  • 고양이 클래스 ( AbstracAnumal 자식 클래스)
    package poly.ex3;
    public class Cat extends AbstractAnimal {
     @Override
     public void sound() {
     System.out.println("냐옹");
     }
    }
    
  • 소 클래스 ( AbstracAnumal 자식 클래스)
    package poly.ex3;
    public class Caw extends AbstractAnimal {
     @Override
     public void sound() {
     System.out.println("음매");
     }
    }
    
  • 메인 클래스
    public class AbstractMain {
     public static void main(String[] args) {
     //추상클래스 생성 불가
     //AbstractAnimal animal = new AbstractAnimal();  추상클래스를 생성하려 하면 컴파일 오류 발생.
     Dog dog = new Dog();
     Cat cat = new Cat();
     Caw caw = new Caw();
     cat.sound();
     cat.move();
     soundAnimal(cat);
     soundAnimal(dog);
     soundAnimal(caw);
     }
     //동물이 추가 되어도 변하지 않는 코드
     private static void soundAnimal(AbstractAnimal animal) {
     System.out.println("동물 소리 테스트 시작");
     animal.sound();
     System.out.println("동물 소리 테스트 종료");
     }
    }
    
  • 메모리 구조

image description

  • 추상 클래스 덕분에 실수로 Animal 인스턴스를 생성할 문제를 근본적으로 방지해준다.
  • 추상 메서드 덕분에 새로운 동물의 자식 클래스를 만들떄 실수로 sound()를 오버라이딩 하지 않을 문제를 근본적으로 방지해준다.

✅추상 클래스2(완전한 추상 클래스)

순수 추상 클래스: 모든 메서드가 추상 메서드인 추상 클래스

  • 부모 클래스의 추상클래스
 public abstract class AbstractAnimal {
 public abstract void sound();
 public abstract void move();
}
  • 메서드 바디가 없고, 순수 추상 클래스는 실행 로직을 전혀 가지고있지않으며 단지 다형성을 위한 부모 타입으로써 껍데기 역활만 제공,

  • 순수 추상 클래스 특징

    1. 인스턴스를 생성 할수 없다
    2. 상속시 자식은 모든 메서드를 오버라이딩 해야한다.
    3. 주로 다형성을 위해 사용된다.

📌인터페이스

자바 는 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능을 제공한다. 인터페이스는 메서드 이름만 있는 설계도 이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 한다. => 순수 추상 클래스와 같은데 여기서 약간의 편의 기능이 추가됨.

  • 인터페이스 사용이유
    모든 메서드가 추상 메서드인 경우 순수 추상 클래스를 만들어도 되고,인터페이스를 만들어도 된다. 그런대 왜 인터페이스를 사용해야 할까?
    1. 제약: 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현해라는 제약을 주는것이다. 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수있다. 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 추상 클래스가 아니게 된다.
    2. 다중구현:자바에서 클래스 상속은 부모를 하나만 지정할 수 있다. 반면에 인터페이스는 모를 여려명 두는 다중 구현(다중 상속)이 가능하다.
  • 인터페이스 특징
    1. 인터페이스의 메서드는 모두 public,abstract이다.
    2. 메서드에 public abstract를 생략할수 있다.
    3. 인터페이스는 다중 구현(다중 상속)을 지원한다.
  • 인터페이스 클래스
    package poly.ex5;
    public interface InterfaceAnimal {
     void sound();
     void move();
    }
    
  1. 인터페이스는 class 대신에 interface로 선언한다.
  2. sound(),move() 앞에 public abstract가 생략되어있다. 다라서 상속 받는 곳에서 모든 메서드를 오버라이딩 해야한다.
  • dog 자식 클래스
    package poly.ex5;
    public class Dog implements InterfaceAnimal {
     @Override
     public void sound() {
     System.out.println("멍멍");
     }
     @Override
     public void move() {
     System.out.println("개 이동");
     }
    }
    
    1. 인터페이스를 상속 받을 떄는 extends 대신에 implements 라는 구현이라는 키워드를 사용해야 한다. 인터페이스는 그래서 상속이라 하지 않고 구현이라 한다.

✅인터페이스 다중구현 (다중상속)

image description

InterfaceA,InterfaceB는 둘다 methodCommon()을 가지고 있다.그리고 Child는 두 인터페이스를 구현했다. 상속 관계의 경우 두 부모 중에 어떠한 부모의 methodCommon()을 사용해야 할지 문제가 발생했다. 하지만 인터페이스를 사용한 결과 두부모 중에 어떤 한 부모의 methodCommon()을 선택하는 것이 아니라 그냥 인터페이스 구현한 child에 있는 methodCommon()이 사용된다. 이러한 이윯 인터페이스는 다중 구현을 허용함.

✅인터페이스 다중구현 활용 예시

package poly.diamond;
public interface InterfaceA {
 void methodA();
 void methodCommon();
}
package poly.diamond;
public interface InterfaceB {
 void methodB();
 void methodCommon();
}
package poly.diamond;
public class Child implements InterfaceA, InterfaceB {
 @Override
 public void methodA() {
 System.out.println("Child.methodA");
 }
 @Override
 public void methodB() {
 System.out.println("Child.methodB");
 }
 @Override
 public void methodCommon() {
 System.out.println("Child.methodCommon");
 }
}
  1. implements InterfaceA, interfaceB와 같이 다중 구현을 할 수 있다. implements 키워드에 , 로 여러 인터페이스를 구분하면된다.
  2. methodCommon()의 경우 양쪽 인터페이스에 다있지만 같은 메서드이므로 구현은 하나만 하면된다.
package poly.diamond;
//인터페이스 다중 구현
public class DiamondMain {
 public static void main(String[] args) {
 InterfaceA a = new Child();
 a.methodA();
 a.methodCommon();
 InterfaceB b = new Child();
 b.methodB();
 b.methodCommon();
 }
}
  • 실행결과
Child.methodA
Child.methodCommon
Child.methodB
Child.methodCommon

image description

  1. a.methodCommon()을 호출하면 먼저 x001 Child 인스턴스를 찾는다.
  2. 변수 aInterfaceA 타입이므로 해당 타입에서 methodCommon()을 찾는다.
  3. methodCommon()은 하위 타입인 child에서 오버라이딩 되어 있다.따라서 childmethodCommon()이 호출된다.

image description

  1. b.methodCommon() 을 호출하면 먼저 x001 Child 인스턴스를 찾는다.
  2. 변수 b 가 InterfaceB 타입이므로 해당 타입에서 methodCommon() 을 찾는다.
  3. methodCommon() 은 하위 타입인 Child 에서 오버라이딩 되어 있다. 따라서 Child 의 methodCommon() 호출된다.

✅클래스와 인터페이스 활용

image description

  1. AbstractAnimal은 추상 클래스이다.
  2. sound(): 동물의 소리를 내기 위한 추상 메서드를 제공한다.
  3. move():동물의 이동을 표한하기 위한 메서드이다. 이메서드는 추상 메서드가 아니라 상속을 목적으로 사용된다.
  4. fly는 인터페이스이다. 나는 동물을 이 인터페이스에를 구현할수 있다.
  5. Bird,Chicken은 날 수 있는 동물이다. fly()메서드를 구현 해야 한다.
package poly.ex6;
public abstract class AbstractAnimal {
 public abstract void sound();
 public void move() {
 System.out.println("동물이 이동합니다.");
 }
}
package poly.ex6;
public interface Fly {
 void fly();
}
package poly.ex6;
public class Dog extends AbstractAnimal {
 @Override
 public void sound() {
 System.out.println("멍멍");
 }
}
  • Dog는 AbstractAnimal만 상속 받는다.
package poly.ex6;
public class Bird extends AbstractAnimal implements Fly {
 @Override
 public void sound() {
 System.out.println("짹짹");
 }
 @Override
 public void fly() {
 System.out.println("새 날기");
 }
}
  • Bird 는 AbstractAnimal 클래스를 상속하고 Fly 인터페이스를 구현한다. extends 를 통한 상속은 하나만 할 수 있고 implements 를 통한 인터페이스는 다중 구현 할 수 있기 때문에 둘이 함 께 나온 경우 extends 가 먼저 나와야 한다.
package poly.ex6;
public class Chicken extends AbstractAnimal implements Fly {
 @Override
 public void sound() {
 System.out.println("꼬끼오");
 }
 @Override
 public void fly() {
 System.out.println("닭 날기");
 }
}
package poly.ex6;
public class SoundFlyMain {
 public static void main(String[] args) {
 Dog dog = new Dog();
 Bird bird = new Bird();
 Chicken chicken = new Chicken();
 soundAnimal(dog);
 soundAnimal(bird);
 soundAnimal(chicken);
 flyAnimal(bird);
 flyAnimal(chicken);
 }
 //AbstractAnimal 사용 가능
 private static void soundAnimal(AbstractAnimal animal) {
 System.out.println("동물 소리 테스트 시작");
 animal.sound();
 System.out.println("동물 소리 테스트 종료");
 }
 //Fly 인터페이스가 있으면 사용 가능
 private static void flyAnimal(Fly fly) {
 System.out.println("날기 테스트 시작");
 fly.fly();
 System.out.println("날기 테스트 종료");
 }
}

실행결과

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
짹짹
동물 소리 테스트 종료
동물 소리 테스트 시작
꼬끼오
동물 소리 테스트 종료
날기 테스트 시작
 날기
날기 테스트 종료
날기 테스트 시작
 날기
날기 테스트 종료

image description

soundAnimal(AbstractAnimal animal) AbstractAnimal 를 상속한 Dog , Bird , Chicken 을 전달해서 실행할 수 있다.

  • 실행과정
    1. soundAnimal(bird)를 호출한다고 가정하자.
    2. 메서드 안에서 animal.sound()를 호출하면 참조 대상인 x001 Bird 인스턴스를 찾는다.
    3. 호출한 animal 변수는 AbstractAnimal 타입이다. 따라서 AbstractAnimal.sound()를 찾는다. 해당 메서드는 Bird.sound()에 오버라이딩 되어있다.
    4. Bird.sound()가 호출된다.

image description

flyAnimal(Fly fly) Fly 인터페이스를 구현한 Bird , Chicken 을 전달해서 실행할 수 있다.

  • 실행 과정
    1. fly(bird) 를 호출한다고 가정하자.
    2. 메서드 안에서 fly.fly() 를 호출하면 참조 대상인 x001 Bird 인스턴스를 찾는다.
    3. 호출한 fly 변수는 Fly 타입이다. 따라서 Fly.fly() 를 찾는다. 해당 메서드는 Bird.fly() 에 오버라이 딩 되어 있다.
    4. Bird.fly() 가 호출된다


📌좋은 객체 지향 프로그래밍이란?

✅객체지향 특징

  1. 추상화
  2. 캡슐화
  3. 상속
  4. 다형성

✅객체 지향 프로그래밍

  1. 객체지향 프로그래밍은 프로그램을 명령어 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 “객체”들의 모임을 파악하고자 하는 것이다. 각각의 객체는 메세지를 주고받고,데이터를 처리할수있다.
  2. 객체지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 떄문에 대규모 소프트웨어 개발에 많이 사용된다.

✅다형성의 실세계 비유

  1. 실세계와 객체 지향을 1:1로 메칭하는것이아님.
  2. 역활과 구현으로 세상을 구분

image description

역활과 구현으로 구분하면 세상이 단순해지고,유연해지며 변경도 편리해진다.

  • 역활과 구현으로 구분하면 장점
    1. 클라이언트는 대상의 역할(인터페이스만)알면 된다.
    2. 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
    3. 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지않는다
    4. 클라이언트는 구현 대상 자체를 변경해도 영향을 받지않는다
  • 자바 언어의 다형성을 활용
    1. 역활 = 인터페이스
    2. 구현 = 인터페이스를 구현한 클래스, 구현 객체
    3. 객체를 설계할 떄 역활과 구현을 명확히 분리해야함
    4. 객체 설계시 역할(인터페이스를)먼저 부여하고, 그역활을 수행하는 구현 객체 만들기

✅다형성을 사용하지않고,역활과 구현을 분리하지 않은 예제 코드

image description
Driver는 k3Car를 운전하는 프로그램이다.

package poly.car0;
public class K3Car {
 public void startEngine() {
 System.out.println("K3Car.startEngine");
 }
 public void offEngine() {
 System.out.println("K3Car.offEngine");
 }
 public void pressAccelerator() {
 System.out.println("K3Car.pressAccelerator");
 }
}
package poly.car0;
public class Driver {
 
 private K3Car k3Car;
 public void setK3Car(K3Car k3Car) {
 this.k3Car = k3Car;
 }
 public void drive() {
 System.out.println("자동차를 운전합니다.");
 k3Car.startEngine();
 k3Car.pressAccelerator();
 k3Car.offEngine();
 }
}
package poly.car0;
public class CarMain0 {
 public static void main(String[] args) {
 Driver driver = new Driver(); 
 K3Car k3Car = new K3Car();
 driver.setK3Car(k3Car);
 driver.drive();
 }
}
  • Driver 와 K3Car 를 먼저 생성한다. 그리고 driver.setK3Car(..) 를 통해 driver 에게 k3Car 의 참 조를 넘겨준다.
  • driver.driver() 를 호출한다

image description

  • 여기서 새로운 Model3 차량을 추가 해야한다면? 기존 Driver 코드를 많이 변경해야 한다. image description

  • 새로운 Model3차량 추가후 Driver 클래스 수정

package poly.car0;

public class Driver {

    private  K3Car k3Car; // x002 대입
    private  Model3Car model3Car; //추가

    public void setK3Car(K3Car k3Car  /* 메인에서 k3car = x002가 넘어옴*/ ) {
        this.k3Car = k3Car;
    }

    public void setModel3Car(Model3Car model3Car) {
        this.model3Car = model3Car;
    }

    public void drive() {
        System.out.println("자동차를 운전합니다");
        if (k3Car != null) {
            k3Car.startEngine();
            k3Car.pressAccelerator();
            k3Car.offEngine();
        } else if (model3Car != null) {
            model3Car.startEngine();
            model3Car.pressAccelerator();
            model3Car.offEngine();
        }

    }
}

  • 오직 새로운 차량을 하나만 추가하는데에도 많은 코드 수정이 필요하다.

image description

✅다형성을 활용한 역활과 구현을 분리한 예제.

image description

1.Driver : 운전자는 자동차( Car )의 역할에만 의존한다. 구현인 K3, Model3 자동차에 의존하지 않는다. 2.Driver 클래스는 Car car 멤버 변수를 가진다. 따라서 Car 인터페이스를 참조한다. 3.인터페이스를 구현한 K3Car , Model3Car 에 의존하지 않고, Car 인터페이스에만 의존한다.(의존한다 = 이 클래스 알아?) 4.Car : 자동차의 역할이고 인터페이스이다. K3Car , Model3Car 클래스가 인터페이스를 구현한다.

package poly.car1;
public interface Car {
 void startEngine();
 void offEngine();
 void pressAccelerator();
}
package poly.car1;
public class K3Car implements Car {
 @Override
 public void startEngine() {
 System.out.println("K3Car.startEngine");
 }
 @Override
 public void offEngine() {
 System.out.println("K3Car.offEngine");
 }
 @Override
 public void pressAccelerator() {
 System.out.println("K3Car.pressAccelerator");
 }
}
package poly.car1;
public class Model3Car implements Car {
 @Override
 public void startEngine() {
 System.out.println("Model3Car.startEngine");
 }
 @Override
 public void offEngine() {
 System.out.println("Model3Car.offEngine");
 }
 @Override
 public void pressAccelerator() {
 System.out.println("Model3Car.pressAccelerator");
 }
}
package poly.car1;
public class Driver {
 private Car car;
 public void setCar(Car car) {
 System.out.println("자동차를 설정합니다: " + car);
 this.car = car;
 }
 public void drive() {
 System.out.println("자동차를 운전합니다.");
 car.startEngine();
 car.pressAccelerator();
 car.offEngine();
 }
}
  1. Driver 는 멤버 변수로 Car car 를 가진다.
  2. setCar(Car car) : 멤버 변수에 자동차를 설정한다. 외부에서 누군가 이 메서드를 호출해주어야 Driver 는 새로운 자동차를 참조하거나 변경할 수 있다.
  3. drive() : Car 인터페이스가 제공하는 기능들을 통해 자동차를 운전한다.
package poly.car1;
/**
 * 다형성을 활용한 런타임 변경
 * 런타임: 애플리케이션 실행 도중에 변경 가능
 */
public class CarMain1 {
 public static void main(String[] args) {
 Driver driver = new Driver();
 //차량 선택(k3)
 Car k3Car = new K3Car();
 driver.setCar(k3Car);
 driver.drive();
 //차량 변경(k3 -> model3)
 Car model3Car = new Model3Car();
 driver.setCar(model3Car);
 driver.drive();
 }
}
자동차를 설정합니다: poly.car.K3Car@24d46ca6
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 설정합니다: poly.car.Model3Car@372f7a8d
자동차를 운전합니다.
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine

image description

  1. Driver와 k3Car를 생성한다.
  2. driver.setCar(k3Car를 호출해서 Driver의 car car 필드가 k3Car의 인스턴스를 참조하도록 한다.
  3. driver.drive() 를 호출하면 x001 을 참조한다. car 필드가 Car 타입이므로 Car 타입을 찾아서 실행 하지만 메서드 오버라이딩에 의해 K3Car 의 기능이 호출된다.

image description

  1. Model3Car 를 생성한다.
  2. driver.setCar(model3Car) 를 호출해서 Driver 의 Car car 필드가 Model3Car 의 인스턴스를 참조 하도록 변경한다.
  3. driver.drive() 를 호출하면 x002 을 참조한다. car 필드가 Car 타입이므로 Car 타입을 찾아서 실행 하지만 메서드 오버라이딩에 의해 Model3Car 의 기능이 호출된다.

✅OCP(Open-closed Principle)원칙

좋은 객체지향 설계 원칙 중 하나로 OCP 원칙이 있다.

  • Open for extension: 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다.
  • Closed for modification: 기존의 코드는 수정되지 않아야 한다. 확장에는 열려있고, 변경에는 닫혀 있다는 뜻인데, 쉽게 이야기해서 기존의 코드 수정 없이 새로운 기능을 추가할 수 있 다는 의미다.

image description

  • 확장에 열려있다는 의미
    Car 인터페이스를 사용해 새로운 차량을 자유롭게 추가할수 있다.Car 인터페이스를 구현해서 기능을 추가할 수 있다는 의미이다. 그리고 Car 인터페이스를 사용하는 클라이언트 코드인 Driver 도 Car 인터페이스를 통해 새롭게 추가된 차량을 자유롭게 호출할 수 있다. 이것이 확장에 열려있다는 의미이다.

  • 코드 수정은 닫혀 있다는 의미
    새로운 차를 추가하게 되면 기능이 추가되기 때문에 기존 코드의 수정은 불가피하다. 당연히 어딘가의 코드는 수정해야 한다.

결론

  1. Car 를 사용하는 클라이언트 코드인 Driver 코드의 변경없이 새로운 자동차를 확장할 수 있다.
  2. 다형성을 활용하고 역할과 구현을 잘 분리한 덕분에 새로운 자동차를 추가해도 대부분의 핵심 코드들을 그대로 유 지할 수 있게 되었다.

Categories:

Updated:

Leave a comment