자바 객체 지향 프로그래밍 - 다형성, 추상화

계리·2022년 12월 2일
0
post-thumbnail
post-custom-banner

다형성(polymorphism)이란?

일단 다형성은 상속과 깊은 관계가 있으므로 상속에 대해 충분히 알고 있어야 한다.
객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하고 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
구체적으로 말하면 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다


class Tv{
	boolean power;		// 전원상태(on/off)
    int channel;		// 채널
    
    void power() 		{ power = !power; }
    void channerUp() 	{ ++channel;	  }
    void channelDown()	{ --channel;	  }
}

class CaptionTv extends Tv{
	String text;	// 캡션을 보여주기 위한 문자열
    void caption() { /* 내용 생략 */}
}

클래스 Tv와 CaptionTv는 상속관계이다. 아래 코드는 두 클래스의 인스턴스를 생성할 경우이다.

	Tv t = new Tv();
    CaptionTv c = new CaptionTv();

위 코드 처럼 보통은 참조변수의 타입과 일치하는 타입의 변수만을 사용했다. 하지만 Tv 클래스와 CaptionTv 클래스는 서로 상속관계에 있으면 조상 클래스의 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.

	Tv t = new CaptionTv();		// 조상 타입의 참조변수로 자손 인스턴스를 참조

그렇다면 인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조 하는 것의 차이점을 확인해보자.

	CaptionTv c = new CaptionTv();
    Tv t = new CaptionTv();

참조변수 t의 경우에는 조상타입의 참조변수로 참조하였기 때문에 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다.
T타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 상속 받은 멤버들만 사용할 수 있다. 즉 t.text, t.caption()은 사용할 수가 없다. 둘다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 불가능하다.

class Tv {
    boolean power;		// 전원상태(on/off)
    int channel;		// 채널

    void power() 		{ power = !power; }
    void channelUp() 	{ ++channel;	  }
    void channelDown()	{ --channel;	  }
}

class CaptionTv extends Tv{
    String text;	// 캡션을 보여주기 위한 문자열
    void caption() { /* 내용 생략 */}
}

public class TvTest {
    public static void main(String[] args){
        CaptionTv c = new CaptionTv();
        Tv t = new CaptionTv();

        boolean c_on_off = c.power;
        int c_channel = c.channel;
        c.power();
        c.channelUp();
        c.channelDown();
        String text = c.text;
        c.caption();

        boolean t_on_off = t.power;
        int t_channel = t.channel;
        t.power();
        t.channelUp();
        t.channelDown();

//        CaptionTv c = new Tv();	//  에러 발생
									// java: variable c is already defined in method main(java.lang.String[])
    }
}

CaptionTv c = new Tv();를 실행시키면 에러가 발생한다. 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다.
CaptionTv클래스에는 text와 caption()이 정의되어 있어서 c.text, t.caption()을 사용할 수 있지만 c가 참조하고 있는 인스턴스는 Tv타입이고 Tv타입에는 text와 caption이 존재하지 않기 때문에 이들을 사용하려면 문제가 발생한다.
참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

 조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.



참조변수의 형변환

참조변수도 기본형 변수처럼 형변환이 가능하다. 단 서로 상속관계에 있는 클래스 사이에서만 가능하다. 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로의 형변환만 가능하다.
기본형 변수에서 작은 자료형에서 큰 자료형으로 형변환 생략이 가능하듯이 자손타입의 참조변수에서 조상타입의 참조변수로 형변환 경우에도 형변환 생략이 가능하다.


 자손타입 --> 조상타입(Up-casting)   : 형 변환 생략가능
 자손타입 <-- 조상타입(Down-casting) : 형 변환 생략불가

