추상 클래스(abstract class)

추상 클래스는 abstract 키워드로 선언된 불완전한 클래스를 말합니다. 추상 클래스는 인스턴스화 할 수는 없으나, 추상 클래스를 상속받는 자식 클래스의 인스턴스화는 가능합니다. 추상 클래스를 선언하는 방법은 다음과 같습니다.

abstract class 클래스명 {
	// ...
}

추상 클래스는 클래스처럼 생성자를 가질 수 있으며 인스턴스 메서드나 정적 메서드를 멤버로 가질 수도 있습니다. 이어서 추상 클래스의 특징과 추상 메서드를 함께 살펴보도록 하겠습니다.

추상 메서드(abstract method)

추상 메서드도 마찬가지로 abstract 키워드로 선언된 메서드를 말합니다. 추상 메서드는 아래와 같이 메서드 본문이 없습니다.

abstract class 클래스명 {
	abstract 반환형 메서드명(매개변수1, 매개변수2, ...);
}

클래스에 추상 메서드가 하나 이상 포함되면 반드시 추상 클래스로 선언되어야 합니다. 그렇지 않으면 '추상 메서드를 정의하는 클래스는 반드시 추상 클래스여야 한다'는 컴파일 에러가 발생합니다. 그리고 추상 클래스를 상속받는 자식 클래스는 부모가 가진 추상 메서드를 모두 오버라이딩 해야 합니다.

abstract class Shape {
	abstract double getArea();
}

// 에러: Rectangle 타입은 상속받은 추상 메서드 Shape.getArea()를 반드시 구현해야 합니다.
class Rectangle extends Shape { }

에러가 발생하는 이유는 추상 메서드의 구현이 없기 때문입니다. 따라서 아래와 같이 자식 클래스에서 getArea() 메서드를 구현해야 하거나, 추상 메서드를 물려받은 자식 클래스도 추상 클래스로 선언되어야 합니다.

abstract class Shape {
	abstract double getArea();
	abstract double getCircumference();
}

class Rectangle extends Shape {
	private double width;
	private double length;
	
	public Rectangle(double width, double length) {
		this.width = width;
		this.length = length;
	}
	
	public double getArea() {
		return width * length;
	}
	
	public double getCircumference() {
		return (width + length) * 2;
	}
}

추상 클래스라고 해서 추상 메서드만 들어가는 것은 아닙니다. 추상 메서드가 아닌 메서드가 추상 클래스의 멤버일 수도 있습니다.

abstract class Shape {
	private String name;
	
	public Shape(String name) {
		this.name = name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	
	abstract double getArea();
	
	abstract double getCircumference();
}

추상 클래스를 사용하는 이유

각각의 모양의 넓이나 둘레를 구하기 위해서 원, 사각형 등의 모양을 클래스 구조로 나타내고 싶은데, 이를 어떻게 작성해야 할까요? 이를 아래와 같이 작성할 수 있을 것입니다.

class Rectangle {
	private double width;
	private double length;
	
	public Rectangle(double width, double length) {
		this.width = width;
		this.length = length;
	}
	
	public double getArea() { /* ... */ return 0; }
	
	public double getCircumference() { /* ... */ return 0; }
}

class Circle {
	private double radius;
	
	public Circle(double radius) {
		this.radius = radius;
	}
	
	public double getArea() { /* ... */ return 0; }
	
	public double getCircumference() { /* ... */ return 0; }
}

간단히 생각해볼 수 있는 각 모양의 공통된 특성을 뽑으면 면적과 둘레를 구할 수 있다는 것입니다. 이렇게 공통된 특성을 뽑아서 하나의 클래스를 만들고, 그 클래스를 상속받아 위와 같이 오버라이딩 하면 다형성을 이용해서 타입을 신경쓰지 않고 각 객체에 면적이나 둘레를 구하라는 메시지를 전달할 수 있습니다.

class Shape { /* ... */ }

class Rectangle extends Shape { /* ... */ }

class Circle extends Shape { /* ... */ }

하지만 여기에 크게 두 가지의 문제가 있습니다. 첫 번째는 Shape 클래스를 인스턴스화 할 수 있다는 것입니다. 원, 사각형 등의 면적이나 둘레를 구하는 방법은 알겠는데, 모양의 면적이나 둘레는 어떻게 구해야 할까요? 필요한 정보가 아직 없으므로 구현을 자식 클래스로 미뤄 이를 구현하도록 해야 합니다. 그리고 두 번째는 Shape에 정의된 getArea(), getCircumference() 메서드를 자식 클래스인 Rectangle, Circle 클래스에서 오버라이딩 하지 않을 수도 있으므로 이를 강제해야 합니다.

abstract class Shape {
	public abstract double getArea();
	
	public abstract double getCircumference();
}

class Rectangle extends Shape {
	private double width;
	private double length;
	
	public Rectangle(double width, double length) {
		this.width = width;
		this.length = length;
	}
	
	public double getArea() {
		return width * length;
	}
	
	public double getCircumference() { 
		return (width + length) * 2;
	}
}

class Circle extends Shape {
	private double radius;
	
	public Circle(double radius) {
		this.radius = radius;
	}
	
	public double getArea() { 
		return Math.PI * radius * radius;
	}
	
	public double getCircumference() { 
		return Math.PI * 2 * radius;
	}
}


public class AbstractClassExample {
	public static void main(String[] args) {
		Shape[] shapes = { new Circle(5), new Rectangle(5, 10) };
		
		for (int i = 0; i < shapes.length; i++) {
			System.out.println((i + 1) + "번째 도형의 넓이: " + shapes[i].getArea());
			System.out.println((i + 1) + "번째 도형의 둘레: " + shapes[i].getCircumference());
		}
	}
}

위와 같이 추상 클래스를 사용하면 어떤 기능을 자식 클래스에서 구현하고 있다는 것을 보장할 수 있습니다. 이해가 되시나요?