2023.01.30 - 안드로이드 앱개발자 과정

CHA·2023년 1월 30일
0

Java



다형성 (Polymorphism)

하나의 객체에 여러가지 타입을 대입할 수 있다는 의미


업캐스팅(UpCasting) , 다운캐스팅(DownCasting)


먼저, 캐스팅(Casting) 의 뜻은 무언가를 붙잡다 라는 뜻입니다. 즉, 원래 알던 캐스팅의 뜻을 알아보기 위해 First 클래스와 Second 클래스를 생성해봅시다.

First f = new First();
Second s = new Second();

각 클래스의 참조변수를 만들고 객체를 생성하여 주솟값을 전달합니다. 이러한 방식이 제대로 된 캐스팅 입니다. 단, First 타입의 참조변수에 다른 클래스의 객체를 할당하면 에러를 일으킵니다. 그렇다면 First 타입의 참조변수에는 항상 First 클래스의 객체만 할당할 수 있을까요? 그렇지는 않습니다.

자식아, 니 객체 좀 쓸게 : 업캐스팅

만일, Second 클래스가 First 클래스를 상속받았다고 해봅시다. 그러면 다음과 같은 코드도 가능합니다.

First f = new Second();

즉, 부모참조변수에 자식 객체를 할당할 수 있습니다. 이러한 상황을 업캐스팅 이라 합니다.
단, 참조는 가능하나 자식객체의 고유한 기능, 다시 말해 부모클래스에 있던 기능이 아닌 자식 클래스에만 있는 기능은 사용할 수 없습니다. 그렇다면 이러한 고유한 기능을 사용하고 싶다면 어떻게 해야할까요? 이런 경우에는 어쩔수 없이 자식객체를 참조하는 참조변수가 필요합니다.

부모님, 객체 좀 쓰게 해줘요 : 다운캐스팅

새로운 자식 참조변수 하나를 만듭시다. 그리고 Second 객체를 참조하던 First 참조변수 f의 참조값을 대입해줍시다.

First f = new Second();
Second s2 = f; // ERROR!

이러면 에러가 납니다. 실제로는 자식 객체의 주소를 담고있는 f 이지만, 여기서는 자식이 부모를 참조한다고 해석해버립니다. 그래서 여기에서는 f 가 참조를 하는것이 Second 의 객체라고 명시적으로 알려주면 대입이 가능해집니다.

Second s2 = (Second) f; // OK!

당연하게도, 업캐스팅이 되어있지 않은 상황에서의 다운캐스팅은 오류를 발생시킵니다.

First f2 = new First();
Second s3 = (Second) f2; // ERROR!

그렇다면 이러한 업캐스팅, 다운캐스팅을 하는 이유는 무엇일까요? 그 이유를 다형성의 의미가 무엇인지와 묶어서 한번 간단하게만 확인해봅시다.

다형성 . . ?

먼저, First 클래스와 First 클래스를 상속받는 Test 클래스, Second 클래스를 다음과 같이 설계합니다.

------------ First.java
public class First {

	void show() {
		System.out.println("First 클래스의 show Method.");
	}
}

------------ Second.java
public class Second extends First{

	@Override
	void show() {
		// TODO Auto-generated method stub
		System.out.println("Second 클래스의 show Method.");
	}
}
------------ Test.java
public class Test extends First{
	@Override
	void show() {
		// TODO Auto-generated method stub
		System.out.println("Test show 입니다.");
	}
}

부모 클래스인 First 에서 show() 메서드를 하나 정의하였으며 자식 클래스들에서 오버라이드한 코드입니다. 그러면 여기에서 다음과 같이 부모의 참조변수 1개로 자식객체들을 모두 제어할 수 있게됩니다.

----------------Main.java
public class Main {
	public static void main(String[] args) {
    	First obj;
        obj = new First();
        obj.show();
        
        obj = new Second();
        obj.show();
        
        obj = new Test();
        obj.show();
    }
}

