본문 바로가기

Studying/JAVA

[JAVA]_Polymorphism2(다형성2)

● 다형성 사용하지 않은 코드 ●

 

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

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

        System.out.println();

        soundCow(cow);
    }

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

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료

동물 소리 테스트 시작
야옹
동물 소리 테스트 종료

동물 소리 테스트 시작
음메
동물 소리 테스트 종료

기존 코드에 새로운 동물을 추가하면, 해당 동물의 클래스 생성 후 다음 코드도 추가해야 함.

 

예를 들어 Duck(오리) 클래스 생성하면

Duck duck = new Duck();

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

 

위에 코드를 매번 동물이 추가될 때마다 계속 작성해야 하는 불편함이 생김.

 

main 클래스에서 중복된 코드가 있는 것을 알 수 있음.

중복 제거하고 싶으면 메서드 사용하거나, 배열과 for문을 사용하면 됨.

하지만 Dog, Cat, Cow는 서로 완전히 다른 클래스이므로 메서드를 사용하면 다음과 같이 매개변수의 클래스를 Dog, Cat, Cow 중 하나로 정해야 함.

 

만약 메서드 사용하면 매개변수의 클래스를 Dog, Cat, Cow 중 하나로 정해야 하는데 이 중 하나를 선택해 전용 메서드를 만들면 그 나머지의 클래스는 인수로 사용할 수 없음.

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

 

문제의 핵심은 타입이 다른 것.

Dog, Cat, Cow가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있음.

이것이 다형성을 활용하는 이유임.


● 다형성 사용한 코드 ●

 

다형성을 사용하기 위해서는 상속관계를 사용.

Animal(동물)이라는 부모 클래스 생성 후 sound() 메서드를 정의하는데, 이 메서드는 자식 클래스에서 오버라이딩 할 목적으로 만듦.

 

package poly.ex2;

public class Dog extends Animal {

    @Override
    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 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) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
실행결과

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료

실행결과는 기존 코드와 동일.

 

[AnimalPolyMain1]

 

soundAnimal(dog)을 호출하면 soundAnimal(Animal animal)에 Dog 인스턴스가 전달.

Animal animal = dog로 이해하면 됨. 부모는 자식을 담을 수 있고, Animal은 Dog의 부모임.

animal 변수의 타입은 Animal이므로 Dog 인스턴스에 있는 Animal 클래스 부분을 찾아 sound() 메서드 호출을 시도.

그런데 하위 클래스인 Dog에서 sound() 메서드를 오버라이딩 했으므로 Dog 클래스에 있는 sound() 메서드가 호출됨.

(오버라이딩 당한 메서드가 우선권을 가지기 때문)

 

다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Cow의 인스턴스를 참조할 수 있음.

메서드 오버라이딩 덕분에 animal.sound()를 호출해도 Dog.sound(), Cat.sound(), Cow.sound()와 같이 각 인스턴스의 메서드를 호출할 수 있음.

 

따라서 다형성 덕분에 새로운 동물을 추가해도 private static voud soundAnimal(Animal animal) { ... } 이 부분은 그대로 재사용 가능.


○ 중복 제거 ○

package poly.ex2;

