[JAVA] 다형성_2
다형성의 활용
다형성을 왜? 사용하는지
다형성을 사용하지 않고 프로그램을 만든 후 다형성을 사용해보자
개, 고양이, 소의 울음 소리를 테스트 하는 프로그램 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Cat {
    public void sound() {
        System.out.println("야옹ㅇ");
    }
}
public class Cow {
    public void sound() {
        System.out.println("음메메메메");
    }
}
public class Dog {
    public void sound() {
        System.out.println("멍멍멍");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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("동물 소리 테스트 종료");
}
- 클래스 마다 sound()메서드를 따로 만들고 호출하는 코드가 중복된다.
- 새로운 동물이 추가될 때마다 같은 구조의 코드가 반복된다.
중복제거
메서드 사용
메서드를 사용해 중복을 제거하려면 매개변의 클래스를 Dog, Cat, Caw 중 하나로 정해야한다.
1
2
3
4
5
private static void soundCaw(Caw caw) {
	System.out.println("동물 소리 테스트 시작");
	caw.sound();
	System.out.println("동물 소리 테스트 종료");
}
배열, for문 사용
1
2
3
4
5
6
Caw[] cawArr = {cat, dog, caw}; //컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
	for (Caw caw : cawArr) {
		cawArr.sound();
	}
System.out.println("동물 소리 테스트 종료");
Dog, Cat, Caw 는 서로 타입(클래스)가 다르기 때문에 배열에 담거나 메서드 매개변수로 함께 전달할 수 없다.
다형성 사용
부모 클래스
1
2
3
4
5
6
7
package poly.ex2;
public class Animal {
    public void sound() {
        System.out.println("동물 울음 소리");
    }
}
자식 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍멍");
    }
}
public class Cow extends Animal {
    @Override
    public void sound() {
        System.out.println("음메메ㅔ메으메메");
    }
}
public class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("야옹야옹");
    }
}
메인 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AnimalPolyMain1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();
        Duck duck = new Duck();
        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(cow);
        soundAnimal(duck);
    }
    private static void soundAnimal(Animal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
- 공통 부모 클래스 Animal을 만들고
- 자식 클래스가 Animal을 상속 + 오버라이딩
- 모든 동물을 Animal타입으로 다룰 수 있음!
- 덕분에 배열과 for문을 활용할 수 있게 된다.
1
2
3
4
5
6
7
public static void main(String[] args) {
    Animal[] animalArr = {new Dog(), new Cat(), new Cow(), new Duck(), new Pig()};
    for (Animal animal : animalArr) {
        soundAnimal(animal);
    }
}
- 다형적 참조 덕분에 animal의 변수 자식인 Dog,Cat,Caw의 인스턴스를 참조할 수 있다. (부모 → 자식 가능)
- 메서드 오버라이딩 덕분에 animal.sound()를 호출해도Dog.sound(),Cat.sound(),Caw.sound()와 같이 각 인스턴스의 메서드를 호출할 수 있다
- Dog,- Cat,- Caw같은 구체 클래스가 아닌 부모 타입- Animal만 사용해서 코드를 유연하게 설계할 수 있다.
- 새로운 동물을 추가해도 soundAnimal()메서드는 코드 변경 없이 재사용 가능하다.
- 이런 코드는 유지보수가 쉽고, 확장성도 높다.
하지만 문제점이 있다.
- Animal을 직접 생성할 수 있다.
1
Animal animal = new Animal(); // 이상하지만 가능함
- Animal은 개념적으로 추상적인 “동물”이지, 실제 동물은 아님
- 그런데 클래스이기 때문에 누군가 실수로 인스턴스를 생성할 수도 있다
- sound()오버라이딩을 누락할 수도 있다.
1
2
3
public class Pig extends Animal {
    // sound() 오버라이딩 안 함
}
- 컴파일 에러는 없지만, 실행 시 Animal.sound()가 호출됨 → 예상치 못한 결과 발생
그래서 자바에서 제공하는 추상 클래스와 추상 메서드를 사용하면 된다.
추상 클래스
추상 클래스를 사용하면 다형성을 더 안전하게 사용할 수 있다.
동물( Animal )과 같이 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 추상 클래스라 한다.
- Animal은 추상적인 개념인데- new Animal()이 가능하다 → ❌
- sound()오버라이딩을 누락해도 문제 없이 컴파일 된다 → ❌
- 실수로 잘못 사용해도 오류가 발생하지 않음 → 위험한 코드
따라서
- Animal을 추상 클래스로 선언하고,- sound()를 추상 메서드로 만들면,
- 자식 클래스는 반드시 sound()를 오버라이딩해야 한다.
1
2
3
4
5
6
7
8
9
public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void eat();
    public void sleep() {
        System.out.println("Sleep");
    }
}
- abstract키워드를 사용하면 인스턴스 생성 불가
- sound(),- eat()을 추상 메서드로 선언해 강제 오버라이딩 가능
- sleep()같은 일반 메서드는 선택적 오버라이딩 가능
자식 클래스
1
2
3
4
5
6
7
8
9
10
11
public class Dog extends AbstractAnimal {
    @Override
    public void sound() {
            System.out.println("Dog.sound");
    }
    @Override
    public void eat() {
        System.out.println("Dog.eat");
    }
}
1
2
3
4
5
6
7
8
9
10
11
public class Cow extends AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("Cow.sound");
    }
    @Override
    public void eat() {
        System.out.println("Cow.eat");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Cat extends AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("Cat.sound");
    }
    @Override
    public void eat() {
        System.out.println("Cat.eat");
    }
    @Override
    public void sleep() {
        System.out.println("Cat.sleep");
    }
}
메인 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class AnimalMain {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Cow cow = new Cow();
        Dog dog = new Dog();
        soundAnimal(cat);
        soundAnimal(cow);
        soundAnimal(dog);
        eatAnimal(cat);
        eatAnimal(cow);
        eatAnimal(dog);
        cat.sleep();
        cow.sleep();
        dog.sleep();
    }
    
    private static void soundAnimal(AbstractAnimal animal) {
        animal.sound();
    }
    public static void eatAnimal(AbstractAnimal animal) {
        animal.eat();
    }
}
//출력결과
Cat.sound
Cow.sound
Dog.sound
Cat.eat
Cow.eat
Dog.eat
Cat.sleep
Sleep
Sleep
순수 추상 클래스
모든 메서드가 추상 메서드인 클래스
1
2
3
4
public abstract class Animal {
    public abstract void sound();
    public abstract void move();
}
- 모든 자식 클래스는 모든 메서드를 구현해야 함
- 인스턴스를 생성할 수 없음
- 실행 로직을 가지고 있지않음 다형성을 위한 부모 타입으로서 껍데기 역할 제공
순수 추상 클래스의 개념은 프로그래밍에서 매우 자주 사용된다. 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공한다.
인터페이스
왜 인터페이스가 필요한지?
- 추상 클래스도 좋지만, 다음과 같은 한계가 존재한다
    - new를 막을 수는 있지만 실행 메서드가 끼어들 수 있음
