대학생 시절에 '추상클래스는 클래스를 추상화하여 구현없는 껍데기를 정의해놓은 클래스이며 인터페이스는 클래스와 달리 인스턴스 변수를 가질 수 없는 즉, 더욱 추상화된 껍데기이며 다중 구현이 가능하다.' 라고 배웠다. 자바의 정석을 읽으면서 더 깊은 내용을 정리해보려고 한다.

추상클래스

추상클래스는 자체적으로 클래스의 역할을 다 하지는 못한다. 새로운 클래스를 작성할 때, 아무것도 없는 상태에서 시작하지 않고 어느 정도 틀을 갖춘 상태를 정의한다. 물론 추상클래스도 일반 클래스와 같이 구현, 생성자, 초기화 블럭의 사용이 가능하다.

특징

  • 일반클래스의 특징을 갖는다. 즉 상태, 생성자, 초기화 블럭, 구현된 메서드등을 포함할 수 있다.
  • 일반클래스의 특징을 갖기때문에 Object라는 최상위 부모가 존재한다.
  • 다중 상속이 불가능하다.

예시

하나의 큰 카테고리에서 하위의 카테고리들의 설계는 60~80%는 비슷할 것이다. 같은 크기의 냉장고, Tv라고 하더라도 여러 종류의 모델이 있지만 냉장고, Tv의 종류가 다르다고해서 전혀 다른 역할은 하지 않기 때문이다.

그냥 조상클래스에 빈 메소드를 만들어 놓고 오버라이딩 하면 되지 않나?

추상클래스는 구현된 메소드와 추상메소드를 모두 정의할 수 있다. 만약 해당 클래스를 추상클래스가 아닌 일반클래스에서 상속한다면 추상메소드는 반드시 오버라이딩해야한다. 반면에, 이미 구현된 메소드의 오버라이딩은 선택사항이다.

인터페이스

인터페이스는 추상클래스와 비슷하다. 추상클래스처럼 추상메소드를 갖지만 추상화의 정도가 보다 높다. 추상화가 더 높기 때문에 추상메소드와 상수를 제외하고는 어떠한 요소도 가질 수 없다(자바8 이전).

특징

  • Object와 같은 최상위 조상은 없다.
  • 인터페이스도 추상클래스와 마찬가지로 인스턴스의 생성이 불가능하다.
  • 클래스의 경우 여러 개의 인터페이스를 구현할 수 있으며, 인터페이스 간에는 다중 상속이 가능하다.

다형성

스타크래프트에서 테란에는 유닛이 있다. 이 유닛은 다시 지상유닛과 공중유닛으로 구분된다. 지상유닛에는 SCV, 탱크, 마린과 같은 유닛들이 있으며 공중유닛에는 배틀크루저와 같은 유닛들이 있다.

  • 유닛
    • 지상유닛
      • SCV
      • 탱크
      • 마린
    • 공중유닛
      • 배틀크루저
class Unit {
    int hp = 100;
};

class GroundUnit extends Unit { };
class AirUnit extends Unit { };

class SCV extends GroundUnit { };
class Tank extends GroundUnit { };
class Marine extends GroundUnit { };

class Battle extends AirUnit { };

이러한 상황에서 SCV에 기계(SCV, Tank, Battle) 유닛을 수리하는 기능을 추가해보자.

class SCV extends GroundUnit {
    public void repair(SCV scv) { scv.hp += 10; };
    public void repair(Tank tank) { tank.hp += 10; };
    public void repair(Battle battle) { battle.hp += 10; };
};

repair() 메소드는 인자로 넘어온 유닛의 체력을 일부 회복시킨다. 위와 같이 오버로딩을 이용하여 각 유닛마다 메소드를 추가하면 된다. 하지만 이 코드에는 문제가 존재한다. 체력이라는 속성은 Unit에 속해있다. 심지어 각 유닛마다 구현이 다를 필요도 없다. 이는 코드의 중복을 의미한다.

해결방법을 찾아보자.

Scv와 Tank는 지상유닛, Battle은 공중 유닛이므로 메소드를 2개로 줄인다.

public void repair(GroundUnit groundUnit) {
    if (groundUnit instanceof SCV || groundUnit instanceof Tank) {
        groundUnit.hp += 10;
    } else {
        throw new IllegalArgumentException();
    }
}
public void repair(AirUnit airUnit) {
    if (airUnit instanceof Battle) {
        airUnit.hp += 10;
    } else {
        throw new IllegalArgumentException();
    }
};

GroudnUnit 중 SCV와 Tank만 수리가 가능하므로 instanceof 구문을 이용하여 repair() 메소드를 구현했다. 이제 repair() 메소드에 Marine의 인스턴스를 전달하면 IllegalArgumentException가 던져질 것이다. 무사히 해결한 것 같지만, 이 코드 역시 문제점이 존재한다. 게임을 업데이트하면서 기계 유닛이 추가되면 if문에 코드를 추가해야한다.

코드의 변경이나 추가가 없이 해결할 수 있는 방법을 찾아보자.

interface Repairable { };
class Tank extends GroundUnit implements Repairable { };
class Battle extends GroundUnit implements Repairable { };
class Scv extends AirUnit implements Repairable { 
	void repair(Repairable r) {
		if(r instanceof Unit) {
			Unit unit = (Unit)r; // 조상 클래스인 Unit에 정의된 인스턴스 변수를 사용하기 위해 업캐스팅
			unit.hp += 20;
		}
	}
}

각각의 클래스는 이미 부모클래스가 있기 때문에 인터페이스를 이용한다. Repairable이라는 인터페이스를 만들어 수리가 가능한 유닛들이 이를 구현하도록 한다. Repairable이라는 타입으로 수리가 가능한 타입의 객체를 참조할 수 있다. 여러가지 형태를 가질 수 있는 다형성의 특징을 이용한 것이다.

위 코드에서는 Repairable의 참조 변수로 Scv, Tank, Battle를 사용할 수 있다는 의미다. 이렇게 repair메소드의 인자로 Repairable의 참조변수를 넘겨 받는 이유는 해당 참조변수와 상속관계에 있지 않은 객체가 넘어오면 오류를 내기 위해서다.이제 새로운 유닛을 추가할 때, 해당 유닛이 수리가 가능하게하려면 단순히 Repairable만 구현하도록 하면 되는 것이다.