다형성( Polymorphism)
- 객체지향 프로그래밍의 대표 특성인 캡슐화, 상속, 다형성 중 하나.
- 한 객체가 여러 타입의 객체로 취급될 수 있는 능력
다형성의 핵심 이론
- 다형적 참조
- 메서드 오버라이딩
다형적 참조
부모와 자식이 있고, 각각 다른 메서드를 가지는 상속 관계
Parent class
package poly.basic;
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
Child class
package poly.basic;
public class Child extends Parent{
public void childMethod() {
System.out.println("Child.childMethod");
}
}
PolyMain class
package poly.basic;
public class PolyMain {
public static void main(String[] args) {
// 부모 변수가 부모 인스턴스 참조
System.out.println("Parent -> Parent");
Parent parent = new Parent();
parent.parentMethod();
// 자식 변수가 자식 인스턴스 참조
System.out.println("Child -> Child");
Child child = new Child();
child.parentMethod();
child.childMethod();
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
System.out.println("Parent -> Child");
Parent poly = new Child();
poly.parentMethod();
//자식은 부모를 담을 수 없다
// Child child1 = new Parent();
//자식의 기능은 호출할 수 없다.
// poly.childMethod();
}
}
부모 타입의 변수가 부모 인스턴스 참조
- 부모 타입의 변수가 부모 인스턴스 참조
- Parent 인스턴스 생성(메모리 상에 Parent만 생성 자식은 생성 안 됨)
- 생성된 참조값을 Parent 타입 변수인 parent에 저장
- parent.parentMethod( ) 호출 시 인스턴스 Parent 클래스의 parentMethod( )가 호출 됨
자식 타입의 변수가 자식 인스턴스 참조
- 자식 타입의 변수가 자식 인스턴스를 참조
- Child 인스턴스 생성(메모리 상에 Parent, Child 둘 다 생성)
- 생성된 참조값을 Child 타입 변수인 child에 저장
- child.childMethod( ) 호출 시 인스턴스 Child 클래스의 childMethod( )가 호출 됨
다형적 참조 : 부모 타입의 변수가 자식 인스턴스 참조
- 부모 타입의 변수가 자식 인스턴스를 참조
- Child 인스턴스를 생성(메모리 상에 Child, Parent 모두 생성)
- 생성된 참조값을 Parent 타입의 변수인 poly에 담아둔다.
부모는 자식을 담을 수 있다.
- 부모 타입은 자식 타입을 담을 수 있다.
- 자식 타입은 부모 타입을 담을 수 없다.
- Child child1 = new Parent( ) : 컴파일 오류 발생
다형적 참조
부모 타입의 변수는 자식 타입을 참조할 수 있다, 자식 타입 아래에 상속된 손자 타입이 있을 경우 참조 가능, 그 하위 타입도 참조 가능(부모 타입은 자신을 기준으로 모든 자식 타입을 참조할 수 있음)
다형적 참조와 인스턴스 실행
poly.parentMethod( ) 호출 시 참조값을 사용해 인스턴스 찾음 → 인스턴스 안에서 실행할 타입 찾기(Parent 타입) → Parent 클래스부터 시작해 필요 기능 찾기 → 해당 메서드가 있을 경우 호출 됨
다형적 참조의 한계
상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수 없기 때문에 다형적 참조를 통해 자식 타입을 참조하더라도 자식 타입의 메서드를 호출할 수 없다(컴파일 오류 발생).
다형적 참조의 핵심 - 부모는 자식을 품을 수 있다.
다형성과 캐스팅
package poly.basic;
public class CastingMain1 {
public static void main(String[] args) {
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
// 단 자식의 기능은 호출할 수 없음.
//poly.childMethod();
// 다운 캐스팅(부모 타입 -> 자식 타입)
Child child = (Child) poly; // x001
child.childMethod();
}
}
다운캐스팅
호출 타입을 Child 타입으로 변경하면 인스턴스의 Child에 있는 childMethod( )를 호출할 수 있으나 문제가 발생한다.
부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없다.
- 부모 타입을 사용하는 변수를 자식 타입에 대입하려고 하면 컴파일 오류 발생
- 다운캐스팅 기능을 사용해서 부모 타입을 잠깐 자식 타입으로 변경해야 된다
- (타입) 처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경 가능(캐스팅)
- 캐스팅해도 poly의 타입은 Parent로 기존과 같이 유지 됨
캐스팅
- 업캐스팅(upcasting) : 부모 타입으로 변경
- 다운캐스팅(downcasting) : 자식 타입으로 변경
캐스팅의 종류
일시적 다운 캐스팅(다운 캐스팅 결과를 변수에 담아두는 과정 생략)
package poly.basic;
public class CastingMain2 {
public static void main(String[] args) {
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
// 단 자식의 기능은 호출할 수 없음.
//poly.childMethod();
// 일시적 다운 캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();
}
}
((Child) poly).childMethod();
해당 코드 실행 시 Parent 타입을 임시로 Child로 변경, 메서드를 호출할 때 Child 타입에서 찾아서 실행(poly의 타입은 유지).
일시적 다운캐스팅 사용 시 별도의 변수 없이 인스턴스의 자식 타입의 기능을 사용할 수 있음.
업캐스팅
다운캐스팅과 반대로 현재 타입을 부모 타입으로 바꾸는 것
package poly.basic;
public class CastingMain3 {
public static void main(String[] args) {
// up casting vs down casting
Child child = new Child();
Parent parent1 = (Parent) child; // 업캐스팅은 생략 가능(생략 권장)
Parent parent2 = child; // 업캐스팅 생략
parent1.parentMethod();
parent2.parentMethod();
}
}
업캐스팅은 생략할 수 있음. 다운캐스팅은 생략할 수 없다(업캐스팅은 매우 자주 사용하기 때문에 생략을 권장).
다운캐스팅과 주의점
다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다.
package poly.basic;
public class CastingMain4 {
// 다운캐스팅 자동으로 하지 않는 이유
public static void main(String[] args) {
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // 문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
child2.childMethod(); // 실행 불가
}
}
다운캐스팅이 가능한 경우
다운캐스팅이 불가능한 경우
parent2를 Child 타입으로 다운캐스팅할 경우 parent2는 Parent로 생성 되었기 때문에 메모리 상에는 Child가 존재하지 않아서 Child를 사용할 수 없다.
- 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException이라는 예외가 발생하고 프로그램이 종료 됨.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅은 객체를 생성하는 경우 해당 타입의 상위 부모 타입은 모두 함께 생성되서 위로만 타입을 변경하는 경우 메모리 상에 인스턴스가 모두 존재해서 항상 안전함.
다운캐스팅은 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있음(객체를 생성 시 부모 타입은 모두 함께 생성되자만 자식 타입은 생성되지 않음), 런타임 오류 발생 가능 - 명시적 캐스팅 필요
instanceof
다형성에서 참조형 변수는 다양한 자식을 대상으로 참조할 수 있기 때문에 참조하는 대상이 어떤 인스턴스를 참조하는지 확인하기 위해 사용
package poly.basic;
public class CastingMain5 {
public static void main(String[] args) {
Parent parent1 = new Parent();
System.out.println("parent1 호출");
call(parent1);
Parent parent2 = new Child();
System.out.println("parent2 호출");
call(parent2);
}
private static void call(Parent parent) {
parent.parentMethod();
if (parent instanceof Child) {
System.out.println("Child 인스턴스 맞음");
Child child = (Child) parent;
child.childMethod();
}
}
}
- instanceof 키워드는 오른쪽 대상의 자식 타입을 왼쪽에서 참조하는 경우에도 true를 반환
- 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 됨(되면 true, 안 되면 false)
자바 16 - Pattern Matching for instanceof
instanceof를 사용하면서 동시에 변수를 선언할 수 있는 기능
package poly.basic;
public class CastingMain6 {
public static void main(String[] args) {
Parent parent1 = new Parent();
System.out.println("parent1 호출");
call(parent1);
Parent parent2 = new Child();
System.out.println("parent2 호출");
call(parent2);
}
private static void call(Parent parent) {
parent.parentMethod();
// Child 인스턴스인 경우 childMethod() 실행
if (parent instanceof Child child) {
System.out.println("Child 인스턴스 맞음");
child.childMethod();
}
}
}
- 인스턴스가 맞는 경우 직접 다운캐스팅하는 코드를 생략할 수 있음
다형성과 메서드 오버라이딩
다형성을 이루는 중요한 핵심 이론 중 하나는 메서드 오버라이딩.
메서드 오버라이딩에서 오버라이딩 된 메서드가 항상 우선권을 가짐
- Parent, Child 모두 value 멤버 변수를 가짐
- 멤버 변수는 오버라이딩 안 됨
- Parent, Child 모두 method() 라는 메서드를 가짐. Child에서 메서드 오버라이딩
- 메서드는 오버라이딩 됨
package poly.overriding;
public class OverridingMain {
public static void main(String[] args) {
//자식 변수가 자식 인스턴스 참조
Child child = new Child();
System.out.println("Child -> Child");
System.out.println("value = " + child.value);
child.method();
//부모 변수가 부모 인스턴스 참조
Parent parent = new Parent();
System.out.println("Parent -> Parent");
System.out.println("value = " + parent.value);
parent.method();
//부모 변수가 자식 인스턴스 참조
Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("value = " + poly.value);
poly.method();
}
}
- child 변수는 Child 타입. child.value, child.method() 호출 시 인스턴스의 Child 타입에서 기능을 찾아서 실행
parent → parent
- parent 변수는 Parent 타입 parent.value, parent.method() 호출 시 인스턴스의 Parent 타입에서 기능을 찾아서 실행
- poly 변수는 Parent 타입, poly.value, poly.method( ) 호출 시 인스턴스의 Parent 타입에서 기능을 찾아서 실행
- poly.method( ) : Parent의 하위 타입인 Child.method( )가 오버라이딩 되어 있음(오버라이딩 메서드는 항상 우선권), 따라서 Child.method( )가 실행
자식 타입에서도 하위 타입에서 오버라이딩 하는 경우 하위 타입의 오버라이딩 메서드가 우선권을 가진다.
- 다형적 참고: 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
- 메서드 오버라이딩: 기존 기능을 하위 타입에서 새로운 기능으로 재정의
다형성 활용
예제) 개, 고양이, 소의 울음 소리를 테스트하는 프로그램 작성
다형성 미사용 코드
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 = new Cow();
System.out.println("동물 소리 테스트 시작");
cow.sound();
System.out.println("동물 소리 테스트 종료");
꼭 필요한 생성 부분을 제외한 출력 부분은 계속 중복이 증가하게 된다.
중복 제거를 위해서는 메서드나 배열, for문을 사용하면 되지만, 세 클래스는 서로 완전히 다른 클래스임.
문제점
- 메서드를 사용할 경우 세 클래스 중 한 개 클래스의 전용 메서드가 되고 다른 클래스는 인수로 사용할 수 없음
- 배열과 for문을 사용하려고 해도 배열의 타입을 세 클래스 중 한 개 클래스의 타입으로 지정해야 되기 때문에 역시 다른 클래스를 배열에 담을 수 없음.
- 결론적으로 새로운 클래스 추가 시마다 중복 코드 수도 증가하게 됨
문제의 핵심은 타입이 다르다는 점. 세 클래스가 모두 같은 타입을 사용할 수 있을 경우 메서드나 배열을 활용해 코드의 중복 제거 가능
위 예제를 다형성을 사용해서 변경
상속 관계를 사용해서 다형성을 활용 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 AnimalSoundMain {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
Cow cow = new Cow();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(cow);
}
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
코드 분석
- soundAnimal(dog) 호출 시 soundAnimal(Animal animal)에 Dog 인스턴스가 전달
- 메서드 안에서 animal.sound( ) 호출, Animal 클래스에서 sound( ) 메서드를 실행해야 하나, 하위 클래스에서 sound( ) 메서드 오버라이딩으로 인해 Dog 클래스의 sound( ) 메서드가 호출 됨.
- soundAnimal(dog)의 결과로 "멍멍"이 출력 됨.
다형성 활용 3
배열과 for문 사용 중복 제거
package poly.ex2;
public class AnimalPolyMain {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
Cow cow = new Cow();
Animal[] animalArr = {dog, cat, cow};
for (Animal animal : animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
배열은 같은 타입의 데이터를 나열할 수 있음.
Animal 타입의 배열을 만들고 다형적 참조를 사용(Dog, Cat, Cow 의 부모 타입인 Animal 타입으로 배열만들고 각각을 배열에 포함)
배열을 for문을 사용해서 반복(배열에 각각의 인스턴스가 있음, 메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound( ) 메서드가 호출 됨).
개선
package poly.ex2;
public class AnimalPolyMain {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Cow()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
// 변하지 않는 부분
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
- soundAnimal(...) 메서드는 구체적인 클래스가 아닌 Animal이라는 추상적인 부모를 참조하기 때문에 Animal을 상속받은 새로운 동물이 추가되어도 메서드의 코드 변경 없이 유지 가능
- 새로운 기능이 추가되었을 때 변하는 부분을 최소화하는 것이 잘 작성된 코드(변하는 부분과 변하지 않는 부분을 명확하게 구분)
문제
- Animal 클래스를 생성할 수 있음
- Animal 클래스를 상속받는 곳에서 sound( ) 메서드 오버라이딩을 하지 않을 가능성
추상 클래스와 추상 메서드를 사용하면 위의 문제 해결 가능
추상 클래스
추상 클래스 : 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스(추상적인 개념을 제공하는 클래스, 실체인 인스턴스가 존재하지 않음). 상속을 목적으로 사용되고, 부모 클래스의 역할을 담당
abstract class AbstractAnimal {...}
- 추상 클래스는 클래스를 선언할 때 추상이라는 의미의 abstract 키워드를 붙인다
- 추상 클래스는 기존 클래스와 완전히 같음(직접 인스턴스를 생성하지 못하는 제약이 추가됨)
추상 메서드
부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있음(추상 메서드). 추상적인 개념을 제공하는 메서드(실체가 존재하지 않고 메서드 바디가 없음).
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 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(cat);
soundAnimal(dog);
soundAnimal(cow);
}
// 변하지 않는 부분
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
제약(인스턴스 생성 불가, 추상 메서드는 반드시 오버라이딩 해야 됨)을 제외하면 나머지는 모두 일반적인 클래스와 동일(제약이 추가된 클래스).
정리
- 추상 클래스로 인해 Animal 인스턴스를 실수로 생성할 문제를 근본적으로 방지
- 추상 메서드로 인해 새로운 동물의 자식 클래스를 만들때 실수로 sound( )를 오버라이딩 하지 않을 문제를 근본적으로 방지
순수 추상 클래스
모든 메서드가 추상 메서드인 추상 클래스
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(cat);
soundAnimal(dog);
soundAnimal(cow);
moveAnimal(cat);
moveAnimal(dog);
moveAnimal(cow);
}
// 변하지 않는 부분
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
// 변하지 않는 부분
private static void moveAnimal(AbstractAnimal animal) {
System.out.println("동물 이동 테스트 시작");
animal.move();
System.out.println("동물 이동 테스트 종료");
}
}
순수 추상 클래스
모든 메서드가 추상 메서드인 순수 추상 클래스는 코드를 실행할 바디 부분이 전혀 없음. 다형성을 위한 부모 타입으로써 껍데기 역할만 제공
특징
- 인스턴스를 생성할 수 없음
- 상속 시 자식은 모든 메서드를 오버라이딩 해야 한다
- 주로 다형성을 위해 사용됨
상속하는 클래스는 모든 메서드를 구현해야 한다.
상속 받는 클래스의 입장에서 부모의 모든 메서드를 구현해야 하는 것. 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스 개념을 제공.
인터페이스
자바에서 순수 추상 클래스를 더 편리하게 사용할 수 있도록 제공하는 기능
인터페이스는 class가 아니라 interface 키워드를 사용
인터페이스 - public abstract 키워드 생략 가능
public interface InterfaceAnimal {
void sound();
void move();
}
인터페이스의 기능
- 인터페이스의 메서드는 모두 public, abstrcat 임
- 메서드에 publlic abstract를 생략할 수 있다(생략을 권장)
- 인터페이스는 다중 구현(다중 상속)을 지원
인터페이스와 멤버 변수
public interface InterfaceAnimal {
public static final int MY_PI = 3.14;
int MY_PI = 3.14; // 처럼 생략 가능
}
인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주
클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속) 관계는 UML에서 점선을 사용
package poly.ex5;
public interface InterfaceAnimal {
void sound(); //public abstract 생략
void 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("개 이동");
}
}
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();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(cow);
}
// 변하지 않는 부분
private static void soundAnimal(InterfaceAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
클래스, 추상 클래스, 인터페이스는 모두 같음
- 클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조 상 모두 같음.
- 인터페이스는 순수 추상 클래스와 비슷하다
상속 vs 구현
상속은 부모의 기능을 물려 받는 것이 목적, 인터페이스는 메서드 이름만 있는 설계도이고, 실제 작동은 하위 클래스에서 모두 구현해야 하기 때문에 인터페이스는 구현이라고 함(일반 상속 구조와 동일하게 작동).
인터페이스 사용 이유
- 제약: 인터페이스 구현 시 인터페이스의 메서드를 반드시 구현하라는 제약을 주기 때문(인터페이스는 모든 메서드가 추상 메서드이기 때문에 나중에 실행 가능한 메서드가 추가될 수 있는 문제를 차단 가능)
- 다중 구현: 클래스 상속은 부모를 하나만 지정할 수 있음, 인터페이스는 부모를 여러 명 두는 다중 구현이 가능함
인터페이스 - 다중 구현
인터페이스는 모두 추상 메서드이기 때문에 다중 구현을 허용
예제
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");
}
}
- 다중 구현 시 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();
}
}
메서드 호출 과정(methodCommon( ))
클래스와 인터페이스 활용
클래스 상속과 인터페이스 구현을 함께 사용하는 예제
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("새 날기");
}
}
하나의 클래스 여러 인터페이스 예시
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 사용 가능
private 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("날기 테스트 종료");
}
}
실행 과정
- soundAnimal(bird) 호출
- 메서드 안에서 animal.sound( ) 호출 시 참조 대상인 Bird 인스턴스를 찾는다
- animal 변수는 같은 타입인 AbstractAnimal.sound( )를 찾는데 오버라이딩 되어 있기 때문에 Bird.sound( )를 호출
- fly(bird) 역시 호출 시 메서드 안에서 fly 타입 변수인 Fly.fiy( )를 찾는데 오버라이딩 되어 있기 때문에 Bird.fly( )를 호출함
다형성 이해
역할과 구현을 분리
- 역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해짐
- 장점
- 클라이언트는 대상의 역할(인터페이스)만 알면 된다
- 클라이언트는 구현 대상의 내부 구조를 몰라도 된다
- 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다
- 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다
자바 언어
- 자바 언어의 다형성 활용
- 역할 = 인터페이스
- 구현 = 인터페이스를 구현한 클래스, 구현 객체
- 객체를 설계할 때 역할과 구현을 명확히 분리
- 객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기
객체의 협력이라는 관계부터 생각
- 혼자 있는 객체는 없음
- 클라이언트: 요청, 서버: 응답
- 수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가짐
자바 언어의 다형성
- 오버라이딩
- 오버라이딩 된 메서드가 실행
- 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있음
- 클래스 상속 관계도 다형성, 오버라이딩 적용 가능
다형성의 본질
- 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다
- 다형성의 본질을 이해하려면 협력이라는 객체 사이의 관계에서 시작해야 함
- 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
역할과 구현을 분리
한계
- 역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생
- 인터페이스를 안정적으로 잘 설계하는 것이 중요
정리
- 다형성이 가장 중요하다
- 디자인 패턴 대부분은 다형성을 활용하는 것
- 스프링의 핵심인 제어의 역전(loC), 의존관계 주입(DI)도 결국 다형성을 활용하는 것
- 다형성을 잘 활용하면 구현을 편리하게 변경할 수 있다
다형성 - 역할과 구현 예제
K3Car 클래스
package poly.car0;
public class K3Car {
public void startEngine() {
System.out.println("K3Car.startEngine");
}
public void offEngine() {
System.out.println("K3Car.offEngine");
}
public void pressAccelerator() {
System.out.println("K3Car.pressAccelerator");
}
}
Driver 클래스
package poly.car0;
public class Driver {
private K3Car k3Car;
public void setK3Car(K3Car k3Car) {
this.k3Car = k3Car;
}
public void drive() {
System.out.println("자동차를 운전합니다");
k3Car.startEngine();
k3Car.pressAccelerator();
k3Car.offEngine();
}
}
CarMain 클래스
package poly.car0;
public class CarMain0 {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setK3Car(k3Car);
driver.drive();
}
}
메모리 그림
예제 2
새로운 차량 클래스를 추가, 드라이버는 두 차량을 운전할 수 있어야 함(동시 운전 x)
Model3Car 클래스
package poly.car0;
public class Model3Car {
public void startEngine() {
System.out.println("Model3Car.startEngine");
}
public void offEngine() {
System.out.println("Model3Car.offEngine");
}
public void pressAccelerator() {
System.out.println("Model3Car.pressAccelerator");
}
}
Driver 클래스 수정
package poly.car0;
public class Driver {
private K3Car k3Car;
private Model3Car model3Car; // 추가
public void setK3Car(K3Car k3Car) {
this.k3Car = k3Car;
}
//추가
public void setModel3Car(Model3Car model3Car) {
this.model3Car = model3Car;
}
//수정
public void drive() {
System.out.println("자동차를 운전합니다");
if (k3Car != null) {
k3Car.startEngine();
k3Car.pressAccelerator();
k3Car.offEngine();
} else if (model3Car != null) {
model3Car.startEngine();
model3Car.pressAccelerator();
model3Car.offEngine();
}
}
}
Main 클래스 수정
package poly.car0;
public class CarMain0 {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setK3Car(k3Car);
driver.drive();
// 추가
Model3Car model3Car = new Model3Car();
driver.setK3Car(null);
driver.setModel3Car(model3Car);
driver.drive();
}
}
- 운전자가 차량을 변경해서 운전하는 코드
- driver.setK3Car(null)을 통해서 기존 K3Car의 참조를 제거
- driver.setModel3Car(model3Car)를 통해서 새로운 model3Car의 참조 추가
- driver.drive() 호출
메모리 그림
- 새로운 차량이 추가될 때마다 Driver 코드를 만이 변경해야한다는 문제가 있음
예제 3
다형성 활용해 역할과 구현을 분리해, 클라이언트 코드를 변경 없이 구현 객체를 변경할 수 있음
- Driver : Car의 역할에만 의존, 구현인 K3, Model3 에 의존하지 않음
- Driver는 Car의 멤버 변수를 가진다, Car 인터페이스를 참조
- 구현 객체에는 의존하지 않고 Car 인터페이스에만 의존
- Car : 자동차 역할이고 인터페이스, K3Car, Model3Car 클래스가 인터페이스를 구현
Car 인터페이스
package poly.car1;
public interface Car {
void startEngine();
void offEngine();
void pressAccelerator();
}
인터페이스 구현
package poly.car1;
public class K3Car implements Car {
@Override
public void startEngine() {
System.out.println("K3Car.startEngine");
}
@Override
public void offEngine() {
System.out.println("K3Car.offEngine");
}
@Override
public void pressAccelerator() {
System.out.println("K3Car.pressAccelerator");
}
}
package poly.car1;
public class Model3Car implements Car{
@Override
public void startEngine() {
System.out.println("Model3Car.startEngine");
}
@Override
public void offEngine() {
System.out.println("Model3Car.offEngine");
}
@Override
public void pressAccelerator() {
System.out.println("Model3Car.pressAccelerator");
}
}
Driver 클래스
package poly.car1;
public class Driver {
private Car car;
public void setCar(Car car) {
System.out.println("자동차를 설정합니다: " + car);
this.car = car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
car.startEngine();
car.pressAccelerator();
car.offEngine();
}
}
- Driver는 멤버 변수로 Car car를 가짐
- setCar(Car car): 멤버 변수에 자동차를 설정, 외부에서 이 메서드를 호출해야 Driver는 새로운 자동차를 참조하거나 변경 가능
- drive( ) : Car 인터페이스가 제공하는 기능들을 통해 자동차를 운전
Main 클래스
package poly.car1;
public class CarMain1 {
public static void main(String[] args) {
Driver driver = new Driver();
//차량 선택(k3)
K3Car k3Car = new K3Car();
driver.setCar(k3Car);
driver.drive();
//차량 변경(k3 -> Model3)
Model3Car model3Car = new Model3Car();
driver.setCar(model3Car);
driver.drive();
}
}
실행 과정
OCP(Open-Closed Principle) 원칙
좋은 객체 지향 설계 원칙 중 하나
- Open for extension: 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다
- Closed for modification: 기존 코드는 수정되지 않아야 한다
기존 코드의 수정 없이 새로운 기능을 추가할 수 있다는 의미.
확장에 열려있다는 의미
Car 인터페이스를 사용해서 새로운 차량을 자유롭게 추가할 수 있음(Car 인터페이스를 구현해 기능을 추가할 수 있음)
Car 인터페이스를 사용하는 클라이언트 코드인 Driver도 Car 인터페이스를 통해 새로 추가된 차량을 자유롭게 호출할 수 있다
코드 수정은 닫혀 있다는 의미
새로운 차를 추가하면 기능이 추가되기 때문에 기존 코드의 수정은 불가피함(코드 어딘가는 수정해야 함).
변하지 않는 부분
새로운 자동차를 추가할 때 가장 영향을 받는 중요한 클라이언트는 바로 Car의 기능을 사용하는 Driver, 핵심은 Car 인터페이스를 사용하는 클라이언트인 Driver 코드를 수정하지 않아도 됨
변하는 부분
main( )과 같이 새로운 차를 생성하고 Driver에게 필요한 차를 전달해주는 역할은 당연히 코드 수정이 발생, 전체 프로그램을 설정하고 조율하는 역할을 하는 부분은 OCP를 지켜도 변경이 필요함
정리
- Car를 사용하는 클라이언트 코드인 Driver 코드의 변경없이 새로운 자동차를 확장할 수 있다
- 다형성을 활용하고 역할과 구현을 잘 분리한 덕분에 새로운 자동차를 추가해도 대부분의 핵심 코드들을 그대로 유지할 수 있게 됨
문제 풀이
1. 다중 메시지 발송
한 번에 여러 곳에 메시지를 발송하는 프로그램 개발
요구사항
- 다형성을 활용
- Sender 인터페이스를 사용
- EmailSender, SmsSender, FaceBookSender를 구현
SendMain
package poly.ex.sender;
public class SendMain {
public static void main(String[] args) {
Sender[] senders = {new EmailSender(), new SmsSender(), new FaceBookSender()};
for (Sender sender : senders) {
sender.sendMessage("환영합니다!");
}
}
}
실행 결과
메일을 발송합니다: 환영합니다!
SMS를 발송합니다: 환영합니다!
페이스북에 발송합니다: 환영합니다!
Sender 인터페이스
package poly.ex.sender;
public interface Sender {
void sendMessage(String text);
}
구현 클래스
package poly.ex.sender;
public class EmailSender implements Sender{
@Override
public void sendMessage(String text) {
System.out.println("메일을 발송합니다: " + text);
}
}
package poly.ex.sender;
public class SmsSender implements Sender{
@Override
public void sendMessage(String text) {
System.out.println("SMS를 발송합니다: " + text);
}
}
package poly.ex.sender;
public class FaceBookSender implements Sender{
@Override
public void sendMessage(String text) {
System.out.println("페이스북에 발송합니다: " + text);
}
}
2. 결제 시스템 개발
결제 수단을 쉽게 추가할 수 있도록 기존 코드를 리펙토링
요구사항
- OCP 원칙을 지켜라
- 메서드를 포함한 모든 코드를 변경해도 됨, 클래스나 인터페이스 추가 가능
- 프로그램을 실행하는 PayMain0 코드는 변경하지 않고, 그대로 유지
- 리펙토링 후에도 실행결과는 기존과 같아야 함
PayMain0
package poly.ex.pay0;
public class PayMain0 {
public static void main(String[] args) {
PayService payService = new PayService();
//kakao 결제
String payOption1 = "kakao";
int amount1 = 5000;
payService.processPay(payOption1, amount1);
//naver 결제
String payOption2 = "naver";
int amount2 = 10000;
payService.processPay(payOption2, amount2);
//잘못된 결제 수단 선택
String payOption3 = "bad";
int amount3 = 15000;
payService.processPay(payOption3, amount3);
}
}
실행결과
결제를 시작합니다: option=kakao, amount=5000
카카오페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.
결제를 시작합니다: option=naver, amount=10000
네이버페이 시스템과 연결합니다.
10000원 결제를 시도합니다.
결제가 성공했습니다.
결제를 시작합니다: option=bad, amount=15000
결제 수단이 없습니다.
결제가 실패했습니다.
Payment 인터페이스
package poly.ex.pay0;
public interface Payment {
boolean payment(int amount);
}
구현 객체
package poly.ex.pay0;
public class DefaultPay implements Payment{
@Override
public boolean payment(int amount) {
System.out.println("결제 수단이 없습니다.");
return false;
}
}
package poly.ex.pay0;
public class NaverPay implements Payment{
@Override
public boolean payment(int amount) {
System.out.println("네이버페이 시스템과 연결합니다.");
System.out.println(amount + "원 결제를 시도합니다.");
return true;
}
}
package poly.ex.pay0;
import com.sun.security.jgss.GSSUtil;
public class KakaoPay implements Payment {
@Override
public boolean payment(int amount) {
System.out.println("카카오페이 시스템과 연결합니다.");
System.out.println(amount + "원 결제를 시도합니다.");
return true;
}
}
PayStore 클래스
package poly.ex.pay0;
public abstract class PayStore {
public static Payment matchPay(String option) {
if (option.equals("kakao")) {
return new KakaoPay();
} else if (option.equals("naver")) {
return new NaverPay();
} else {
return new DefaultPay();
}
}
}
클라이언트 서버
package poly.ex.pay0;
public class PayService {
public void processPay(String option, int amount) {
System.out.println("결제를 시작합니다: option=" + option + ", amount=" + amount);
Payment pay = PayStore.matchPay(option);
boolean result = pay.payment(amount);
if (result) {
System.out.println("결제가 성공했습니다.");
} else {
System.out.println("결제가 실패했습니다.");
}
}
}
3. 결제 시스템 개발 - 사용자 입력
기존 시스템에 사용자 입력을 추가
결제 수단을 입력하세요:kakao
결제 금액을 입력하세요:5000
결제를 시작합니다: option=kakao, amount=5000
카카오페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.
결제 수단을 입력하세요:exit
프로그램을 종료합니다.
PayMain0
package poly.ex.pay0;
import java.util.Scanner;
public class PayMain0 {
public static void main(String[] args) {
System.out.println("결제 시스템");
System.out.println("결제 수단에 exit를 입력하면 프로그램을 종료합니다.");
paySystem();
}
public static void paySystem() {
PayService payService = new PayService();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("결제 수단을 입력하세요:");
String option = sc.nextLine();
if (option.equals("exit")) {
System.out.println("프로그램을 종료합니다.");
break;
}
System.out.print("결제 금액을 입력하세요:");
int amount = sc.nextInt();
sc.nextLine();
payService.processPay(option, amount);
}
}
}
문제 풀이 간 어려웠던 점
- 2번 문제의 PayService에서 새로운 결제 수단이 추가될 때 코드의 수정을 최소화 시키기 위해 리펙토링하는 과정이 어려웠음
- 결제 수단을 인터페이스를 활용해 구현하는 것으로 결제 시스템 추가 시에 PayService 코드를 수정하는 것을 줄이는 것까지는 설계
- String option으로 들어온 결제 수단 선택에 따른 구현 객체를 불러오도록 설계하는 과정이 어려웠음.
- PayStore를 추상 클래스로 만들고 option 값으로 결제 수단 별 구현 객체를 불러오는 부분을 생각하지 못함
- 다형성에서 역할과 구현을 나누어서 프로그램을 설계하는 것에 대해 좀 더 연습이 필요함, 프로그램의 요구 조건에 대한 이해도를 높여야 함.
'항해 99 > Java' 카테고리의 다른 글
WIL - 1 (1) | 2024.02.12 |
---|---|
역할과 구현, OCP (0) | 2024.02.08 |
객체 지향, 상속, 다형성 (1) | 2024.02.07 |
연산자, 조건문, 반복문, 배열 (1) | 2024.02.06 |
Java 기초 개념 (0) | 2024.02.05 |