- 다중 상속(다중 구현)이 불가능
 
- 순수한 설계도로서 기능만 제공하고, 강제성 + 유연성이 모두 필요한 경우에는 인터페이스가 더 적합하다.
인터페이스란?
- interface 키워드로 선언
- 모든 메서드는 기본적으로 public abstract
- 메서드 몸체가 없고, 반드시 자식 클래스가 오버라이딩 해야 함
- 인스턴스 생성 불가능
1
2
3
public interface InterfaceAnimal {
    public void sleep();
}
- implements를 사용해 인터페이스 구현
- 모든 메서드를 반드시 오버라이딩해야 컴파일 오류가 없음
자식 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Cat implements InterfaceAnimal {
    @Override
    public void sleep() {
        System.out.println("cat sleep");
    }
}
public class Dog implements InterfaceAnimal {
    @Override
    public void sleep() {
        System.out.println("dog sleep");
    }
}
public class Cow implements InterfaceAnimal{
    @Override
    public void sleep() {
        System.out.println("cow sleep");
    }
}
메인 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class InterfaceMain {
    public static void main(String[] args) {
        //인터페이스 생성불가
        //InterfaceAnimal animal = new Interfaceanimal();
        Cat cat = new Cat();
        Dog dog = new Dog();
        Cow cow = new Cow();
        sleep(cat);
        sleep(dog);
        sleep(cow);
    }
    private static void sleep(InterfaceAnimal animal) {
        System.out.println("---sleep Start");
        animal.sleep();
        System.out.println("sleep End---");
    }
}
//출력결과
---sleep Start
cat sleep
sleep End---
---sleep Start
dog sleep
sleep End---
---sleep Start
cow sleep
sleep End---
순수 추상 클래스 예제와 거의 유사하다. 순수 추상 클래스가 인터페이스가 되었을 뿐이다.
메서드 규약 = “설계도”
인터페이스는 메서드 이름만 존재하는 설계도이다.
실제 로직은 모두 자식 클래스가 구현한다.
이 구조 덕분에 인터페이스를 기준으로 코드를 작성하면 실제 구현체가 무엇이든 관계없이 유연하게 동작한다.
다형성과 인터페이스
1
2
3
4
5
private static void sleep(InterfaceAnimal animal) {
        System.out.println("---sleep Start");
        animal.sleep();
        System.out.println("sleep End---");
    }
