다형성 활용1

다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보자.

아주 단순하고 전통적인 동물 소리 문제로 접근해보자.

개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해보자. 먼저 다형성을 사용하지 않고 코드를 작성해보자.

 

예제1

package poly.ex1;

public class Dog {
    public void sound(){
        System.out.println("멍멍");
    }
}
package poly.ex1;

public class Cat {
    public void sound(){
        System.out.println("냐옹");
    }
}
package poly.ex1;

public class Cow {
    public void sound(){
        System.out.println("음매");
    }
}
package poly.ex1;

public class AnimalSoundMain {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

        System.out.println("동물 소리 테스트 시작");
        dog.sound();
        System.out.println("동물 소리 테스트 종료");

        System.out.println("동물 소리 테스트 시작");
        cat.sound();
        System.out.println("동물 소리 테스트 종료");

        System.out.println("동물 소리 테스트 시작");
        cow.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

실행 결과

 

단순히 개, 고양이, 소 동물들의 울음 소리를 출력하는 프로그램이다. 만약 여기서 새로운 동물이 추가되면 어떻게 될까? 만약 기존 코드에 소가 없었다고 가정해보자, 소가 추가된다고 가정하면 Cow 클래스를 만들고 다음 코드도 추가해야 한다.

//Cow를 생성하는 코드
Cow cow = new Cow();

//Cow를 사용하는 코드
System.out.println("동물 소리 테스트 시작");
cow.sound();
System.out.println("동물 소리 테스트 종료");

Cow를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog, Cat, Cow를 사용해서 출력하는 부분은 계속 중복이 증가한다.

 

중복 코드

System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
...

이 부분의 중복을 제거할 수 있을까?

 

중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for문을 사용하면 된다. 그런데 Dog , Cat , Cow는 서로 완전히 다른 클래스다.

 

중복 제거 시도

1. 메서드로 중복 제거 시도

메서드를 사용하면 다음과 같이 매개변수의 클래스를 Cow , Dog , Cat 중에 하나로 정해야 한다.

private static void soundCow(Cow cow) {
     System.out.println("동물 소리 테스트 시작");
     coww.sound();
     System.out.println("동물 소리 테스트 종료");
}

따라서 이 메서드는 Cow 전용 메서드가 되고 Dog , Cat은 인수로 사용할 수 없다. Dog , Cat , Cow의 타입(클래스)이 서로 다르기 때문에 soundCow 메서드를 함께 사용하는 것은 불가능하다.

 

2. 배열과 for문을 통한 중복 제거 시도

Cow[] cowArr = {cat, dog, cow}; //컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
 for (Cow x : cowArr) {
	 x.sound();
 }
System.out.println("동물 소리 테스트 종료");

배열과 for문 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog , Cat , Cow 중에 하나로 지정해야 한다. 같은 Cow들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog , Cat , Cow를 하나의 배열에 담는 것은 불가능하다.

결과적으로 지금 상황에서는 해결 방법이 없다. 새로운 동물이 추가될 때마다 더 많은 중복 코드를 작성해야 한다.

 

지금까지 설명한 모든 중복 제거 시도가 Dog , Cat , Cow의 타입이 서로 다르기 때문에 불가능하다. 문제의 핵심은 바로 타입이 다르다는 점이다. 반대로 이야기하면 Dog , Cat , Cow가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것이다.

 

다형성의 핵심은 다형적 참조메서드 오버라이딩이다. 이 둘을 활용하면 Dog , Cat , Cow가 모두 같은 타입을 사용하고, 각자 자신의 메서드도 호출할 수 있다.

 


다형성 활용2

이번에는 앞서 설명한 예제를 다형성을 사용하도록 변경해보자.

 

예제2

다형성을 사용하기 위해 여기서는 상속 관계를 사용한다. Animal (동물)이라는 부모 클래스를 만들고 sound() 메서드를 정의한다. 이 메서드는 자식 클래스에서 오버라이딩할 목적으로 만들었다. Dog , Cat , Cow는 Animal 클래스를 상속받았다. 그리고 각각 부모의 sound() 메서드를 오버라이딩 한다.

 

package poly.ex2;

public class Animal {
    public void sound(){
        System.out.println("동물 울음 소리");
    }
}
package poly.ex2;

public class Cat extends Animal{
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}
package poly.ex2;

public class Cow extends Animal{
    @Override
    public void sound() {
        System.out.println("음매");
    }
}
package poly.ex2;

public class Dog extends Animal{
    @Override
    public void sound(){
        System.out.println("멍멍");
    }
}
package poly.ex2;

public class AnimalPolyMain1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();
        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(cow);

    }
    private static void soundAnimal(Animal animal){ //Animal은 dog, cat, cow를 담을 수 있다.
        System.out.println("동물 소리 테스트 시작");
        animal.sound(); //오버라이딩 메서드 각각 사용
        System.out.println("동물 소리 테스트 종료");
    }
}