public class CastingTest1 {
    public static void main(String[] args){
        Car car = null;
        FireEngine fe = new FireEngine();
        FireEngine fe2 = null;
        
        

        fe.water();
        car = fe;               // car = (Car)fe; 에서 형변환이 생략된 형태다.
//        car.water();          // 컴파일 에러. Car타입의 참조변수로는 water()를 호출할 수 없다.
        fe2 = (FireEngine) car; // 자손타입 <-- 조상타입
        fe2.water();
        
        FireEngine f;
        Ambulance a;
        
//        a = (Ambulance) f;    // 에러. 상속관계가 아닌 클래스간의 형변환 불가
//        f = (FireEngine) a;   // 에러. 상속관계가 아닌 클래스간의 형변환 불가
    }
}

class Car{
    String color;
    int door;

    void drive(){               // 운전하는 기능
        System.out.println("drive, Brrr~");
    }

    void stop(){                // 멈추는 기능
        System.out.println("stop!!!");
    }
}

class FireEngine extends Car{   // 소방차
    void water(){               // 물을 뿌리는 기능
        System.out.println("water!!!");
    }
}

class Ambulance extends Car{    //엠뷸런스
    void siren(){               // 사이렌을 울리는 기능
        System.out.println("siren~~~");
    }
}

Car클래스는 FireEngine클래스와 Ambulance클래스의 조상이다. 하지만 FireEngine클래스와 Ambulance클래스는 형제관계가 아니다. 자바에서는 조상과 자식관계만 존재하기 때문에 서로 아무 관계가 없다.

따라서 Car타입의 참조변수와 FireEngine타입의 참조변수, Car타입의 참조변수와 Ambulance타입의 참조변수 간에는 서로 형변환이 가능하지만 FireEngine타입의 참조변수와 Ambulance타입의 참조변수 간에는 서로 형변환이 불가능하다.


class CastringTest2 {
	public static void main(String[] args){
    	Car car = new Car();
        Car car2 = null;
        FireEngine fe = null;
        
        car.drive();
        fe = (FireEngine) car;		// 컴파일은 ok. 실행 시 에러
        fe.drive();
        car2 = fe;
        car2.drive();
    }
}

위의 예제에서 컴파일은 성공하지만 실행 시 에러(ClassCastException)가 발생한다.
fe = (FireEngine) car; 에서 조상타입의 참조변수가 자손타입의 참조변수로 형변환한것이기 때문에 문제가 없어 보이지만 car변수가 참조하고 있는 인스턴스가 Car타입 인스턴이기 때문이다 앞선 예시에서 CaptionTv c = new Tv(); 오류 인 것을 생각해보면 된다. 에러가 발생하지 않으면 Car car = new Car(); -> Car car = new FireEngine(); 로 변경하면 실행 시 에러도 발생하지 않을 것이다.

서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.


매개변수의 다형성

참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다.


class Product{
	int price;			// 제품의 가격
    int bonusPoint;		// 제품구매 시 제공하는 보너스점수
}

class Tv extends Product {}
class Computer extends Product {}
class Audio extends Product {}

class Buyer{				// 고객, 물건을 사는 사람
	int money = 1000;		// 소유금액
    int bonusPoint = 0;		// 보너스점수
}

Product클래스는 Tv, Audio, Computer클래스의 조상클래스이며, Buyer클래스는 제품(Product)을 구입하는 사람을 클래스로 표현한 것이다.
Buyer클래스에 물건을 구입하는 기능을 메서드를 추가해보았다. 구입할 대상이 필요하므로 매개변수로 구입할 제품을 넘겨받아야 한다. Tv를 살 수 있도록 매개변수를 Tv타입으로 하였다.


class Buyer{				// 고객, 물건을 사는 사람
	int money = 1000;		// 소유금액
    int bonusPoint = 0;		// 보너스점수
    
    void buy(Tv t){
    	// Buyer가 가진 돈(money)에서 제품의 가격(t.price)만큼 뺀다.
        money = money - t.price;
        
        // Buyer의 보너스점수(bounusPoint)에 제품의 보너스점수(t.bonusPoint)를 더한다.
        bonusPoint = bonusPoint + t.bonusPoint;
    }
}