- Dog,- Cat,- Cow등 모두- Animal인터페이스를 구현
- 하나의 sleep()메서드로 다양한 객체 처리 가능
- 새로운 동물이 추가돼도 기존 코드는 변경 없이 재사용 가능
인터페이스의 장점
- 제약을 줄 수 있다 → 반드시 구현해야 하는 규약 제공
- 다형성을 극대화할 수 있다 → 하나의 타입으로 모든 구현체를 처리
- 다중 구현 가능 → 유연한 설계 가능
좋은 프로그램은 제약이 있는 프로그램 이다.
인터페이스는 자바에서 가장 순수한 형태의 제약(규약)을 제공한다.
이 규약을 기반으로 다형성과 유연함을 갖춘 확장 가능한 프로그램을 만들 수 있다.
인터페이스 다중 구현
자바는 다중 상속을 지원하지 않음!
- 클래스의 다중 상속은 다이아몬드 문제 발생 가능성
- 예: 부모 A, B 둘 다 move()를 가지고 있고, 자식 C가 상속받으면 어떤move()를 호출할지 모호함
- 이런 구조는 복잡하고 유지보수도 어려워짐
그래서 자바는 클래스의 다중 상속은 금지하고
대신 인터페이스의 다중 구현만 허용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface Wifi {
    void scanWifi();
    void sendData();
}
public interface Bluetooth {
    void scanBluetooth();
    void sendData();  //wifi에도 같은 이름의 메서드 있
}
public class Phone implements Wifi, Bluetooth {
    @Override
    public void scanWifi() {
        System.out.println("Phone.scanWifi");
    }
    @Override
    public void scanBluetooth() {
        System.out.println("Phone.scanBluetooth");
    }
    @Override
    public void sendData() {
        System.out.println("Phone.sendData");
    }
}
메인 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PhoneMain {
    public static void main(String[] args) {
        Wifi wifi = new Phone();
        wifi.scanWifi();
        wifi.sendData();
        Bluetooth bluetooth = new Phone();
        bluetooth.scanBluetooth();
        bluetooth.sendData();
    }
}
//출력결과
Phone scanWifi
Phone sendData
Phone scanBluetooth
Phone sendData
- sendData()는- Wifi,- Bluetooth양쪽에 있지만
- Phone에서 하나만 구현하면 끝
- 실제로 호출되는 건 전부 Phone.sendData()
어떤 타입으로 참조하든, 결국 자식 클래스의 오버라이딩 메서드가 실행됨
이 구조가 인터페이스 다중 구현의 핵심이며, 자바가 클래스 다중 상속은 막고 인터페이스만 허용하는 이유이다. 이런 구조가 유연한 확장성과 유지보수의 핵심!
클래스와 인터페이스 활용
자동자 제어 프로그램을 만들어보자
- 클래스 상속과 인터페이스 구현을 동시에 사용하는 구조
- 공통 기능은 추상 클래스, 개별 기능은 인터페이스로 분리
- 다양한 자동차의 기능을 유연하게 표현하고 확장 가능하게 설계
추상 클래스
1
2
3
4
5
6
7
8
9
public abstract class Car {
    //abstract 추상 클래스
    public abstract void startEngine();
    public abstract void stopEngine();
    public void hook() {
        System.out.println("Car hook");
    }
}
- 공통 기능 제공
- 객체 생성을 제한하고 상속을 통해 구현 유도
인터페이스
1
2
3
4
5
6
7
public interface Electric {
    void charge();
}
public interface AutoDrive {
    void autoDrive();
}
- 특화된 기능은 인터페이스로 분리
- 자동차는 다 전기차는 아니고, 다 자율주행 가능한 것도 아니니까!
클래스 - 아반떼
1
2
3
4
5
6
7
8
9
10
11
public class Avante extends Car {
    @Override
    public void startEngine() {
        System.out.println("Avante Engine Start");
    }
    @Override
    public void stopEngine() {
        System.out.println("Avante Engine Stop");
    }
}
- 추상 클래스만 상속
- 전기차도, 자율주행차도 아님
클래스 - 테슬라
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Tesla extends Car implements Electric, AutoDrive {
    @Override
    public void startEngine() {
        System.out.println("Tesla Engine Start");
    }
    @Override
    public void stopEngine() {
        System.out.println("Tesla Engine Stop");
    }
    @Override
    public void charge() {
        System.out.println("Tesla 충전중...");
    }
    @Override
    public void autoDrive() {
        System.out.println("Tesla Auto Drive Start");
    }
}
- Car상속 +- Electric,- AutoDrive인터페이스 다중 구현
- 필요 기능만 오버라이딩
메인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CarMain {
    public static void main(String[] args) {
        Car avante = new Avante();
        drive(avante);
        Car tesla = new Tesla();
        drive(tesla);
        Electric eCar = new Tesla();
        eCar.charge();
        AutoDrive autoCar = new Tesla();
        autoCar.autoDrive();
    }
    private static void drive(Car car) {
        car.startEngine();
        car.hook();
        car.stopEngine();
    }
}
//출력
Avante Engine Start
Car hook
Avante Engine Stop
Tesla Engine Start
Car hook
Tesla Engine Stop
Tesla 충전중...
Tesla Auto Drive Start
- Car추상 클래스는 공통 구조 제공 (템플릿 역할)
- Electric,- AutoDrive인터페이스는 기능 단위 분리 (역할 기반 설계)
- Tesla는 상속 + 다중 구현으로 복합 기능 수행
- Avante는 단일 기능 차량 (자율주행/전기 기능 없음)
이처럼 클래스 상속과 인터페이스 구현을 함께 사용하면, 객체지향 설계를 더 유연하고 확장 가능하게 만들 수 있다.
실생활의 복합 기능 구조를 코드에 자연스럽게 표현할 수 있어 실무에서도 자주 사용되는 패턴이다.
 출처: 김영한의 실전 자바 - 기본편
