점프투자바

SUADI·2022년 5월 10일

6. 객체지향 프로그래밍

(8) 다형성(Polymorphism)

다형성이란 프로그램 언어 내의 각 요소들(클래스, 메소드, 인터페이스 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질이다. 즉, 하나의 타입에 여러 객체들을 대입할 수 있는 성질을 뜻한다. 다형성을 활용하면 기능을 확장해야 하거나 객체를 변경해야 할 때 타입 변경없이 객체 주입만으로 수정할 수 있다.

경비원(Bouncer) 클래스를 추가하여 동물들이 짖게 해서(barkAnimal) 동물원을 지키는 코드를 짠다고 해보자.

...
class Bouncer {
	void barkAnimal(Animal animal) {
    	if (animal instanceof Lion) {
        	System.outprintln("어흥!");
        } else if (animal instanceof Dog) {
        	System.outprintln("왈왈!");
        }
    }
}

public class Sample {
	public static void main(String[] args) {
    	...
        Bouncer bouncer = new Bouncer();
        
        bouncer.barkAnimal(lion);
        bouncer.barkAnimal(dog);
    }
}
어흥!
왈왈!
  • Bouncer 클래스 내에 barkAnimal 메소드를 생성하여 main메소드에서 bouncer 객체를 생성한 후 barkAnimal 메소드를 불러와 동물이 짖는 소리를 출력하도록 한다.

  • barkAnimal의 입력인자로 Animal 클래스 자료형을 사용한다. 하지만 main메소드에서 barkAnimal을 호출할 때는 dog, lion 객체를 전달할 수 있다. 그 이유는 Dog, Lion 클래스(자식 클래스)는 Animal 클래스(부모 클래스)를 상속받았기 때문이다. 자식 클래스에 의해 생성된 객체는 부모 클래스의 자료형으로 사용할 수 있다.

  • barkAnimal 메소드 바디에 조건문으로 animal 객체가 어떤 클래스의 인스턴스인지에 따라 짖는 문구 출력을 달리 한다. animal 객체가 Dog클래스의 객체이면 왈왈!, Lion 클래스의 객체이면 어흥!을 출력한다.

  • 만약 barkAnimal 메소드에 수많은 다른 동물들도 추가하게 되면 동물을 추가할 때마다 분기문이 추가되어야 하므로 보기에 깔끔한 코드라고 할 수 없다. 전에 학습했던 인터페이스를 활용하여 좀 더 깔끔한 코드를 작성해 보면

...

interface Barkable {
	void bark();
}

class Dog extends Animal implements Predator, Barkable {
	...
    
    void bark() {
      	System.outprintln("왈왈!");
    }
}

class Lion extends Animal implements Predator, Barkable {
	...
    
    void bark() {
		System.outprintln("어흥!");
    }
}

class Bouncer {
	void barkAnimal(Barkable animal) {
    	animal.bark();
    }
}

public class Sample {
	...
    
    	Bouncer bouncer = new Bouncer();
        
        bouncer.barkAnimal(dog);
        bouncer.barkAnimal(lion);
}
왈왈!
어흥!
  • Barkable 인터페이스를 생성한 후 bark 메소드를 만든다. interface 내에서 메소드를 생성할 때는 디폴트 메소드를 이용하지 않는 이상 내용을 채울 수 없고, 인터페이스를 implement(수행)하는 클래스에서 바디를 작성한다.

  • 짖는 동물은 Dog, Lion 클래스이므로 각 클래스에 Barkable 인터페이스를 implements한다. 인터페이스는 클래스와 달리 두 개 이상의 인터페이스를 implement할 수 있다.

  • 각 클래스 내에 짖는 소리를 출력하도록 바디를 작성한다.

  • Bouncer 클래스의 barkAnimal 메소드를 이전과 달리 깔끔하게 작성할 수 있다.

변경 전

class Bouncer {
	void barkAnimal(Animal animal) {
    	if (animal instanceof Dog) {
        	System.out.println("왈왈!");
        } else if (animal instanceof Lion) {
        	System.out.println("어흥"!);      
        }
    }
}

변경 후

class Bouncer {
	void barkAnimal(Barkable animal) {
    	animal.bark();
    }
}
  • 클래스들이 더 추가되어 분기문을 더 추가할 수록 코드의 깔끔함의 정도는 더 크게 차이가 날 것이다. 전에 학습했던 것처럼 코드가 깔끔해지는 것 보다도 인터페이스를 사용함으로써 클래스의 추가 및 변경 등에 상관없이 독립적으로 사용할 수 있게 되었다는 점이 인터페이스의 큰 장점이다.

  • barkAnimal 메소드의 입력인자가 Animal 자료형에서 Barkable 자료형으로 변경되었다. 각 동물들의 클래스는 Animal 클래스를 상속받기도 했지만 Barkable 인터페이스를 상속받기도 했다. 자식 클래스의 객체가 부모 클래스를 자료형 타입으로 사용할 수 있는 것처럼 인터페이스 역시 상속을 하면 자료형 타입으로 사용할 수 있다. 이렇게 하나의 객체가 여러개의 자료형 타입을 가질 수 있는 성질을 다형성(polymorphism)이라고 한다.

  • Dog, Lion 클래스는 Predator, Barkable 인터페이스 두개를 모두 implements했는데 상속을 하나만 할 수 있는 클래스(단일상속)와 달리 인터페이스는 여러개를 implement(다중상속)할 수 있다.

  • Predator로 선언된 predator 객체와 Barkable로 선언된 barkable 객체는 각각 getFood 메소드와 bark메소드를 가지고 있는데 각 객체는 각자가 가지고 있는 메소드만을 사용할 수 있다. getFood 메소드와 bark메소드를 모두 이용하고 싶다면 어떻게 해야할까? predator과 Barkable 인터페이스를 모두 구현(implement)한 Dog나 Lion 클래스로 선언된 객체를 그대로 사용하거나 Predaotor와 Barkable 인터페이스를 상속받는 새로운 인터페이스를 생성하는 것도 방법이다. 두 인터페이스를 모두 상속받는 인터페이스를 생성하면 각각이 가지고 있는 메소드를 모두 사용할 수 있다.

interface BarkablePredator extends Barkable, Predator{
}

...

class Lion extends Animal implements BarkablePredator { 
	// Barkable, Predator 인터페이스 두개를 implements 한 코드에서 
    // BarkablePredator 인터페이스를 implement하도록 코드를 변경했다.
	...
}

class Bouncer {
	void barkAnimal(Barkable animal) { 
    	// type을 Barkable로 그대로 두어도 괜찮다.
    	...
    }
}

public class Sample {
	...
    	bouncer.barkAnimal(lion);
}
  • 결과는 이전과 동일하다. Bouncer 클래스의 barkAnimal 메소드 입력인자를 Barkable로 그대로 두더라도 BarkablePredator 인터페이스를 implement한 lion 객체를 전달할 수 있다. 자식 클래스의 객체 자료형을 부모 클래스의 객체 자료형으로 사용할 수 있는 것처럼 자식 인터페이스(BarkablePredator)의 객체 자료형을 부모 인터페이스(Barkable)의 객체 자료형으로 사용할 수 있기 때문이다.

(9) 추상 클래스(Abstract Class)

추상 클래스는 인터페이스의 특징과 유사하다. 추상 클래스란 실체 클래스들의 공통적인 부분을 추출해 어느정도 규격을 잡아놓은 추상적인 클래스이다. 예를 들어 사람들의 열굴을 표현한다고 할 때, 대부분의 사람이 눈 2개, 코 1개, 입 1개, 귀 2개이고, 각각의 위치가 대략적으로는 정해져 있다. 하지만 각각의 생김새와 크기 등은 조금씩 다르다. 대략적으로 정해져 있는 공통적인 특성을 추상 클래스에 정의를 해놓고, 추상 클래스를 상속받은 실체 클래스에서 디테일한 부분을 정의하면 추후에 코드를 수정하거나 추가, 제거할 때 편리하다.

abstract class Predator extends Animal{ 
// interface에서 abstract class로 변경
	abstract String getFood();
    
    void printFood() { // default 제거
    	System.out.println("My food is %s\n", getFood());
    }
    
    static int LEG_COUNT = 4; // static 붙임
    static int speed() {
    	return LEG_COUNT * 30;
    }
}

...

class Dog extends Predator implements Barkable {
// Animal을 상속하던 것에서 Predator을 상속하는 것으로 변경
	...
}

...
  • 인터페이스와 마찬가지로 추상 클래스에서도 어느정도의 규격만 잡아놓는 역할을 하기 때문에 추상 메소드를 정의만 하고 내용은 해당 추상 클래스를 상속받는 자식 클래스에서 작성한다.

  • Animal의 기능을 유지하기 위해 Animal을 상속한다.

  • default method는 인터페이스 내에서 사용되는 메소드이다. 추상 클래스 내에서는 추상 메소드 뿐만 아니라 일반 메소드도 생성할 수 있으므로 default를 지우고 일반메소드를 선언한다.

  • 인터페이스 내의 상수는 자동적으로 static으로 인식하지만 추상 클래스에서 상수를 정의할땐 명시적으로 static을 작성해야만 한다.

  • 추상 클래스와 인터페이스는 상속받는 클래스 혹은 인터페이스 내의 추상 메소드를 구현하도록 강제한다는 점에서 유사하다. 하지만 추상 클래스는 역시나 클래스이기 때문에 다중 상속이 불가한 반면, 인터페이스는 다중 상속이 가능하다. 그리고 추상 클래스는 인터페이스와 달리 일반 클래스처럼 객체변수, 생성자, private 메소드 등을 가질 수 있다. 예전에는 추상 클래스 내에서는 일반 메소드를 생성하여 내용을 정의할 수 있었던 반면에 인터페이스는 그것이 불가능했지만 자바8 이후부터 디폴트 메소드를 사용할 수 있기 때문에 차이점이 더 모호해지기는 했다.

0개의 댓글