public class AnimalPolyMain2 {
    public static void main(String[] args) {

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

        Animal[] animalArr = {dog, cat, cow, duck};

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

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료

[AnimalPolyMain2]

 

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

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


○ 조금 더 개선 ○

package poly.ex2;

public class AnimalPolyMain3 {
    public static void main(String[] args) {

        Animal[] animalArr = {new Dog(), new Cat(), new Cow()};     // ctrl + alt + N
        for (Animal animal : animalArr) {
            soundAnimal(animal);
        }
    }

    // 변하지 않는 부분
    private static void soundAnimal(Animal animal) {    // ctrl + alt + M
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

[AnimalPolyMain3]

 

새로운 동물이 추가되어도 soundAnimal(...) 메서드는 코드 변경 없이 유지 가능.

 

새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드.

이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋음.

 

하지만 여기에서도 문제가 있음.

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

 

우리는 Animal 클래스를 생성해서 사용할 일이 적음.

(Animal animal  = new Animal();)

동물이라는 추상적인 개념이 실제로 존재하는 것은 이상할 수 있음. 사실 이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없음.

 

또한 Aniimal을 상속받은 Duck 클래스를 만들었다고 가정하자.

Duck 클래스가 sound() 메서드를 오버라이딩 하지 않고 main 클래스에서 객체 생성하여 실행했을 때 아무 문제 없이 실행하게 됨.

Duck 클래스에 있는 sound 메서드가 아닌 부모 클래스에 있는 sound() 메서드가 실행될 것임.

 

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


▶ 추상클래스

 

동물(Animal) 과 같이 부모 클래스는 제공하지만, 실제 생성되면 안 되는 클래스를 추상클래스라고 함.

추상 클래스는 이름 그대로 추상적인 개념을 제공하는 클래스임. 따라서 실체인 인스턴스가 존재하지 않음.

대신 상속을 목적으로 사용되고, 부모 클래스 역할을 담당함.

 

abstract class AbstractAnimal { ... } 처럼

추상 클래스는 클래스 선언 시 추상이라는 의미의 abstract 키워드를 붙여주면 됨.

추상 클래스는 기존 클래스와 완전히 같지만 new AbstractAnimal()과 같이 직접 인스턴스를 생성하지 못하는 제약이 추가.


▶ 추상메서드

 

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

추상 메서드는 이름 그대로 추상적인 개념을 제공하는 메서드이므로, 실체가 존재하지 않고, 메서드 바디가 없음.

 

public abstract void sound(); 처럼

추상 메서드 선언할 때 메서드 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 됨.

추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 함.

그렇지 않으면 컴파일 오류가 발생함.

추상 메서드는 메서드 바디가 없음. 따라서 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있으므로 직접 생성하지 못하도록 추상클래스로 선언해야 함.

 

추상 메서드는 상속받는 자식 클래스가 반드시 오버라이딩해서 사용해야 함.

그렇지 않으면 컴파일 오류가 발생함.

추상 메서드는 자식 클래스가 반드시 오버라이딩 해야 하기 때문에 메서드 바디가 없으므로 바디 부분을 만들면 컴파일 오류 발생.

오버라이딩 하지 않으면 자식도 추상 클래스가 되어야 함.

 

추상 메서드는 기존 메서드와 완전히 같지만 메서드 바디가 없고, 자식 클래스가 해당 메서드를 반드시 오버라이딩 해야 한다는 제약이 추가된 것.


package poly.ex3;

public abstract class AbstractAnimal {

    public abstract void sound();
    
    public void move() {
        System.out.println("동물이 움직입니다.");
    }
}

[AbstractAnimal]

 

abstract가 붙은 추상 클래스.

이 클래스는 직접 인스턴스를 생성할 수 없음.

sound()는 abstract가 붙은 추상 메서드. 이 메서드는 자식이 반드시 오버라이딩 해야 함.

move() 메서드는 추상메서드가 아니므로 자식 클래스가 오버라이딩 하지 않아도 됨.

 

package poly.ex3;

public class Dog extends AbstractAnimal{

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
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 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();

        System.out.println();

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

    // 변하지 않는 부분
    private static void soundAnimal(AbstractAnimal animal) {    // ctrl + alt + M
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
실행결과

야옹
동물이 움직입니다.

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료

[AbstractMain]

 

AbstractAnimal animal = new AbstractAnimal(); 생성 불가능.

추상클래스이므로 인스턴스 생성이 불가능하다는 뜻.

 

정리해 보면,

추상 클래스 덕분에 실수로 Animal 인스턴스를 생성할 문제를 근본적으로 방지.

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


▶ 순수 추상 클래스

 

모든 메서드가 추상 메서드인 추상클래스.

앞에 코드에서 sound() 뿐만 아니라 move()도 abstract을 붙여 추상 메서드를 만들면 AbstractAnimal은 순수 추상 클래스가 됨.

 

package poly.ex4;

// 순수 추상 클래스
public abstract class AbstractAnimal {

    public abstract void sound();
    public abstract void move();
}
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 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 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();

        System.out.println();

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

        System.out.println();

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

    // 변하지 않는 부분
    private static void soundAnimal(AbstractAnimal animal) {    // ctrl + alt + M
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }

    private static void moveAnimal(AbstractAnimal animal) {    // ctrl + alt + M
        System.out.println("동물 이동 테스트 시작");
        animal.move();
        System.out.println("동물 이동 테스트 종료");
    }
}
실행결과

야옹
고양이 이동

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료

동물 이동 테스트 시작
개 이동
동물 이동 테스트 종료
동물 이동 테스트 시작
고양이 이동
동물 이동 테스트 종료
동물 이동 테스트 시작
소 이동
동물 이동 테스트 종료

 

● 순수 추상 클래스 특징 ●

- 인스턴스 생성 불가능

- 상속 시 자식은 모든 메서드를 오버라이딩 해야 함.

- 주로 다형성을 위해 사용.

 

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

상속시 자식은 모든 메서드를 오버라이딩해야 한다라는 특징은 상속받는 클래스 입장에서 보면 부모의 모든 메서드를 구현해야 하는 것.

이런 특징을 잘 생각해 보면 순수 추상 클래스는 마치 어떤 규격을 지켜서 구현해야 하는 것처럼 보임.


▶ 인터페이스

 

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

 

// 순수추상 클래스
public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}

// 인터페이스
public interface InterfaceAnimal {
    public abstract void sound();
    public abstract void move();
}

순수추상 클래스와 다르게 인터페이는 interface 키워드를 사용.

인터페이스는 public abstract 키워드 생략가능하며 void sound();, void move(); 이렇게 사용 가능.

 

● 인터페이스 특징 ●

순수추상 클래스에 약간의 편의 기능 추가됨.

- 인터페이스의 메서드는 모두 public, abstract임.

- 메서드에 public abstract 생략 가능. (참고로 생략 권장)

- 인터페이스는 다중 구현(다중 상속) 지원


상속관계(좌) / 구현관계(우)

 

상속 관계의 UML에서는 실선을 사용하지만, 구현(상속) 관계의 UML에서는 점선을 사용.

 

package poly.ex5;

public class Dog implements InterfaceAnimal{

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

    @Override
    public void move() {
        System.out.println("개 이동");
    }
}
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();

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

        System.out.println("interface");
        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(cow);

    }

    // 변하지 않는 부분
    private static void soundAnimal(InterfaceAnimal animal) {    // ctrl + alt + M
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
interface
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료

인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 됨.


▶ 상속 / 구현

 

부모 클래스의 기능을 자식 클래스가 상속받을 때, 클래스는 상속 받는다고 표현.

부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현.

상속은 이름 그대로 부모의 기능을 물려받는 것이 목적이지만, 인터페이스는 모든 메서드가 추상 메서드이므로 물려받을 수 있는 기능은 없고 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 함.

인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 함.

 

● 인터페이스 사용 이유 ●

 

① 제약

인터페이스의 규약(제약)은 반드시 구현해야 하는 것.

만약 순수 추상 클래스에서 누군가 그곳에 실행 가능한 메서드를 끼워 넣고, 추가된 기능을 자식 클래스에서 구현할 수도, 하지 않을 수도 있으므로 더는 순수 추상 클래스가 아니게 됨.

따라서 인터페이스는 이러한 문제를 원천 차단할 수 있음.

 

② 다중구현

자바에서 클래스 상속은 부모를 하나만 지정할 수 있지만, 인터페이스는 부모를 여러 명 두는 다중 구현(다중 상속) 가능.


▶ 인터페이스_다중구현

 

인터페이스는 자신은 구현을 가지지 않고, 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 함.

 

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");
    }
}
package poly.diamond;

public class DiamondMain {
    public static void main(String[] args) {

        InterfaceA a = new Child();
        a.methodA();
        a.methodCommon();

        System.out.println();

        InterfaceB b = new Child();
        b.methodB();
        b.methodCommon();
    }
}
실행결과

Child.methodA
Child.methodCommon

Child.methodB
Child.methodCommon

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

 

 

AbstractAnimal 은 추상 클래스

   - sound() : 동물 소리를 내기 위한 sound() 추상 메서드 제공.

   - move() : 동물의 이동을 표현하기 위한 메서드. 이 메서드는 추상 메서드가 아니고, 상속을 목적으로 사용.

 

Fly 는 인터페이스 → 나는 동물은 이 인터페이스를 구현 가능

   - 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("멍멍");
    }
}
package poly.ex6;