buy(Tv t)는 제품을 구입하면 제품을 구입한 사람이 가진 돈에서 제품의 가격을 빼고, 보너스점수는 추가하는 작업을 하도록 하였다. 그런데 buy(Tv t)로는 Tv밖에 살 수 없기 때문에 아래와 같이 다른 제품들도 구입할 수 있는 메서드를 추가로 필요하였다.


class Buyer{				// 고객, 물건을 사는 사람
	int money = 1000;		// 소유금액
    int bonusPoint = 0;		// 보너스점수
    
    void buy(Tv t){
    	// Buyer가 가진 돈(money)에서 제품의 가격(t.price)만큼 뺀다.
        money = money - t.price;
        
        // Buyer의 보너스점수(bounusPoint)에 제품의 보너스점수(t.bonusPoint)를 더한다.
        bonusPoint = bonusPoint + t.bonusPoint;
    }
    
    void buy(Computer c){
        money = money - c.price;
        bonusPoint = bonusPoint + c.bonusPoint;
    }
    
    void buy(Audio a){
    	money = money - a.price;
        bonusPoint = bonusPoint + a.bonusPoint;
    }
}

이렇게 되면 제품의 종류가 늘어날 때마다 Buyer클래스에는 새로운 buy메서드를 추가해주어야 할 것이다.
하지만 메서드의 매개변수에 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있다.


    void buy(Product p){
    	money = money - a.price;
        bonusPoint = bonusPoint + a.bonusPoint;
    }

매개변수가 Product타입의 참조변수이면 메서드의 매개변수로 Product클래스의 자손타입의 참조변수들도 매개변수로 받아들일 수 있다.
그리고 Product클래스의 price와 bonusPoint가 선언되어 있기 때문에 참조변수 p로 인스턴스의 price, bonusPoint를 사용할 수 있다.

매개변수의 다형성을 이용할 경우로 사용될 수 있는 코드이다.


class Product{
    int price;          // 제품의 가격
    int bonusPoint;     // 제품구매 시 제공하는 보너스점수

    Product(int price){
        this.price = price;
        bonusPoint = (int)(price/10.0);     // 보너스점수는 제품가격의 10%
    }
}

class Tv extends Product{
    Tv(){
        // 조상클래스의 생성자 Product(int price)를 호출한다.
        super((100));   // Tv의 가격을 100만원으로 한다.
    }
    // Object클래스의 toString()을 오버라이딩 한다.
    public String toString(){ return "Tv"; }
}

class Computer extends Product {
    Computer() {
        super((200));
    }

    public String toString() {
        return "Computer";
    }
}

class Buyer{                // 고객, 물건을 사는 사람
    int money = 1000;       // 소유금액
    int bonusPoint = 0;     // 보너스점수

    void buy(Product p) {
        if(money < p.price){
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }

        money -= p.price;           // 가진 돈에서 구입한 제품의 가격을 뺀다.
        bonusPoint += p.bonusPoint;   // 제품의 보너스 점수를 추가한다.
        System.out.println(p + "을/를 구입하셨습니다.");
    }
}

public class PolyArgumentTest {
    public static void main(String args[]){
        Buyer b = new Buyer();

        b.buy(new Tv());
        b.buy(new Computer());

        System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
        System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다.");

    }
}

추상클래스란?

추상클래스는 미완성 설계도라고 할 수 있다. 말 그대로 완성되지 못한 채로 남겨진 설계도를 말한다.
미완성 설계도로 완성된 제품을 만들 수 없듯이 추상클래스로 인스턴스는 생성할 수 없다. 추상클래스는 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.

새로운 클래스를 작성하는데 있어서 아무 것도 없는 상태에서 시작하는 것보다는 완전하지는 못하더라도 어느 정도 틀을 갖춘 상태에서 시작하는 것이 나을 것이다.

