1. 추상 클래스의 문법
먼저 추상 클래스가 무엇이며 왜 필요한지 알아봅시다. LEGO 조립 설명서가 있다고 상상해 보세요: 공통적으로 쓰이는 부품(예: “바퀴”, “블록”)이 있고, 특정 모델만을 위한 특별한 부품이 있습니다. 추상 클래스는 특정 모델을 완성하는 상세 조립법이 아니라 부품을 다루는 일반 규칙에 대한 설명서와 같습니다. 블록은 홈으로 맞물리고, 바퀴는 축에 고정되며, 부품은 층층이 쌓을 수 있다는 식이죠. 추상 클래스는 이런 공통 규칙과 필수 단계를 설명하지만, 구체적인 모델 조립은 서술하지 않습니다. 구체 모델은 하위 클래스이며, 공통 설명서를 기반으로 부족한 단계를 채웁니다.
Java에서 추상 클래스는 직접 인스턴스를 만들 수 없는 클래스(new AbstractClass()는 불가능)지만, 이를 상속받아 “미완성” 메서드를 구현할 수 있는 클래스입니다.
추상 클래스가 필요한 경우:
- 여러 객체에 공통된 동작이나 상태가 있지만, 일부 로직이 서로 다른 경우.
- 기본(공통) 동작은 제공하되, 일부는 하위 클래스에서 구현하도록 남겨 두고 싶은 경우.
- 이 클래스의 인스턴스를 직접 만들지 못하게 하고 싶은 경우(예: “동물”은 추상적 개념이고, “고양이”처럼 구체적인 것만 존재).
추상 클래스를 선언하는 방법
간단합니다. class 앞에 abstract 키워드를 붙입니다.
public abstract class Animal {
// 필드(예: 동물 이름)
protected String name;
// 생성자
public Animal(String name) {
this.name = name;
}
// 추상 메서드 — 본문(구현) 없음!
public abstract void makeSound();
// 일반(구현된) 메서드
public void sleep() {
System.out.println(name + " 잠든다: Zzz...");
}
}
특징:
- 추상 클래스는 구현된 메서드와 추상 메서드를 모두 포함할 수 있습니다.
- 추상 클래스는 필드, 생성자, 심지어 static 메서드도 포함할 수 있습니다.
- 추상 클래스의 인스턴스는 직접 만들 수 없습니다:
-
Animal a = new Animal("아무개"); // 오류!
추상 메서드를 선언하는 방법
추상 메서드는 메서드 본문이 없는 메서드로, 중괄호와 내부 코드가 없습니다. abstract 키워드로 선언하며 반드시 세미콜론 ;으로 끝납니다.
public abstract void makeSound();
- 추상 메서드는 추상 클래스 안에서만 선언할 수 있습니다.
- 추상 클래스를 상속하는 클래스는 모든 추상 메서드를 구현해야 하며, 그렇지 않으면 해당 클래스도 abstract가 됩니다.
2. 추상 클래스 상속: 동작 방식
구체적인 예시를 보겠습니다. 추상 클래스 Animal에 추상 메서드 makeSound()가 있다고 합시다. 이제 이 메서드를 구현하는 하위 클래스 Dog를 만들어 보겠습니다:
public class Dog extends Animal {
public Dog(String name) {
super(name); // 상위 클래스 생성자 호출
}
@Override
public void makeSound() {
System.out.println(name + " 짖는다: 멍멍!");
}
}
하나 더 만들어 보겠습니다:
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " 야옹한다: 야옹!");
}
}
이제 프로그램에서 이 클래스들을 사용할 수 있습니다:
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("샤릭");
Animal cat = new Cat("무르카");
dog.makeSound(); // 샤릭 짖는다: 멍멍!
cat.makeSound(); // 무르카 야옹한다: 야옹!
dog.sleep(); // 샤릭 잠든다: Zzz...
cat.sleep(); // 무르카 잠든다: Zzz...
}
}
유의하세요:
Animal 타입의 변수는 선언할 수 있지만, 인스턴스는 구체적(비추상) 하위 클래스만 생성할 수 있습니다.
도식: 구조
. Animal (abstract)
/ \
Dog Cat
(makeSound 구현) (makeSound 구현)
3. 추상 클래스와 인터페이스는 언제 사용하나요?
면접에서 아주 자주 나오는 질문입니다. 함께 정리해 봅시다:
- 추상 클래스 — 객체들이 공통 상태(예: 필드)와 공통 로직(구현 메서드)을 가지며, “뼈대”를 제공하고 세부 구현은 확장 가능하게 하고 싶을 때.
- 인터페이스 — 구현이나 상태 없이 메서드의 집합(계약)만 정의하고 싶을 때. Java 8부터 인터페이스에도 default/static 메서드가 생겼지만, 여전히 인터페이스는 “무엇을 할 수 있는가”를, 추상 클래스는 “그것을 어떻게 하는가”를 다룹니다.
생활 속 예:
“새” — 추상 클래스: 모든 새는 부리와 날개가 있고 날 수 있지만, 방식은 서로 다릅니다.
“비행” — 인터페이스: 날 수 있는 것은 새만이 아니라 비행기, 슈퍼히어로도 있습니다! 모두 나는 방식은 다르지만, 중요한 것은 “날 수 있다”는 사실입니다.
4. 실전 예시
예제 1: 부분 구현이 있는 추상 클래스
게임을 만든다고 해봅시다. 다양한 종류의 탈것이 있고, 모두 이동하지만 방식은 서로 다릅니다. 다만 모두 속도와 이름이 있고, 정지하는 표준 방식이 있습니다.
public abstract class Transport {
protected String name;
protected int speed;
public Transport(String name, int speed) {
this.name = name;
this.speed = speed;
}
// 추상 메서드 — 구현은 하위 클래스에서
public abstract void move();
// 구현된 메서드
public void stop() {
System.out.println(name + " 멈췄다.");
}
}
public class Car extends Transport {
public Car(String name, int speed) {
super(name, speed);
}
@Override
public void move() {
System.out.println(name + " 도로에서 시속 " + speed + " km/h로 주행한다.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name, int speed) {
super(name, speed);
}
@Override
public void move() {
System.out.println(name + " 시속 " + speed + " km/h로 페달을 밟는다.");
}
}
사용 예:
Transport car = new Car("Toyota", 120);
Transport bike = new Bicycle("Stels", 25);
car.move(); // Toyota 도로에서 시속 120 km/h로 주행한다.
bike.move(); // Stels 시속 25 km/h로 페달을 밟는다.
car.stop(); // Toyota 멈췄다.
bike.stop(); // Stels 멈췄다.
예제 2: 매개변수가 있는 추상 메서드
public abstract class Shape {
public abstract double area();
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
사용 예:
Shape c = new Circle(3);
Shape r = new Rectangle(4, 5);
System.out.println("원의 넓이: " + c.area());
System.out.println("직사각형의 넓이: " + r.area());
5. 추상 클래스의 특징과 주의점
- 추상 클래스는 public, protected, private 같은 모든 접근 제한자를 가질 수 있습니다(예: 필드).
- 추상 메서드가 하나도 없어도 추상 클래스로 선언할 수 있습니다. 기본 클래스의 인스턴스 생성을 금지하고자 할 때 유용합니다.
- 추상 클래스를 상속했지만 모든 추상 메서드를 구현하지 않으면, 그 클래스도 abstract로 선언해야 합니다.
- 추상 클래스도 생성자를 가질 수 있습니다. 이는 하위 클래스의 인스턴스를 만들 때 호출됩니다(super(...) 경유).
- 추상 메서드는 private일 수 없습니다(하위 클래스에서 구현할 수 없기 때문).
- 추상 클래스는 static 메서드와 필드를 포함할 수 있습니다.
- 추상 클래스는 인터페이스를 구현할 수 있지만, 그 메서드를 반드시 직접 구현할 필요는 없습니다 — 하위 클래스가 구현해도 됩니다.
표: 추상 클래스 vs 인터페이스
| 추상 클래스 | 인터페이스 | |
|---|---|---|
| 키워드 | |
|
| 필드를 가질 수 있음 | 가능(제한 없음) | Java 8부터 — static/final만 |
| 구현된 메서드 | 예 | Java 8부터 — default/static |
| 추상 메서드 | 예 | 예 |
| 다중 상속 | 아니오 | 예(여러 인터페이스 구현 가능) |
| 생성자 | 예 | 아니오 |
| 인스턴스 생성 가능 | 아니오 | 아니오 |
6. 추상 클래스와 메서드에서 흔한 실수
오류 №1: 추상 클래스의 인스턴스를 생성하려는 시도.
new Animal("아무개")처럼 작성하면, 컴파일러가 곧바로 추상 클래스는 그런 용도가 아니라고 알려줍니다. 기억하세요: 추상화는 “뼈대”이지 “살아 있는 객체”가 아닙니다.
오류 №2: 하위 클래스에서 모든 추상 메서드를 구현하지 않음.
하나라도 구현하지 않았다면 해당 클래스도 abstract로 선언해야 하며, 그렇지 않으면 컴파일 오류가 발생합니다.
오류 №3: 추상 클래스가 아닌 클래스에 추상 메서드를 선언함.
Java에서는 일반(비추상) 클래스에 추상 메서드를 선언할 수 없습니다 — 컴파일러가 즉시 오류를 보고합니다.
오류 №4: 추상 메서드를 private 또는 static으로 만들려는 시도.
추상 메서드는 private일 수 없습니다(재정의/구현할 수 없음) 그리고 static일 수도 없습니다(정적 메서드는 재정의 대상이 아님). 물론 final도 불가합니다. final은 재정의를 금지하니까요.
오류 №5: 추상 메서드의 접근 제한자를 명시하지 않음.
명시하지 않으면 메서드는 패키지 전용(package-private) 가시성을 갖게 되며, 이는 의도와 다를 수 있습니다.
GO TO FULL VERSION