실행 결과

실행 결과는 기존 코드와 같다.

 

코드를 분석해보자.

  • soundAnimal(dog);를 호출하면
  • soundAnimal(Animal animal)에 Dog 인스턴스가 전달된다.
    • Animal animal = dog로 이해하면 된다. 부모는 자식을 담을 수 있다. Animal은 Dog의 부모다.
  • 메서드 안에서 animal.sound(); 메서드를 호출한다.

  • animal 변수의 타입은 Animal이므로 Dog 인스턴스에 있는 Animal 클래스(부모) 부분을 찾아서 sound() 메서드를 실행하려고 한다. 그런데 하위 클래스인 Dog에서 sound() 메서드를 오버라이딩했다. 따라서 오버라이딩한 메서드가 우선권을 가진다.
  • Dog 클래스에 있는 sound() 메서드가 호출되므로 "멍멍"이 출력된다.

이 코드의 핵심은 Animal animal 부분이다.

  • 다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Cow의 인스턴스를 참조할 수 있다.
    (부모는 자식을 담을 수 있다)
  • 메서드 오버라이딩 덕분에 animal.sound()를 호출해도 Dog.sound() , Cat.sound() , Cow.sound()와 같이 각 인스턴스의 메서드를 호출할 수 있다. 만약 자바에 메서드 오버라이딩이 없었다면 모두 Animal 의 sound()가 호출되었을 것이다.

 


다형성 활용3

이번에는 배열과 for문을 사용해서 중복을 제거해보자.

package poly.ex2;

public class AnimalPolyMain2 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

        Animal[] animalArr = {dog, cat, cow};
        //변하지 않는 부분
        for (Animal animal : animalArr) {
            System.out.println("동물 소리 테스트 시작");
            animal.sound();
            System.out.println("동물 소리 테스트 종료");
        }
    }
}

실행 결과

 

배열은 같은 타입의 데이터를 나열할 수 있다.

Dog , Cat , Cow는 모두 Animal의 자식이므로 Animal 타입이다.

 

Animal 타입의 배열을 만들고 다형적 참조를 사용하면 된다.

//둘은 같은 코드이다.
Animal[] animalArr = new Animal[]{dog, cat, cow};
Animal[] animalArr = {dog, cat, cow};

다형적 참조 덕분에 Dog , Cat , Cow의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했다.

 

이제 배열을 for문을 사용해서 반복하면 된다.

//변하지 않는 부분
for (Animal animal : animalArr) {
     System.out.println("동물 소리 테스트 시작");
     animal.sound();
     System.out.println("동물 소리 테스트 종료");
}

animal.sound() 를 호출하지만 배열에는 Dog , Cat , Cow의 인스턴스가 들어있다. 메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound() 메서드가 호출된다.

 

 

조금 더 개선

이번에는 배열과 메서드 모두 활용해서 기존 코드를 완성해보자.

package poly.ex2;