public class Bird extends AbstractAnimal implements Fly{

    @Override
    public void sound() {
        System.out.println("짹짹");
    }

    @Override
    public void fly() {
        System.out.println("새 날기");
    }
}
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);

        System.out.println();

        // flyAnimal(dog);  // 불가능
        flyAnimal(bird);    // Fly fly = bird;랑 같음
        flyAnimal(chicken);

    }

    // AbstractAnimal 사용가능
    private static void soundAnimal(AbstractAnimal animal) {    // ctrl + alt + M
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }

    // Fly 인터페이스가 있으면 사용 가능
    private static void flyAnimal(Fly fly) {
        System.out.println("날기 테스트 시작");
        fly.fly();
        System.out.println("날기 테스트 종료");
    }
}
실행결과

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

날기 테스트 시작
새 날기
날기 테스트 종료
날기 테스트 시작
닭 날기
날기 테스트 종료

AbstractAnimal을 상속한 Dog, Bird, Chicken을 전달해서 실행 가능.

'Studying > JAVA' 카테고리의 다른 글

[JAVA]_다형성과 설계  (0) 2025.03.20
[JAVA]_Polymorphism1(다형성1)  (0) 2025.03.12
[JAVA]_Inheritance(상속)  (0) 2025.02.24
[JAVA]_final  (0) 2025.02.18
[JAVA]_자바 메모리 구조와 static  (1) 2025.02.11