예를 들어 같은 크기의 TV라도 기능의 차이에 따라 여러 종류의 모델이 있지만 사실 이 들의 설계도는 아마 90%정도는 동일할 것이다. 서로 다른 세 개의 설계도를 따로 그리는 것보다는 이들의 공통부분만을 그린 미완성 설계도를 만들어 놓고 이 미완성 설계도를 이용해서 각각의 설계도를 완성하는 것이다 훨씬 효율적일 것이다.

추상클래스 선언할 때 키워드이다.

abstract class 클래스이름{
	...
}

추상메서드

메서드는 선언부와 구현부(몸통)로 구성되어 있다. 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔 것이 추상메서드이다. 즉 설계만 해 놓고 실제 수행할 내용은 작성하지 않았기 때문에 미완성 메서드인 것이다.

미완성 상태로 남겨 놓는 이윻는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만 작성하고 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려 주고 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 것이다.


추상클래스의 작성

여러 클래스에 공통적으로 사용될 수 있는 클래스를 바로 작성하기도 하고 기존의 클래스의 공통적인 부분을 뽑아서 추상클래스로 만들어 상속하도록 하는 경우도 있다.

추상의 사전적 의미로는
낱낱의 구체적 표상이나 개념에서 공통된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용

추상화는 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이라고 할 수 있다.

상속계층도를 따라 내려갈수록 클래스는 점점 기능이 추가되어 구체화의 정도가 심해지고 반대로 올라갈수록 추상화의 정도가 심해진다고 할 수 있다. 쉽게 얘기하면 상속계측도를 따라 내려 갈수록 세분화, 올라갈수록 공통요소만 남게 된다.

추상화 - 클래스간의 공통점을 찾아내서 공통의 조상을 만드는 작업
구체화 - 상속을 통해 클래스를 구현, 확장하는 작업

스타크래프트에 나오는 유닛들로 클래스를 간단히 정의해보았다.


abstract class Unit{
    int x, y;
    abstract void move(int x, int y);
    void stop() { /* 현재 위치에 정지 */ }
}

class Marine extends Unit{	 	// 보병
	void move(int x, int y)		{ /* 지정된 위치로 이동 */ }
    void stimPack()				{ /* 스팀팩을 사용한다. */ }
}

class Tank extends Unit{		// 탱크
	void move(int x, int y)		{ /* 지정된 위치로 이동 */ }
    void stimPack()				{ /* 공격모드로 변환한다. */ }
}

class Dropship extends Unit{	// 수송선
	void move(int x, int y)		{ /* 지정된 위치로 이동 */ }
    void load()					{ /* 선택된 대상을 태운다 */ }
    void unload()				{ /* 선택된 대상을 내린다 */ }
}

이처럼 공통부분을 뽑아내서 Unit클래스를 정의하고 이로부터 상속을 받아 각 유닛(Marin, Tank, Dropship)에 맞게 메서드들을 선언하여 사용하고 있는 것을 볼 수 있다.

Dropship의 경우 공중유닛이기 때문에 이동하는 방법이 달라서 실제구현은 다를 것이다. 그래도 move메서드 선언부는 같기 때문에 Dropship클래스에 맞게 move메서드를 정의하면 된다.


Unit[] grop = new Unit[4];
group[0] = new Marine();
group[1] = new Tank();
group[2] = new Marine();
group[3] = new Dropship();

for(int i=0; i<grop.length; i++)
	group[i].move(100, 200);		// Unit배열의 모든 유닛을 좌표(100, 200)의 위치로 이동한다.

위 코드는 공통조상인 Unit클래스 타입의 참조변수 배열을 통해서 서로 다른 종류의 인스턴스를 하나의 묶음으로 다룰 수 있다는 것을 보여 주기 위한 것이다.


※ 참고 문헌

남궁성, 『Java의 정석 3nd Edition』, 도우출판(2016) 책으로 공부하고 정리한 내용 입니다.

profile
gyery
post-custom-banner

0개의 댓글