public class AnimalPolyMain3 {
    public static void main(String[] args) {
        Animal[] animalArr = {new Dog(), new Cat(), new Cow()};
        for (Animal animal : animalArr) {
            soundAnimal(animal);
        }
    }
    //동물이 추가되어도 변하지 않는 코드
    public static void soundAnimal(Animal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

새로운 동물이 추가되어도 soundAnimal(..) 메서드는 코드 변경 없이 유지할 수 있다. 이렇게 할 수 있는 이유는 이 메서드는 Dog , Cat , Cow 같은 구체적인 클래스를 참조하는 것이 아니라 Animal이라는 추상적인 부모를 참조 하기 때문이다. 따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는 변경 없이 유지할 수 있다.

 

여기서 잘 보면 새로운 동물이 추가되었을 때 코드가 변하는 부분과 변하지 않는 부분이 있다.

  • main(): 코드가 변하는 부분 - 새로운 동물을 생성하고 필요한 메서드를 호출
  • soundAnimal(..) - 코드가 변하지 않는 부분

새로운 기능이 추가되었을 때 변하는 부분을 최소화하는 것이 잘 작성된 코드이다. 이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋다.

 

 

남은 문제

지금까지의 코드에 사실 2가지 문제가 있다.

1. Animal 클래스를 생성할 수 있는 문제
2. Animal 클래스를 상속받는 곳에서 sound() 메서드를 오버라이딩하지 않을 가능성

 

1. Animal 클래스를 생성할 수 있는 문제

Animal animal = new Animal();

사실 Animal 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없다.

하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는 데 아무런 제약이 없다. 누군가 실수로 new Animal() 을 사용해서 Animal의 인스턴스를 생성할 수 있다는 것이다. 이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않는다.

 

2. Animal 클래스를 상속받는 곳에서 sound() 메서드를 오버라이딩하지 않을 가능성

예를 들어서 Animal을 상속 받은 Pig 클래스를 만든다고 가정해보자. 우리가 기대하는 것은 Pig 클래스가 sound() 메서드를 오버라이딩 해서 "꿀꿀" 이라는 소리가 나도록 하는 것이다. 그런데 개발자가 실수로 sound() 메서드를 오버라이딩하는 것을 빠트릴 수 있다. 이렇게 되면 부모의 기능을 상속 받는다. 따라서 코드상 아무런 문제가 발생하지 않는다. 물론 프로그램을 실행하면 기대와 다르게 "꿀꿀"이 아니라 부모 클래스에 있는 Animal.sound()가 호출될 것이다.

package poly.ex2;

public class Pig extends Animal {
//    @Override
//    public void sound() {
//        System.out.println("꿀꿀");
//    }
    //오버라이딩 코드를 깜빡하고 작성하지 않음
}
package poly.ex2;

public class AnimalPolyMain3 {
    public static void main(String[] args) {
        //2. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩하지 않을 가능성
        Animal[] animalArr = {new Dog(), new Cat(), new Cow(), new Pig()};
        for (Animal animal : animalArr) {
            soundAnimal(animal);
        }
        //1. Animal 클래스를 생성할 수 있는 문제
        Animal animal = new Animal();
    }
    //동물이 추가되어도 변하지 않는 코드
    public static void soundAnimal(Animal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

실행 결과

"꿀꿀" 대신 "동물 울음 소리"가 출력된다. (Animal.sound()가 호출됨)

 

좋은 프로그램은 제약이 있는 프로그램이다. 추상 클래스추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있다.

 


추상 클래스1

추상 클래스

동물(Animal)과 같이 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스추상 클래스라 한다. 추상 클래스는 이름 그대로 추상적인 개념을 제공하는 클래스이다. 따라서 실체인 인스턴스가 존재하지 않는다. 대신에 상속을 목적으로 사용되고, 부모 클래스 역할을 담당한다.

abstract class AbstractAnimal {...}
  • 추상 클래스는 클래스를 선언할 때 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 된다.
  • 추상 클래스는 기존 클래스와 완전히 같다. 다만 new AbstractAnimal(); 와 같이 직접 인스턴스를 생성하지 못하는 제약이 추가된 것이다.

 

추상 메서드

 

부모 클래스를 상속받는 자식 클래스가 반드시 오버라이딩해야 하는 메서드를 부모 클래스에 정의할 수 있다. 이것을 추상 메서드라 한다. 추상 메서드는 이름 그대로 추상적인 개념을 제공하는 메서드이다. 따라서 실체가 존재하지 않고, 메서드 바디가 없다.

public abstract void sound();
  • 추상 메서드는 선언할 때 메서드 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 된다.
  • 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
    • 그렇지 않으면 컴파일 오류가 발생한다.
    • 추상 메서드는 메서드 바디가 없다. 따라서 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있다. 따라서 직접 생성하지 못하도록 추상 클래스로 선언해야 한다.
  • 추상 메서드는 상속받는 자식 클래스가 반드시 오버라이딩해서 사용해야 한다.
    • 그렇지 않으면 컴파일 오류가 발생한다.
    • 추상 메서드는 자식 클래스가 반드시 오버라이딩해야 하기 때문에 메서드 바디 부분이 없다. 바디 부분을 만들면 컴파일 오류가 발생한다.
    • 오버라이딩하지 않으면 자식도 추상 클래스가 되어야 한다.
  • 추상 메서드는 기존 메서드와 완전히 같다. 다만 메서드 바디가 없고, 자식 클래스가 해당 메서드를 반드시 오버라이딩해야 한다는 제약이 추가된 것이다.

이제 추상 클래스와 추상 메서드를 사용해서 예제를 만들어보자.

 

예제3

package poly.ex3;

public abstract class AbstractAnimal { //추상 클래스
    public abstract void sound(); //추상 메서드

    public void move(){
        System.out.println("동물이 움직입니다.");
    }
}
  • public abstract class AbstractAnimal : 이 클래스는 abstract가 붙은 추상 클래스이기 때문에 직접 인스턴스를 생성할 수 없다.
AbstractAnimal animal = new AbstractAnimal(); //컴파일 오류

컴파일 오류: AbstractAnimal이 추상이어서 인스턴스 생성이 불가능하다

  •  sound()는 abstarct가 붙은 추상 메서드이기 때문에 자식이 이 메서드를 반드시 오버라이딩해야 한다.
package poly.ex3;

public class Cat extends AbstractAnimal{
    //sound()를 오버라이딩하지 않아서 컴파일 오류 발생
}

컴파일 오류: Cat이 추상 클래스가 되어야 함 or 추상 메서드 sound()가 오버라이딩되어야 한다는 의미

  • move()는 메서드 바디가 있기 때문에 추상 메서드가 아니다. 따라서 자식 클래스가 오버라이딩하지 않아도 된다.

[자식 클래스 3개]

package poly.ex3;

public class Cat extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}
package poly.ex3;

public class Cow extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("음매");
    }
}
package poly.ex3;

public class Dog extends AbstractAnimal{
    @Override
    public void sound(){
        System.out.println("멍멍");
    }
}

 

[메인 코드]

package poly.ex3;

public class AbstractMain {
    public static void main(String[] args) {
        //추상클래스 생성 불가
        //AbstractAnimal animal = new AbstractAnimal(); //컴파일 오류

        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

        cat.sound(); //오버라이딩된 메서드 사용
        cat.move(); //부모 메서드 사용

        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(cow);
    }
    //동물이 추가되어도 변하지 않는 코드
    public static void soundAnimal(AbstractAnimal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

실행 결과

 

정리

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

 


추상 클래스2

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

앞서 만든 예제에서 move()도 추상 메서드로 만들어야 한다고 가정해보자. 이 경우 AbstractAnimal 클래스의 모든 메서드가 추상 메서드가 된다. 이런 클래스를 순수 추상 클래스라 한다.

 

move() 가 추상 메서드가 되었으니 자식들은 AbstractAnimal의 모든 기능을 오버라이딩 해야 한다.

 

예제4

package poly.ex4;

public abstract class AbstractAnimal { //순수 추상 클래스
    public abstract void sound(); //추상 메서드

    public abstract void move(); //추상 메서드
}
package poly.ex4;

public class Cat extends AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("냐옹");
    }

