1. 추상 클래스: 기초 복습
비교로 넘어가기 전에, 추상 클래스가 무엇인지 간단히 상기해 봅시다.
추상 클래스는 직접 인스턴스를 만들 수 없는 클래스입니다(new Animal()처럼 쓸 수 없음). 대신 구현이 있는 일반 메서드와 구현이 없는 추상 메서드를 모두 가질 수 있습니다. 추상 클래스는 다른 클래스의 기반으로 자주 사용되며, 이들 자식 클래스는 공통 동작을 상속받거나 특정 메서드 구현을 강제받습니다.
예를 들어 애플리케이션에 여러 종류의 교통수단이 있다면, 추상 클래스 Transport를 둘 수 있습니다:
public abstract class Transport {
private String model;
public Transport(String model) {
this.model = model;
}
public String getModel() {
return model;
}
// 추상 메서드 — 구현은 없고 선언만 있음
public abstract void move();
// 일반 메서드 — 구현이 있음
public void printInfo() {
System.out.println("교통수단 모델: " + model);
}
}
추상 클래스의 특징:
- 필드(상태)를 가질 수 있음.
- 구현된 메서드를 가질 수 있음.
- 추상 메서드를 가질 수 있음(자식에서 구현해야 함).
- 직접 인스턴스를 만들 수 없음.
- 공통 동작과 상태를 제공하는 데 사용됨.
2. 인터페이스: 기초 복습
인터페이스는 어떤 클래스가 구현해야 하는 메서드의 집합입니다. 인터페이스는 상태를 설명하지 않으며(일반 필드 불가, 상수만 가능) Java 8 이전에는 메서드 구현을 포함하지 않습니다. 인터페이스는 순수한 계약으로, “나를 구현한다면 이런 능력을 가져야 한다”를 의미합니다.
인터페이스 예시:
public interface Movable {
void move(int x, int y);
}
인터페이스의 특징:
- 상태를 가지지 않음(public static final 상수만 가능).
- Java 8 이전에는 오직 추상 메서드만 존재(Java 8부터 default 및 static 메서드가 추가됨 — 자세한 건 뒤에서).
- 메서드는 기본적으로 항상 public abstract.
- 하나의 클래스가 여러 인터페이스를 구현할 수 있음.
- 클래스가 “무엇을 할 수 있는지”를 기술하는 데 사용됨.
3. 비교 표: 추상 클래스 vs 인터페이스
이제 두 도구를 직접 비교해 봅시다! 아래 표를 참고하세요:
| 특징 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 문법 | |
|
| 인스턴스를 생성할 수 있나요? | 아니요 | 아니요 |
| 구현된(일반) 메서드를 가질 수 있나요? | 예 | Java 8 이전: 아니요, Java 8부터: default/static만 |
| 추상 메서드를 가질 수 있나요? | 예 | 예(Java 8 이전에는 모든 메서드가 추상) |
| 필드(상태)를 가질 수 있나요? | 예(어떤 필드든 가능) | public static final만(상수) |
| 생성자를 가질 수 있나요? | 예 | 아니요 |
| 상속 | 추상/일반 클래스 하나만 상속 가능 | 여러 인터페이스를 구현 가능 |
| 메서드 접근 제한자 | 제한 없음(public, protected, private) | 기본적으로 public abstract. Java 9부터 인터페이스 내부에서 사용할 private 메서드 추가 가능 |
| 상속/구현 키워드 | |
|
| 주 사용 목적 | 공통 구현과 상태 제공 | 능력, 역할의 기술 |
| JDK 예시 | |
|
4. 인터페이스와 추상 클래스는 언제 사용할까?
인터페이스는 다음과 같을 때 사용하세요:
- 구현 방식에는 신경 쓰지 않고, 클래스가 “무엇을 할 수 있는지”만 기술하고 싶을 때.
- 클래스가 서로 독립적인 여러 능력을 동시에 가져야 할 때.
- 예: Comparable(비교 가능), Serializable(직렬화 가능), Runnable(스레드에서 실행 가능).
추상 클래스는 다음과 같을 때 사용하세요:
- 모든 자식에게 공통으로 줄 구현과 상태를 정의하고 싶을 때.
- 모든 자식이 특정 필드나 구현이 있는 메서드를 갖도록 강제하고 싶을 때.
- 상속은 “하나 대 하나”로 제한될 때: 클래스는(일반이든 추상이든) 오직 한 클래스만 상속할 수 있음.
일상적인 비유
- 인터페이스는 “운전면허”와 같습니다. 면허가 있으면 차를 운전할 수 있지만, 어떤 차를 어떻게 운전하는지는 규정하지 않습니다.
- 추상 클래스는 “자동차의 공통 설계도”와 같습니다. 모든 자동차에는 운전대, 페달, 엔진이 있지만, 각 브랜드는 세부 구현이 다릅니다.
5. Java 표준 라이브러리의 예
인터페이스: Comparable
public interface Comparable<T> {
int compareTo(T o);
}
이 인터페이스를 구현하는 클래스는 반드시 compareTo 메서드를 구현해야 합니다. 예를 들어 String, Integer, LocalDate 등이 있습니다.
추상 클래스: AbstractList
public abstract class AbstractList<E> implements List<E> {
// 일부 List 메서드의 기본 구현
// 일부 메서드는 추상으로 남겨둠
}
AbstractList는 컬렉션의 일부 동작(예: 추가/삭제 메서드)을 이미 구현해 두지만, 몇몇 메서드는 자식이 각자 방식으로 구현할 수 있도록 추상으로 남겨 둡니다.
6. 코드 예제: 실제 비교
인터페이스
인터페이스와 이를 구현하는 클래스를 만들어 봅시다.
public interface Printable {
void print();
}
public class Document implements Printable {
@Override
public void print() {
System.out.println("문서를 인쇄합니다...");
}
}
추상 클래스
이제 추상 클래스와 그 자식을 만들어 봅시다.
public abstract class Machine {
public void turnOn() {
System.out.println("기계가 켜졌습니다.");
}
public abstract void work();
}
public class Printer extends Machine {
@Override
public void work() {
System.out.println("프린터가 인쇄 중입니다...");
}
}
클래스가 둘 다 사용: 인터페이스 구현 + 추상 클래스 상속
public class SmartPrinter extends Machine implements Printable {
@Override
public void work() {
System.out.println("스마트 프린터가 작동 중입니다...");
}
@Override
public void print() {
System.out.println("스마트 프린터가 인쇄 중입니다...");
}
}
7. 여러 인터페이스 구현: 왜 유용한가
Java에서 클래스는 (추상/일반) 클래스 한 개만 상속할 수 있지만, 인터페이스는 얼마든지 구현할 수 있습니다! 이를 통해 유연하고 확장 가능한 아키텍처를 만들 수 있습니다.
public interface Scannable {
void scan();
}
public class MultiFunctionPrinter extends Machine implements Printable, Scannable {
@Override
public void work() {
System.out.println("복합기가 작동 중입니다...");
}
@Override
public void print() {
System.out.println("복합기가 인쇄 중입니다...");
}
@Override
public void scan() {
System.out.println("복합기가 스캔 중입니다...");
}
}
언제 무엇을 선택할까?
- 공통 상태(예: 필드)와 기본 기능을 설계한다면 추상 클래스를 사용하세요.
- 객체에 “능력 라벨”(예: “인쇄 가능”, “비교 가능”, “직렬화 가능”)을 부여하고 싶다면 인터페이스를 사용하세요.
- 잘 모르겠다면 인터페이스부터 시작하는 것이 좋습니다. Java에서는 인터페이스가 더 큰 유연성과 확장성을 제공합니다.
8. 흔한 실수와 주의점
오류 1: 여러 클래스를 상속하려고 함 — Java는 허용하지 않습니다!
클래스는 오직 하나의 클래스만 상속할 수 있지만, 여러 인터페이스를 구현할 수 있습니다. 예를 들어 class A extends B, C는 오류이지만, class A extends B implements X, Y, Z는 가능합니다.
오류 2: 인터페이스의 필드와 클래스의 필드를 혼동함.
인터페이스에는 상수(public static final)만 선언할 수 있습니다. 예를 들어 private int count; 같은 일반 상태를 선언할 수 없으며, 컴파일러가 즉시 잡아냅니다.
오류 3: 인터페이스의 모든 메서드를 구현하지 않음.
클래스가 인터페이스의 메서드 중 하나라도 구현하지 않으면 그 클래스는 abstract로 선언되어야 하며, 그렇지 않으면 컴파일 오류가 발생합니다.
오류 4: 인터페이스나 추상 클래스의 인스턴스를 만들려고 함.
둘 다 “반제품”에 가깝습니다. 확장해서 써야 하며, 직접 생성할 수는 없습니다:
Printable p = new Printable(); // 오류!
Machine m = new Machine(); // 오류!
오류 5: 인터페이스에 생성자가 있을 수 있다고 생각함.
인터페이스는 객체의 상태를 정의하지 않으므로 생성자를 가질 수 없습니다. 생성자는 (일반 및 추상) 클래스에만 존재합니다.
GO TO FULL VERSION