본문 바로가기
Java/Basic

추상클래스(abstract class)

by 최로이 2021. 4. 8.

1. 추상클래스란?

클래스를 설계도에 비유한다면, 추상클래스는 '미완성 설계도'에 비유할 수 있다. 말 그대로 완성되지 못한 설계도.

클래스가 미완성이라는 의미는 멤버의 개수에 관계된 것이 아니라, 단지 미완성 메소드를 포함하고 있다는 의미다.

지금까지 공부했던 것처럼 미완성 설계도로 완성된 제품을 만들 수 없듯이 추상클래스로는 '인스턴스'를 생성할 수 없다. 추상클래스는 오로지 상속을 통해서 자식클래스에 의해서만 완성될 수 있다.

 

추상클래스 자체로는 클래스의 역할을 다 하지는 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 부모클래스로서 중요한 의미를 갖는다. 새로운 클래스를 작성할 때, 아무 것도 없는 상태에서 시작하는 것보다는 완전하지는 못하더라도 어느 정도 틀을 갖춘 상태에서 시작하는 것이 나을 거다. 

 

만약 TV라는 제품을 만드는 클래스를 설계할 때 TV가 갖는 공통적인 부분들을 그린 미완성 설계도를 만들어 놓는다면, 이 미완성 설계도를 이용해 각각의 TV설계도를 완성하는 것이 훨씬 효율적일 방법이 될 수 있다.

 

구현 방법

abstract class 클래스명 { ... }

 

추상클래스는 추상메소드를 포함하고 있다는 것을 제외하면 일반적인 클래스와 다르지 않다. 추상클래스에도 생성자가 있고, 멤버변수가 있고, 메소드가 있다.

 

추상메소드를 포함하고 있지 않은 클래스에도 키워드 abstract를 붙여서 추상클래스로 지정할 수도 있다. 추상메소드가 없는 완성된 클래스라고 할지라도 추상클래스로 지정되면 클래스의 인스턴스를 생성할 수 없다.

 

1.1 추상메소드(abstract method)

일반적인 메소드는 선언부와 구현부로 구성이 되어있다. 하지만 추상메소드는 선언부만 있고 구현부가 없는 메소드 형태를 가지고 있다. 즉, 설계만 해놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메소드인 것이다.

 

그렇다면 왜 추상메소드를 사용할까? 그 이유는 메소드 내용이 상속받는 클래스에 따라 달라질 수 있기 때문이다. 즉, 부모클래스에서는 선언부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 거다. 그래서 추상클래스를 상속 받는 자식클래스는 부모의 추상 메소드를 상황에 맞게 적절하게 구현해야 한다.

추상메소드 또한 abstract가 앞에 붙고, 구현부가 없으므로 괄호{ } 대신에 문장의 긑을 알리는 세미콜론( ; )을 적어준다.

/* 주석을 통해 어떤 기능을 수행할 목적으로 작성했는지 설명하는 게 좋다 */
abstract
리턴타입 메소드명();

추상클래스로부터 상속 받는 자식클래스는 오버라이딩을 통해 부모인 추상클래스의 추상메소드를 모두 구현해야 한다. 만약 부모로부터 상속받은 추상메소드 중 '하나라도' 구현하지 않는다면, 자식클래스 역시 추상클래스로 지정해야 한다.

 

추상클래스와 추상메소드의 간단한 예제

abstract class Player {  //추상클래스
	abstract void play(int pos);  //추상메소드
	abstract void stop();  //추상메소드
	
	class AudioPlayer extends Player{
		void play(int pos) {/* 내용 생략 */}  //추상메소드를 구현
		void stop() {/* 내용 생략 */}  //추상메소드를 구현
	}
	
	abstract class AbstractPlayer extends Player{
		void play(int pos) {/*내용 생략*/}  //추상메소드를 구현
	}
}

 

1.2 추상클래스의 작성

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

추상(抽象) : 낱낱의 구체적 표상이나 개념에서 고옹된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용.

상속이 자식클래스를 만드는데 부모클래스를 사용하는 것이라면, 추상화는 기존 클래스의 공통부분을 뽑아내서 부모클래스를 만드는 것이다. 추상화를 반대되는 의미로 이해하면 보다 쉽게 이해할 수 있다.

상속계층도를 따라 내려갈수록 클래스는 점점 기능이 추가돼 구체화의 정도가 강해지고, 상속계층도를 따라 올라갈수록 클래스는 추상화의 정도가 강해진다. 즉, 상속계층도를 따라 내려갈수록 세분화되고, 올라갈수록 공통요소만 남게 된다.

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

 

추상클래스 예제

스타크래프트에 나오는 유닛들을 클래스로 정의.

class Marine {
	int x, y;  //현재 위치
	void move(int x, int y) {/* 지정된 위치로 이동 */}
	void stop() {/* 현재 위치에 정지 */}
	void stimPack() {/* 스팀팩 사용 */}
}

class Tank {
	int x, y;  //현재 위치
	void move(int x, int y) {/* 지정된 위치로 이동 */}
	void stop() {/* 현재 위치에 정지 */}
	void siegeMode() {/* 시즈모드 사용 */}
}

class Dropship {
	int x, y;  //현재 위치
	void move(int x, int y) {/* 지정된 위치로 이동 */}
	void stop() {/* 현재 위치에 정지 */}
	void load() {/* 선택된 유닛 태우기 */}
	void stimPack() {/* 선택된 대상 내리기 */}
}

위 유닛들은 각자의 기능을 가지고 있지만 공통점을 추출해 아래처럼 하나의 클래스로 만들고, 해당 클래스로부터 상속 받도록 변경.

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 siegeMode() {/* 시즈모드로 변환 */}
}

class Dropship extends Unit{
	void move(int x, int y) { /* 지정된 위치로 이동 */ }
	void load() {/* 선택된 유닛들 승차 */}
	void unload() {/* 선택된 유닛들 하차 */}
}

각 클래스의 공톰부분을 추출해 Unit클래스를 정의하고 이로부터 상속받도록 했다. 이 Unit클래스는 다른 유닛을 위한 클래스를 작성하는데 재활용 할 수 있을 것이다.

이들 클래스에 대해 stop메소드는 선언부와 구현부 모두 공통적이지만 Marine, Tank, Dropship의 특성상 이동방법이 서로 다르기 때문에 move메소드의 실제 구현 내용도 다를 것이다.

하지만 move메소드의 선언부는 같기 때문에 추상메소드로 정의할 수 있다. 최대한의 공통부분을 뽑아내기 위한 것이기도 하지만, 모든 유닛은 이동할 수 있어야 하므로 Unit클래스에는 move메소드가 반드시 필요한 것이기 때문이다. 

move메소드가 추상메소드로 선언된 것에는, 앞으로 Unit클래스를 상속받아 작성되는 클래스는 move메소드를 자신의 클래스에 알맞게 반드시 구현해야 한다는 의미가 담겨 있는 것이기도 하다.

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

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

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

다형성에서는 부모클래스 타입의 참조변수로 자식클래스 인스턴스를 참조할 수 있기 때문에 이처럼 부모클래스 타입의 배열에 자식클래스의 인스턴스를 담을 수 있는 것이다. 만약 해당 클래스간의 공통부모가 없었다면 하나의 배열로는 다룰 수 없다. 

헷갈리지 말아야 할 점은 group[i].move(100, 200)과 같이 호출하는 것이 Unit클래스의 추상메소드인 move를 호출하는 것 같이 보이지만 실제로는 이 추상메소드가 구현된 Marine, Tank, Dropship인스턴스의 메소드가 호출되는 것이라는 점이다.

댓글