    @Override
    public void move() {
        System.out.println("고양이 이동");
    }
}
package poly.ex4;

public class Cow extends AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("음매");
    }

    @Override
    public void move() {
        System.out.println("소 이동");
    }
}
package poly.ex4;

public class Dog extends AbstractAnimal {
    @Override
    public void sound(){
        System.out.println("멍멍");
    }

    @Override
    public void move() {
        System.out.println("개 이동");
    }
}
package poly.ex4;

public class AbstractMain {
    public static void main(String[] args) {
        //추상클래스 생성 불가
        //AbstractAnimal animal = new AbstractAnimal();

        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(cow);

        moveAnimal(dog);
        moveAnimal(cat);
        moveAnimal(cow);
    }
    //동물이 추가되어도 변하지 않는 코드
    public static void soundAnimal(AbstractAnimal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
    public static void moveAnimal(AbstractAnimal animal){
        System.out.println("동물 이동 테스트 시작");
        animal.move();
        System.out.println("동물 이동 테스트 종료");
    }
}

실행 결과

 

순수 추상 클래스

package poly.ex4;

public abstract class AbstractAnimal { //순수 추상 클래스
    public abstract void sound(); //추상 메서드

    public abstract void move(); //추상 메서드
}

모든 메서드가 추상 메서드인 순수 추상 클래스는 코드를 실행할 바디 부분이 전혀 없다. 이러한 순수 추상 클래스는 실행 로직을 전혀 가지고 있지 않다. 단지 다형성을 위한 부모 타입으로서 껍데기 역할만 제공할 뿐이다.

 

순수 추상 클래스의 특징

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

상속하는 클래스는 모든 메서드를 구현해야 한다.

순수 추상 클래스는 마치 어떤 규격을 지켜서 구현해야 하는 것처럼 느껴진다. AbstractAnimal의 경우 sound() , move() 라는 규격에 맞추어 구현을 해야 한다.

이것은 우리가 일반적으로 이야기하는 인터페이스와 같이 느껴진다. 예를 들어서 USB 인터페이스를 생각해보자. USB 인터페이스는 분명한 규격이 있다. 이 규격에 맞추어 제품을 개발해야 연결이 된다. 순수 추상 클래스가 USB 인터페이스 규격이라고 한다면 USB 인터페이스에 맞추어 마우스, 키보드 같은 연결 장치들을 구현할 수 있다.

 

이런 순수 추상 클래스의 개념은 프로그래밍에서 매우 자주 사용된다. 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공한다.

 


인터페이스

자바는 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능을 제공한다.

 

순수 추상 클래스

public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}

 

