이번 문서의 목표: 이 문서를 다 읽으면 abstract 클래스와 일반 클래스의 차이를 코드로 구분할 수 있고, abstract 메서드를 자식 클래스가 왜 반드시 구현해야 하는지 설명할 수 있으며, 템플릿 메서드 패턴이 다형성을 어떻게 활용하는지 코드 추적으로 확인할 수 있다.
왜 이 편이 필요한가
09편에서 Shape 클래스를 상속받아 Circle, Rectangle이 각자 area()를 오버라이딩하는 예제를 봤다. 그런데 잘 생각해 보면 이상한 점이 있다. “도형”이라는 개념 자체는 실제로 존재하는 구체적인 사물이 아니다. 넓이를 계산할 수 있는 것은 항상 원이거나 사각형이지, “도형 그 자체”의 객체를 만들어서 넓이를 구할 일은 없다. 그런데도 09편의 Shape 클래스는 new Shape()로 객체를 만드는 것을 문법적으로 막지 않았다. 이것은 설계상 허점이다. “이 클래스는 틀만 제공하고, 객체로 직접 만들어지면 안 된다”는 의도를 코드로 강제하고 싶을 때 쓰는 것이 이 편의 주제인 추상 클래스(abstract class)다.
쉽게 말하면: 추상 클래스는 “이 클래스 자체로는 완전하지 않으니, 자식 클래스가 나머지를 반드시 채워 넣어야 완성된다”는 설계 의도를 컴파일러가 강제하도록 만든 클래스다.
abstract 클래스와 abstract 메서드
클래스 선언에 abstract 키워드를 붙이면 그 클래스는 추상 클래스가 되고, new 연산자로 직접 객체를 생성할 수 없게 된다.
abstract class Shape {
abstract double area();
String describe() {
return "이 도형의 넓이는 " + area() + "이다.";
}
}여기서 area() 앞에도 abstract가 붙어 있는데, 이렇게 몸체(중괄호와 그 안의 코드) 없이 선언만 하고 세미콜론으로 끝내는 메서드를 추상 메서드(abstract method)라 한다. 추상 클래스와 추상 메서드에는 다음 규칙이 적용된다.
- 추상 클래스는
new로 직접 인스턴스화할 수 없다.new Shape()는 컴파일 오류다. - 추상 메서드는 반드시 abstract 클래스 안에서만 선언할 수 있다. 일반 클래스 안에 추상 메서드를 두면 컴파일 오류다.
- 추상 클래스라고 해서 모든 메서드가 추상 메서드일 필요는 없다. 위 예제의
describe()처럼 몸체가 있는 일반 메서드(구체 메서드, concrete method)를 함께 가질 수 있다. 이렇게 일부는 구현하고 일부는 비워 두는 것이 추상 클래스의 핵심 특징이며, 이를 부분 구현(partial implementation)이라 부른다. - 추상 클래스를 상속받는 자식 클래스는 부모의 모든 추상 메서드를 오버라이딩해서 구현해야 한다. 하나라도 구현하지 않으면 그 자식 클래스도 자동으로 추상 클래스가 되어야 하며, 그렇지 않으면 컴파일 오류다.
- 추상 클래스도 생성자를 가질 수 있다. 직접
new로 호출되지는 않지만, 자식 클래스의 생성자가super()로 호출한다(05편에서 다룬 생성자 호출 순서와 동일하게 동작한다). - 추상 클래스도 필드를 가질 수 있고, static 메서드도 가질 수 있다.
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
public class Main {
public static void main(String[] args) {
Shape s = new Circle(3.0);
System.out.println(s.describe());
}
}이 도형의 넓이는 28.274333882308138이다.Circle이 area()를 오버라이딩했기 때문에 Shape가 추상 클래스임에도 Circle 객체는 정상적으로 만들 수 있다. 만약 Circle이 area()를 구현하지 않았다면 다음과 같은 컴파일 오류가 발생한다.
error: Circle is not abstract and does not override abstract method area() in Shape이 오류 메시지는 “Circle이 abstract도 아니면서 Shape의 추상 메서드 area()를 오버라이딩하지도 않았다”는 뜻이며, 독학사 시험에서 “다음 코드가 컴파일되지 않는 이유”를 묻는 문제의 정답 근거로 자주 그대로 인용된다.
자주 틀리는 점: “추상 클래스는 메서드를 하나도 구현할 수 없다”고 착각하는 경우가 많다. 실제로는 구체 메서드와 추상 메서드를 섞어서 가질 수 있으며, 오히려 이 혼합이 추상 클래스의 존재 이유다.
추상 클래스를 왜 쓰는가 — 공통 코드의 재사용과 강제
추상 클래스가 필요한 이유는 두 가지로 요약된다.
- 공통 코드의 재사용.
describe()처럼 모든 자식 클래스가 똑같이 쓸 수 있는 코드는 부모(추상 클래스)에 한 번만 작성해 두면, 자식마다 중복해서 작성할 필요가 없다. - 구현의 강제.
area()처럼 자식마다 반드시 다르게 구현해야 하는 부분은 추상 메서드로 선언해서, 자식 클래스가 “깜빡하고 구현을 빠뜨리는 것”을 컴파일 오류로 막는다.
이 두 가지를 표로 정리하면 추상 클래스의 역할이 분명해진다.
| 요소 | 추상 클래스가 제공하는 것 | 자식 클래스가 해야 하는 것 |
|---|---|---|
구체 메서드(예: describe()) | 완성된 코드를 물려줌 | 그대로 상속받아 사용(재정의는 선택) |
추상 메서드(예: area()) | 이름·매개변수·반환 타입만 규정 | 반드시 몸체를 채워 구현해야 함 |
| 필드 | 공통 상태를 물려줄 수 있음 | 그대로 상속받아 사용 |
| 생성자 | 공통 초기화 코드를 제공 | super()로 호출해 활용 |
템플릿 메서드 패턴의 기초
09편에서 본 describe()처럼, 구체 메서드 안에서 추상 메서드를 호출하는 구조를 조금 더 발전시키면 템플릿 메서드 패턴(template method pattern)이 된다. 이 패턴은 “전체 진행 순서(알고리즘의 뼈대)는 부모 클래스가 고정하고, 각 단계의 세부 동작만 자식 클래스가 채워 넣는” 설계다.
abstract class CookingRecipe {
// 템플릿 메서드: 전체 순서는 여기서 고정
final void cook() {
prepareIngredients();
heat();
season();
System.out.println("요리 완성!");
}
void prepareIngredients() {
System.out.println("재료를 손질한다.");
}
abstract void heat();
abstract void season();
}
class FriedRice extends CookingRecipe {
@Override
void heat() {
System.out.println("팬에 기름을 두르고 강한 불로 볶는다.");
}
@Override
void season() {
System.out.println("간장과 소금으로 간을 맞춘다.");
}
}
class Soup extends CookingRecipe {
@Override
void heat() {
System.out.println("냄비에 물을 붓고 끓인다.");
}
@Override
void season() {
System.out.println("된장을 풀어 간을 맞춘다.");
}
}
public class Main {
public static void main(String[] args) {
CookingRecipe recipe1 = new FriedRice();
CookingRecipe recipe2 = new Soup();
recipe1.cook();
System.out.println("---");
recipe2.cook();
}
}재료를 손질한다.
팬에 기름을 두르고 강한 불로 볶는다.
간장과 소금으로 간을 맞춘다.
요리 완성!
---
재료를 손질한다.
냄비에 물을 붓고 끓인다.
된장을 풀어 간을 맞춘다.
요리 완성!이 코드에서 핵심은 cook() 메서드에 붙은 final 키워드다. final이 붙은 메서드는 자식 클래스가 오버라이딩할 수 없다(09편에서 다룬 오버라이딩 금지 대상 중 하나). 즉 “요리의 전체 순서(재료 손질 → 가열 → 간 맞추기 → 완성)는 절대 바뀌면 안 된다”는 설계 의도를 final로 못박아 두고, heat()와 season()이라는 바뀔 수 있는 부분만 추상 메서드로 열어 둔 것이다. prepareIngredients()는 기본 구현을 제공하되 자식이 원하면 오버라이딩할 수 있게 final을 붙이지 않았다.
쉽게 말하면: 템플릿 메서드 패턴은 요리 레시피의 “순서”는 고정하고 “구체적인 조리법”만 자식에게 맡기는 것과 같다. 순서를 뒤바꿀 자유는 주지 않고, 세부 동작만 갈아 끼울 수 있게 한다.
- 부모 추상 클래스에 템플릿 메서드(전체 순서를 담은 메서드)를
final로 선언한다. 순서 자체가 바뀌면 안 되기 때문이다. - 순서 안에서 호출되는 각 단계를 추상 메서드로 선언한다. 자식마다 달라야 하는 부분이다.
- 공통으로 쓰이는 단계는 구체 메서드로 미리 구현해 둔다. 자식이 굳이 다시 쓸 필요가 없게 한다.
- 자식 클래스는 추상 메서드만 오버라이딩해서 구현한다. 전체 순서를 알 필요 없이 자신이 맡은 세부 동작만 신경 쓰면 된다.
추상 클래스 vs 일반 클래스(상속만 하는 경우) 비교
시험에서는 “왜 굳이 추상 클래스를 쓰는가, 그냥 일반 클래스를 상속받으면 안 되는가”를 묻는 서술형 유형도 나온다. 다음 표로 차이를 정리한다.
| 상황 | 일반 클래스를 부모로 쓸 때 | 추상 클래스를 부모로 쓸 때 |
|---|---|---|
| 부모 클래스의 직접 인스턴스화 | 가능(막을 방법이 없음) | 불가능(컴파일러가 강제로 막음) |
| 자식이 특정 메서드를 구현하지 않았을 때 | 컴파일 오류 없음(부모의 기본 동작이 조용히 실행됨) | 컴파일 오류 발생(자식도 추상 클래스가 되어야 함) |
| 설계 의도 전달 | 문서나 주석으로만 전달 가능 | 언어 문법 자체로 강제됨 |
이 표가 보여주는 것은 “추상 클래스는 실수를 컴파일 시점에 잡아 준다”는 점이다. 일반 클래스로 같은 설계를 흉내 내면, 자식이 area() 구현을 깜빡해도 프로그램은 컴파일되고 실행되다가 잘못된 값(예: 부모의 기본값 0.0)을 조용히 반환할 수 있다. 추상 클래스는 이런 실수를 아예 컴파일 단계에서 차단한다.
자주 틀리는 점
- 추상 클래스에 생성자가 없다고 착각하는 것. 추상 클래스도 생성자를 가지며, 자식 생성자가
super()로 반드시 호출한다. - 추상 메서드에
private을 붙일 수 있다고 착각하는 것. 추상 메서드는 자식 클래스가 오버라이딩해서 구현해야 하므로private으로 선언할 수 없다.private은 자식 클래스에서 아예 보이지 않기 때문에 오버라이딩 자체가 불가능해진다. - 추상 클래스와 인터페이스(11편)를 완전히 같은 것으로 혼동하는 것. 추상 클래스는 필드와 생성자, 구체 메서드를 함께 가질 수 있어 “공통 상태와 공통 동작을 물려주는” 데 강점이 있고, 인터페이스는 다중 구현이 가능하다는 점에서 차이가 있다. 자세한 비교는 11편에서 다룬다.
- abstract 클래스에 static 메서드를 쓸 수 없다고 착각하는 것. abstract 클래스도 일반 클래스와 마찬가지로 static 메서드·static 필드를 가질 수 있다.
- 템플릿 메서드에 final을 안 붙여도 패턴이 성립한다고 생각하는 것. final 없이도 문법적으로는 동작하지만, 그러면 자식이 전체 순서 자체를 오버라이딩해서 바꿔 버릴 수 있어 “순서는 고정, 세부만 변경”이라는 패턴의 설계 의도가 깨진다.
핵심 정리
abstract키워드가 붙은 클래스는 직접 인스턴스화할 수 없고,abstract메서드는 몸체 없이 선언만 하며 반드시 abstract 클래스 안에만 존재할 수 있다.- 추상 클래스는 구체 메서드(공통 재사용)와 추상 메서드(구현 강제)를 함께 가질 수 있는 부분 구현 클래스다.
- 자식 클래스가 부모의 모든 추상 메서드를 구현하지 않으면 그 자식도 추상 클래스로 선언해야 하며, 그렇지 않으면 컴파일 오류가 발생한다.
- 템플릿 메서드 패턴은 전체 순서를
final메서드로 고정하고, 각 단계를 추상 메서드로 열어 자식이 세부 동작만 채우게 하는 설계다. - 추상 클래스는 실수(구현 누락)를 컴파일 시점에 강제로 잡아 준다는 점에서 일반 클래스 상속과 구별된다.
마무리 복습
참고 자료
- Oracle Java Tutorials - Classes and Objects — abstract 클래스와 abstract 메서드의 공식 문법 설명.
- E. Balagurusamy, Object-Oriented Programming in Java — 추상 클래스와 인터페이스를 교육용으로 체계적으로 정리한 교재.