[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
는 단일 기능 차량 (자율주행/전기 기능 없음)
이처럼 클래스 상속과 인터페이스 구현을 함께 사용하면, 객체지향 설계를 더 유연하고 확장 가능하게 만들 수 있다.
실생활의 복합 기능 구조를 코드에 자연스럽게 표현할 수 있어 실무에서도 자주 사용되는 패턴이다.
출처: 김영한의 실전 자바 - 기본편