인터페이스는 class가 아니라 interface 키워드를 사용하면 된다.

 

인터페이스

public interface InterfaceAnimal {
    void sound(); //public abstract
    void move(); //public abstract
}
  • 인터페이스의 메서드는 모두 public, abstract이다.
  • 메서드에 public abstract를 생략할 수 있으며, 생략이 권장된다.
  • 인터페이스는 다중 구현(다중 상속)을 지원한다.

 

예제5

클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속) 관계는 UML에서 점선을 사용한다.

 

package poly.ex5;

public interface InterfaceAnimal {
    void sound(); //public abstract
    void move(); //public abstract
}

인터페이스는 class 대신에 interface로 선언하면 된다.

sound() , move()는 앞에 public abstract가 생략되어 있다. 따라서 상속 받는 곳에서 모든 메서드를 오버라이딩 해야 한다.

 

package poly.ex5;

public class Dog implements InterfaceAnimal{
    @Override
    public void sound() {
        System.out.println("멍멍");
    }

    @Override
    public void move() {
        System.out.println("개 이동");
    }
}

인터페이스를 상속 받을 때는 extends 대신에 implements라는 구현이라는 키워드를 사용해야 한다. 인터페이스는 그래서 상속이라 하지 않고 구현이라 한다.

package poly.ex5;

public class Cat implements InterfaceAnimal{
    @Override
    public void sound() {
        System.out.println("냐옹");
    }

    @Override
    public void move() {
        System.out.println("고양이 이동");
    }
}
package poly.ex5;

public class Cow implements InterfaceAnimal{
    @Override
    public void sound() {
        System.out.println("음매");
    }

    @Override
    public void move() {
        System.out.println("소 이동");
    }
}
package poly.ex5;


public class InterfaceMain {
    public static void main(String[] args) {
        //인터페이스는 순수 추상 클래스이기 때문에 인스턴스를 생성할 수 없다.
        //InterfaceAnimal interfaceAnimal = new InterfaceAnimal();

        Cat cat = new Cat();
        Dog dog = new Dog();
        Cow cow = new Cow();

        soundAnimal(cat);
        soundAnimal(dog);
        soundAnimal(cow);

        moveAnimal(cat);
        moveAnimal(dog);
        moveAnimal(cow);
    }