/* 출력결과 
First 클래스의 show Method.
Second 클래스의 show Method.
Test show 입니다.

이렇게 obj.show() 처럼 쓰여진 코드는 똑같은 코드지만, 결과는 모두 달랐습니다. 이러한 특성을 보고 우리는 다형성 이라고 합니다. 그렇다면 이러한 다형성은 어떨 때 사용될까요? 좀 더 상세한 예시를 위해 Dog 클래스, Cat 클래스, Pig 클래스를 생성해봅시다.


추상적 개념의 클래스


묶자. 묶어버리자. 근데 잘 묶자.

아래와 같이 클래스를 설계해봅시다,

------------- Dog.java
public class Dog {
	void say() {
    	System.out.println("멍멍!");
    }
}
------------- Cat.java
public class Cat {
	void say() {
    	System.out.println("야옹~");
    }
}
------------- Pig.java
public class Pig {
	void say() {
    	System.out.println("꿀꿀...");
    }
}

이렇게 설계하고 Main 클래스에서 각각의 객체를 생성해 각 클래스가 가지고 있는 기능들을 사용할 수 있음은 자명합니다. 그런데 만약 이런 동물캐릭터들이 여러마리라면 어떨까요? 뭐 Dog 가 5마리, Cat 10마리, Pig 4마리가 있다고 한다면 각각 마다 참조변수를 만드는것은 도저히 용납이 안됩니다. 귀찮으니까요. 그렇다면 이렇게 데이터가 많으니, 배열을 이용해보면 어떨까요? 다음 코드를 봅시다.

Dog[] dogs = new Dog[5];
dogs[0] = new Dog();
dogs[1] = new Dog();

dogs[0].say();
dogs[1].say();

10마리의 Dog 를 각각의 참조변수를 만들지 않고 배열객체 한개를 이용해서 만들었습니다. 나름 발전한것 같네요. 하지만, 이렇게 배열로 만들어버리게 되면 dogs 는 Dog 의 객체만 관리할 수 있습니다. 그러면 한번 더 묶어봅시다. Dog 와 Cat 그리고 Pig 를 모두 제어할 수 있는 참조변수가 있으면 좋을것 같습니다. 그러면 세 종류의 동물을 하나의 배열로 묶을 수 있으니까요. 자 그래서 우리는 Animal 클래스를 만들겠습니다. 그리고 이 클래스를 각 동물 클래스가 상속받게 하겠습니다. 앞서 배웠던 업캐스팅을 통해 각각의 클래스들의 객체를 Animal 클래스의 참조변수가 할당받도록 해봅시다.

------------ Animal.java
public class Animal {
	
}
------------ Main.java
Animal ani;

메소드 오버라이드 : 자식 기능이 사용하고 싶다면

그런데 위처럼 사용을 하게 되면 자식들의 고유 기능에는 접근할 수 없기 때문에 아무런 기능을 사용할 수 없습니다. 그런데 앞서 보았듯, 업캐스팅을 해서 부모의 참조변수에 자식객체를 할당해준다면, 그리고 부모의 메서드가 오버라이드 되어 자식 클래스에 정의된다면 그 메서드는 부모의 참조변수로 접근이 가능했었습니다. 그러면 다음과 같이 say() 메서드를 Animal 에 작성하면 각 클래스가 만들었던 say() 메서드는 오버라이드 된 형태로 남게됩니다.

------------ Animal.java
public class Animal {
	void say() { }
}
------------ Main.java
Animal ani;
ani = new Dog();
ani.say();
ani = new Cat();
ani.say();
ani = new Pig();
ani.say();

// 출력결과 : 멍멍! / 야옹~ / 꿀꿀...

이 처럼 ani.say() 로 모양은 모두 같지만 다 다른결과를 도출했습니다. 이를 다형성 이라고 합니다. 그런데 가만히 살펴보니, Animal 참조변수를 통해 Dog, Cat, Pig 의 객체를 담을 수 있었습니다. 그렇다면 Animal 참조변수의 배열을 만들면, 여러 종류의 객체를 모두 참조할 수 있을것 같습니다!

------------- Main.java
Animal[] anis = new Animal[3];
anis[0] = new Dog();
anis[0] = new Cat();
anis[0] = new Pig();

추가적으로 반복문 또한 사용이 가능합니다.

// 일반 for 문
for (int i = 0; i < anis.length; i++) {
	anis[i].say();
}


// 확장 for 문 
for(Animal t : anis) {
	t.say();
}

다운캐스팅 : 자식의 고유기능을 사용하고 싶다면.

추가로 AnimalFactory 라는 클래스를 하나 만들어 봅시다. 이 클래스에서는 Dog, Cat, Pig 객체를 생성하여 리턴해주는 메소드를 가진 클래스 입니다. 그리고 메인 클래스에서 각 클래스의 참조변수에 객체를 담는 코드를 만들어보겠습니다. 아래 switch 문을 통한 코드구성은 잘 봐두고 넘어갑시다.

------------ Dog.java
public class Dog extends Animal{
	
	@Override
	void say() {
		// TODO Auto-generated method stub
		System.out.println("멍멍!");
	}
	void guardHouse() {
		System.out.println("잘지켜!");
	}
}

------------ Cat.java
public class Cat extends Animal{

	@Override
	void say() {
		// TODO Auto-generated method stub
		System.out.println("야옹~");
	}
	void handleButler() {
		System.out.println("집사 부리기~~~");
	}
}
------------ Pig.java
public class Pig extends Animal{
	
	@Override
	void say() {
		// TODO Auto-generated method stub
		System.out.println("꿀꿀...");
	}
	void eatAndEat() {
		System.out.println("먹고먹고먹고..");
	}
}
------------ AnimalFactory.java
public class AnimalFactory {
	
    Animal makeAnimal(int n) {
    	Animal ani = null;
        
        switch(n) {
        case 1:
        	ani = new Dog();
            break;
        case 2:
        	ani = new Cat();
            break;
        case 3:
        	ani = new Pig();
            break;
            
        return ani;
    }
}
------------- Main.java
public class Main {
	public static void main(String[] args) {
    	final int DOG = 1, CAT = 2, PIG = 3;
    	AnimalFactory af = new AnimalFactory();
        
        Dog d = (Dog) af.AnimalFactory(DOG);
        d.say();
        d.guardHouse();
        
        Cat c = (Cat) af.AnimalFactory(Cat);
        c.say();
        c.HandleButler();
        
        Pig p = (Pig) af.AnimalFactory(PIG);
        p.say();
        p.eatAndEat();
    }
}

Main 클래스에서 보면 다운캐스팅을 통해 각 클래스의 참조변수에 객체의 주솟값을 담아주고 있습니다. 그래서 동물클래스들의 공통적인 기능인 say()메서드 뿐 아니라 각 클래스의 고유한 기능들 까지 사용이 가능함을 알 수 있습니다.

종합 예제 - 공통기능과 고유기능을 사용해보자!

------------------- Main.java
... 중략
Random rand = new Random();

Animal[] anis = new Animal[5];

for(int i = 0; i < anis.length ; i++) {
	int n = rand.nextInt(3) + 1;
    anis[i] = af.makeAnimal(n)
}

for(int i = 0; i < anis.length ; i++) {
	// 동물 클래스들의 공통 기능
    anis[i].say();
    
    // 동물 클래스들의 고유 기능
    if(anis[i] instanceof Dog) {
    	((Dog) anis[i]).guardHouse();
    } else if( anis[i] intanceof Cat ) {
    	((Cat) anis[i]).HandleButler();
    } else if( anis[i] intanceof Pig ) {
    	((Pig) anis[i]).eatAndEat();
    }
}

공통 기능을 호출하는것은 어렵지 않습니다. 각 클래스마다 오버라이드된 say() 메서드가 있기 때문에, anis[i].say() 로 호출하면 잘 작동 됩니다. 눈여겨봐야할 부분은 각 클래스의 고유기능을 호출하는 부분입니다. anis[i] 에 어떤 클래스의 객체가 들어가있는지는 랜덤값으로 결정되기 때문에, if 문을 활용하여 경우의 수를 나누어주어야 합니다. 여기서 활용될수 있는 연산자가 instanceof 입니다. 연산자 왼쪽에 오는 대상이 판단하고자 하는 객체이며, 오른쪽에 오는 대상은 판단의 기준이 되는 클래스 입니다. 즉, 어떠한 객체가 어떠한 클래스의 객체이냐 아니냐를 판별해줍니다. 여기서는 만일, anis[i] 가 Dog 의 객체라면~ 이라는 뜻으로 사용되었습니다. 그래서 다운캐스팅을 통해 각 클래스의 고유 기능을 호출해주었습니다.

추상적 개념의 클래스

마지막으로 Animal 클래스에 대해 한번 생각해봅시다. Animal 클래스가 가지고 있는 정보는 무엇인가요? 사실, 아무런 정보도 없습니다. 메소드의 내용도 없고, 멤버변수도 없습니다. 다만, Dog와 Cat, Pig 를 한데 개념적으로 묶어주는 역할만 합니다. 즉, 오로지 상속을 위해서만 존재하는 추상적 개념의 클래스 입니다. 그렇게 묶어서 사용해야 업캐스팅이 가능해지고 메소드 오버라이드를 통해 기능구현이 가능하기 때문입니다. 그런데 사실 보기좋은 모양새는 아닌것같습니다. 모름지기 클래스란, 어떠한 기능을 하는 단위의 묶음인데, 아무런 기능도 하지 않는 클래스는 사실 좀 그렇습니다. 또한 Animal 처럼 클래스를 설계하게 된다면 설계를 한 당사자는 Animal 이 추상적인 개념을 가지고 있는 클래스라는 사실을 인지하고 있지만, 사용하는 사람의 입장에서는 단순히 아직 완성되지 않은 클래스일 뿐입니다. 그래서 자바에서는 이러한 클래스가 객체를 생성하지 못하도록 문법적으로 막아주는 개념이 있습니다. 바로 추상클래스 입니다.



추상클래스와 인터페이스


추상 클래스 (abstract class)

구체적이지 않은, 추상적인 데이터를 담고 있는 클래스


추상 클래스, 어떻게 쓰는데?

일단, 추상 클래스의 생김새 부터 살펴보겠습니다.

public abstract class Test {
	int a;
    static int b;
    
    public Test() {
    	System.out.println("Test 생성자");
    }
    
    public void show() {
    	System.out.println("Test show 입니다.");
    }
    
    abstract void aaa();
}

class 키워드 앞쪽에 abstract 키워드를 붙이면 추상 클래스가 생성됩니다. 추상 클래스란 곧바로 객체 생성을 할 수 없는 클래스입니다. 즉, 상속용 클래스라고 보면 될것 같습니다. 위 코드에서도 볼 수 있듯, 일반 클래스 처럼 멤버변수, static 변수, 생성자, 일반 메소드 등을 포함할 수 있습니다. 다만 특이한 점은 추상 메소드도 가질 수 있다는 점입니다. 추상 메소드란 이름만 있고 기능은 없는 메소드를 이야기 합니다. 또한 추상 메소드를 보유한 클래스는 추상 클래스로 선언되어야만 합니다.

또하나의 특징은 추상클래스를 상속받는 클래스들은 반드시 추상 클래스에 정의되어 있는 추상메소드를 구현해야 한다는 점입니다. 앞서 Animal 예제에서 Animal 클래스를 상속받는 클래스들은 say() 메소드를 오버라이드 하여 사용하였습니다. 추상 클래스는 이를 강제하는 문법이라고 생각하면 됩니다. 자식 클래스들이 반드시 구현해야 하는 메소드를 추상클래스의 추상메소드로 만들어 놓으면 자식 클래스에서는 이 메서드를 반드시 구현해야 합니다.


인터페이스(interface)

추상메소드만 가지는 추상 클래스!


추상 클래스 vs 인터페이스

추상 클래스는 상속용 클래스 이며, 일반 클래스 처럼 멤버변수, static 변수, 생성자 , 일반 메소드 등을 포함할 수 있다고 했습니다. 반면에 인터페이스의 경우, 추상메소드만 가지는 클래스 입니다. 일반 메소드는 정의할 수 없습니다. 추상 클래스에 비해 조금 더 엄격하며 인터페이스라는 기능이 가지는 의미가 좀 더 명확하다고 보는게 좋을것 같습니다.

인터페이스 구현

인터페이스는 규격만을 정하는 용도이기 때문에 기능구현이 따로 되어있지 않은 설계도 입니다. 그렇기 때문에 추상클래스와 마찬가지로 곧바로 객체 생성은 불가합니다.(단, 참조변수는 생성가능합니다.) 그래서 이러한 인터페이스를 사용하기 위해서는 규격을 구현한 별도의 클래스를 설계하고 객체를 생성해서 사용해야 합니다. 또한 상속을 위한 키워드가 extends 였듯, 인터페이스를 구현하기 위해서는 implements 키워드가 필요합니다. 이제 구현해봅시다.

-------------- Test.java
public interface Test {
	abstract void aaa(); 
    void bbb();
}

-------------- First.java
public class First implements Test {
		@Override
	public void aaa() {
		// TODO Auto-generated method stub
		System.out.println("First가 구현한 aaa");
	}
	@Override
	public void bbb() {
		// TODO Auto-generated method stub
		System.out.println("First가 구현한 bbb");
	}
}
-------------- Main.java
public class Main {
	public static void main(String[] args) {
    	First f = new First();
        f.aaa();
        f.bbb();
    }
}

인터페이스의 추상 메소드는 abstract 키워드를 사용해서 만들어도 됩니다만, 따로 명시하지 않아도 자동으로 abstract 키워드를 붙여줍니다.

다중 인터페이스 상속

자바에서 다중 상속은 허용하지 않습니다. A 라는 조부모 클래스가 있고, A 클래스를 상속받는 B,C 클래스가 있다고 합시다. 그리고 A 클래스에는 aaa() 라는 메소드가 있으며, B,C 클래스에서 이 메서드를 오버라이드 했습니다. 만일, 다중상속을 허용한다면, 그래서 B,C 클래스를 다중 상속 받았다면 자식 클래스에서 aaa() 메서드를 호출했을 때 어떤 클래스의 메서드를 호출해야 할지 불분명한 문제가 생깁니다. 그러한 이유로 자바에서는 다중 상속을 허용하지 않는데, 인터페이스의 경우 다중 상속을 허용합니다. 인터페이스에는 추상 메소드만 존재하기 때문에 어떠한 메소드를 구현할지는 상관이 없기 때문입니다. 또한 인터페이스를 상속하는 클래스를 상속하는것도 가능합니다. 다음 코드로 종합해보겠습니다.

------------- AAA.java
public interface AAA {
	public void aaa();
    public void ccc();
}

------------- BBB.java
public interface BBB {
	public void bbb();
}

------------- First.java
public class First implements Test{

	@Override
	public void aaa() {
		// TODO Auto-generated method stub
		System.out.println("First가 구현한 aaa");
	}
	@Override
	public void bbb() {
		// TODO Auto-generated method stub
		System.out.println("First가 구현한 bbb");
	}
}
------------- Good.java
public class Good extends First implements AAA,BBB{

	@Override
	public void ccc() {
		// TODO Auto-generated method stub
		System.out.println("Good 이 구현한 ccc");
	}

}

스타크래프트 종합예제

업캐스팅, 다운캐스팅, 추상메서드, 인터페이스를 한번에! (feat. ArrayList)


어떻게 만들까?

팀장과 팀원 3명이 마린, 탱크, 레이스 유닛을 설계하는 예제입니다. 먼저, 팀장은 인터페이스를 설계합니다. 그리고 추상메서드를 만들면 팀원들은 그것을 토대로 기능을 구현해 클래스를 설계하게 됩니다.

팀장 : Unit 인터페이스 설계

public interface Unit {
	public abstract move();
    public abstract attack();
}

팀원 1 : Marine 클래스 정의 및 Unit 기능 구현

public class Marine implements Unit {
	public void move() {
    	System.out.println("걸어서 이동...");
    }
    public void attack() {
    	System.out.println("총알 발사!");
    }
}

팀원 2 : Tank 클래스 정의 및 Unit 기능 구현

public class Tank implements Unit {
	public void move() {
    	System.out.println("바퀴로 이동...");
    }
    public void attack() {
    	System.out.println("자주포 발사!");
    }
}

팀원 3 : Wraith 클래스 정의 및 Unit 기능 구현

public class Wraith implements Unit {
	public void move() {
    	System.out.println("날아서 이동...");
    }
    public void attack() {
    	System.out.println("미사일 발사!");
    }
}

팀장은 이제 만들어진 클래스를 이용하여 객체를 생성하고, 게임의 알고리즘을 구현하게 됩니다!

유닛별 참조변수를 따로 만들어서 사용할 수도 있지만, 부모 참조변수로 자식 객체들을 모두 참조(업캐스팅) 할 수 있으니 차라리 Unit 참조변수 타입 하나로 제어하는것이 보다 효과적일것 같습니다. 배열로도 묶을 수 있으니 말이죠. 단, 실제 게임의 경우 유닛들의 생성과 제거가 빈번하기 때문에 배열의 개념을 사용하기 보다는 유동적 배열의 개념을 사용하는것이 효과적입니다. 그러한 개념을 ArrayList 라고 합니다. 그러면 ArrayList 를 사용해보겠습니다. 아직 배우지 않은 개념도 나오니 그렇구나 정도로만 익히고 넘어가겠습니다.

--------------- Main.java
ArrayList<Unit> units = new ArrayList<Unit>();

int num = units.size(); // 요소 개수 확인

units.add(new Marine());
units.add(new Tank());
units.add(new Wraith()); // 업캐스팅을 활용한 요소 추가

for(int i = 0; i < units.size() ; i++) { // 유닛 기능 사용해보기
	Unit unit = units.get(i);
    unit.move();
    unit.attack();
}

for(Unit unit : units) { // 확장 for 문 이용
	unit.move();
    unit.attack();
}

이제 기능을 하나만 추가해보겠습니다. 레벨업을 하는 기능입니다. 단, 레벨업을 하는 유닛은 마린과 탱크 유닛 입니다. 다음과 같이 인터페이스를 하나 만들어주고 마린과 탱크 클래스에 구현시켜 줍니다.

-------------- LevelUpAble.java
public interface LevelUpAble {
	public abstract void levelUp();
}

-------------- Marine.java
public class Marine implements Unit,LevelUpAble{
... 중략 

	@Override
	public void levelUp() {
		// TODO Auto-generated method stub
		System.out.println("레벨업!");
	}
	
}

-------------- Tank.java
public class Tank implements Unit,LevelUpAble{
... 중략

	@Override
	public void levelUp() {
		// TODO Auto-generated method stub
		System.out.println("레벨업!");
	}

}

그러면 다음 Main 클래스에서 레벨업 하는 로직을 짜볼 수 있습니다.

--------------- Main.java
... 중략

if( unit instanceof Marine) {
	((Marine)unit).levelUp();
} else if(unit instanceof Tank) {
	((Tank)unit).levelUp();
}

위 코드에서는 마린과 탱크만 레벨업이 가능했습니다. 그런데 만약 레벨업 되는 유닛이 많다면 어떻게 해야할까요? 다음 코드를 보겠습니다.

--------------- Main.java
... 중략

if(unit instanceof LevelUpAble) {
	((LevelUpAble)unit).levelUp();
}
profile
Developer

0개의 댓글