    public static void soundAnimal(InterfaceAnimal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
    public static void moveAnimal(InterfaceAnimal animal){
        System.out.println("동물 이동 테스트 시작");
        animal.move();
        System.out.println("동물 이동 테스트 종료");
    }
}

실행 결과

 

상속 vs 구현
  • 상속: 클래스
    부모의 기능을 물려 받는 것이 목적
  • 구현: 인터페이스
    인터페이스는 모든 메서드가 추상 메서드이다. 따라서 물려받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩해서 기능을 구현해야 한다. 따라서 구현한다고 표현한다.
    → 인터페이스는 메서드 이름만 있는 설계도. 설계도가 실제로 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 한다.
인터페이스를 사용해야 하는 이유
  1. 제약
    인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현해라는 규약(제약)을 주는 것이다. USB 인터페이스를 생각해보자. USB 인터페이스에 맞추어 키보드, 마우스를 개발하고 연결해야 한다. 그렇지 않으면 작동하지 않는다. 인터페이스의 규약(제약)은 반드시 구현해야 하는 것이다. 그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있다. 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 된다. 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 이런 문제를 원천 차단할 수 있다.
  2. 다중 구현
    자바에서 클래스 상속은 부모를 하나만 지정할 수 있다. 반면에 인터페이스는 부모를 여러 명 두는 다중 구현(다중 상속)이 가능하다.

 


인터페이스 - 다중 구현

자바의 클래스는 다이아몬드 문제 때문에 다중 상속을 허용하지 않는다.

다이아몬드 문제

만약 비행기와 자동차를 상속 받아서 하늘을 나는 자동차를 만든다고 가정해보자. 만약 그림과 같이 다중 상속을 사용하게 되면 AirplaneCar 입장에서 move()를 호출할 때 어떤 부모의 move()를 사용해야 할지 애매한 문제가 발생한다. 이것을 다이아몬드 문제라 한다. 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해지 수 있다. 이런 문제점 때문에 자바는 클래스의 다중 상속을 허용하지 않는다. 대신에 인터페이스의 다중 구현을 허용하여 이러한 문제를 피한다.

 

클래스는 다중 상속이 안되는데, 인터페이스의 다중 구현은 허용한 이유가 뭘까?

인터페이스는 모두 추상 메서드로 이루어져 있기 때문이다.

 

 

인터페이스 다중 구현 그림

InterfaceA , InterfaceB는 둘다 같은 methodCommon()을 가지고 있다. 그리고 Child는 두 인터페이스를 구현했다. 상속 관계의 경우 두 부모 중에 어떤 한 부모의 methodCommon()을 사용해야 할지 결정해야 하는 다이아몬드 문제가 발생한다.

 

하지만 인터페이스 자신은 구현을 가지지 않는다. 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 한다. 여기서 InterfaceA , InterfaceB 는 같은 이름의 methodCommon() 를 제공하지만 이것의 기능은 Child 가 구현한다.

그리고 오버라이딩에 의해 어차피 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");
    }
}

 

  • implements InterfaceA, InterfaceB와 같이 다중 구현을 할 수 있다. implements 키워드 위에 ,로 여러 인터페이스를 구분하면 된다.
  • 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();
    }
}

실행 결과

 


클래스와 인터페이스 활용

이번에는 클래스 상속과 인터페이스 구현을 함께 사용하는 예를 알아보자.

  • AbstractAnimal은 추상 클래스다.
    • sound(): 동물의 소리를 내기 위한 sound() 추상 메서드를 제공한다.
    • move(): 동물의 이동을 표현하기 위한 메서드이다. 이 메서드는 추상 메서드가 아니다. 상속을 목적으로 사용된다.
  • Fly는 인터페이스이다. 나는 동물은 이 인터페이스를 구현할 수 있다.
    • Bird, Chicken은 날 수 있는 동물이다. fly() 메서드를 구현해야 한다.

 

예제6

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 인터페이스를 구현한다.

 

하나의 클래스 상속, 여러 인터페이스 구현 예시

public class Bird extends AbstractAnimal implements Fly, Swim {

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 사용 가능
    public 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("날기 테스트 종료");
    }
}

실행 결과

 

+ Recent posts