상속(extends) : is Kind Of , IS - A “~이다”
구현(implements) : be Able to, HAS - A “~을 할 수 있는”
동작이 정의되지 않은 하나 이상의 추상 메서드를 포함하는 클래스이다.
상속(extends)을 통해 자손 클래스에서 재정의(Override)하여 사용할 수 있기에, 인스턴스 생성 불가
추상 클래스를 상속받는 자식 클래스가 반드시 추상 메서드를 구현하도록 하기 위한 목적으로 사용한다.
인터페이스는 클래스가 “무엇을 할 수 있다”라고 하는 기능을 구현하도록 강제하는 특징이다.
동일한 목적 하에 동일한 기능을 수행하도록 강제하여, Java의 다형성을 통해 유지보수성을 높인다.
ex) Serializable, Comparable, Runnable, Trainable, Petable
인터페이스는 클래스에 다중 구현을 지원하고, 인터페이스끼리 다중 상속이 가능하다.
기존 Java 클래스 다중 상속의 문제점 : 상속한 클래스들이 동일한 형태의 메서드를 가졌을때,
어떤 메서드를 실행해야하는지 판단할 수 없었다.
인터페이스의 메서드는 모두 추상 메서드이기 때문에, 겹치는 메서드이더라도,
인터페이스를 상속한 클래스에서 해당 메서드를 최종 구현하기에 다중 상속이 가능하다.
Java 8 부터 인터페이스에 default 메서드를 명시하여 선언할 수 있으므로,
동일한 default 메서드 명을 가진다면 다중 상속이 불가능하다.
해결 방법 3가지
“하위 호환성”을 위해 default 메서드가 구현 가능해졌다.
인터페이스를 보완하거나 수정하는 과정에서 추가로 구현해야할 필수적인 메서드가 생길때,
이미 인터페이스를 구현한 모든 클래스에서 추가된 추상 메서드를 구현해줘야 한다.
ex) Java 8의 Collection 인터페이스에 forEach(), stream() 등을 추가
따라서, default 메서드를 추가하고, 이를 정의하면 하위 호환성이 유지되며 인터페이스를 보완할 수 있다.
* 그대로 사용해도 되고, 재정의해서 사용 가능
interface Trainable {
void train(String command);
default void reset() {
System.out.println("기본 초기화!");
}
}
// 1. 그냥 상속 - default 메서드 그대로 사용
class Dog implements Trainable {
public void train(String command) { System.out.println(command + " 완료!"); }
// reset() 따로 구현 안 해도 됨 ✅
}
// 2. 오버라이드 - default 메서드 재정의
class Cat implements Trainable {
public void train(String command) { System.out.println("..."); }
@Override
public void reset() { // 필요하면 재정의 가능
System.out.println("Cat 전용 초기화!");
}
}
interface Trainable {
int MAX_LEVEL = 10; // 상수 (Java 8 이전부터)
void train(String command); // 추상 메서드 (Java 8 이전부터)
// ✅ Java 8 추가
default void reset() { // default 메서드 (구현 포함)
System.out.println("훈련 초기화!");
}
// ✅ Java 8 추가
static Trainable of(String type) { // static 메서드
if (type.equals("dog")) return new Dog("unknown");
throw new IllegalArgumentException();
}
// ✅ Java 9 추가
private void log(String msg) { // private 메서드 (내부 공통 로직용)
System.out.println("[LOG] " + msg);
}
}
static은 인스턴스가 아닌, 클래스/인터페이스에 귀속되기 때문에 다형성 대상이 아니다.
컴파일 시점에 타입이 고정되기 때문에 재정의 할 수 없다.
인터페이스와 강하게 관련된 유틸리티 기능을 인터페이스 내부에 함께 두기 위해서 추가됐다.
인스턴스 객체 필요없이, 필요한 유틸 기능을 인터페이스 내부에 포함시켜, 불필요한 객체 생성 로직이 없어진다.
ex) Comparator.comparing / List.of
private 메서드가 없었다면 log() 같은 공통 로직을
인터페이스 내부에서 중복 작성해야 했기 때문에 Java 9에서 예외적으로 허용했다.
interface Animal {
default void breathe() {
log("호흡 시작"); // 공통 로직 재사용
System.out.println("숨을 쉽니다");
log("호흡 완료");
}
default void sleep() {
log("수면 시작"); // 공통 로직 재사용
System.out.println("잠을 잡니다");
log("수면 완료");
}
// ✅ default 메서드들의 공통 로직을 묶기 위해 Java 9에서 허용
// 외부에서는 호출 불가, 인터페이스 내부에서만 사용
private void log(String msg) {
System.out.println("[LOG] " + msg);
}
}
| 추상 클래스 | 인터페이스 | |
|---|---|---|
| 의미 | ~이다 (정체성) | ~할 수 있다 (능력) |
| 키워드 | extends (상속, 확장의 느낌) | implements (상속, 구현의 느낌) |
| 다중 상속 | X | O |
| 가질 수 있는 것 | 일반 메서드, 추상 메서드, 일반 변수, 생성자 | 상수, 추상 메서드, default 메서드(일반 메서드) |
ex) Animal 클래스 예시
추상클래스 : Creature - Human, Animal / 인터페이스 : Trainable, Petable, Flyable, Swimable
abstract class Animal { // 모든 동물의 공통 본질
String name;
abstract void speak(); // 모든 동물은 소리를 낸다 ✅
}
interface Trainable { // 훈련 가능한 능력
void train(String command); // 되는 애만 구현하면 됨
}
interface Petable { // 쓰다듬을 수 있는 능력
void pet();
}
class Dog extends Animal implements Trainable, Petable {
@Override void speak() { System.out.println("멍멍!"); }
@Override public void train(String c){ System.out.println(c + " 완료!"); }
@Override public void pet() { System.out.println("꼬리를 흔듭니다"); }
}
class Cat extends Animal implements Petable { // Trainable 없음!
@Override void speak() { System.out.println("야옹!"); }
@Override public void pet() { System.out.println("그루밍합니다"); }
}
class Snake extends Animal { // 둘 다 없음!
@Override void speak() { System.out.println("쉬이익.."); }
}
train()이 추상 클래스였다면, 아래와 같이 List 같은 능력 기반의 분류가 어렵다.
// 훈련 가능한 동물만 모아서 훈련시키기
List<Trainable> trainees = new ArrayList<>();
trainees.add(new Dog("바둑이"));
trainees.add(new Dog("흰둥이"));
// trainees.add(new Cat("나비")); // ❌ Cat은 Trainable이 아님!
for (Trainable t : trainees) {
t.train("앉아");
}
Dog 객체 하나가 4가지 타입 모두로 참조 될 수 있다.
// 이 모든 것이 가능합니다
Animal a = new Dog("바둑이"); // 추상 클래스 타입
Trainable t = new Dog("바둑이"); // 인터페이스 타입
Petable p = new Dog("바둑이"); // 인터페이스 타입
Dog d = new Dog("바둑이"); // 구체 클래스 타입
타입에 따라 사용할 수 있는 메서드가 달라진다.
Dog dog = new Dog("바둑이");
// Animal 타입으로 보면 → Animal 메서드만 접근 가능
Animal a = dog;
a.speak(); // ✅
a.breathe(); // ✅
a.train("앉아"); // ❌ 컴파일 에러! Animal엔 train 없음
// Trainable 타입으로 보면 → Trainable 메서드만 접근 가능
Trainable t = dog;
t.train("앉아"); // ✅
t.speak(); // ❌ 컴파일 에러! Trainable엔 speak 없음
// Dog 타입으로 보면 → 전부 접근 가능
Dog d = dog;
d.speak(); // ✅
d.train("앉아"); // ✅
d.pet(); // ✅
그렇다면, 왜 구체 타입(Dog) 대신 인터페이스를 사용할까?
→ 구현체에 종속되지않고, “훈련가능한 것”이라는 기능에 의존하여 유지보수성이 올라간다.
// ❌ 구체 타입 사용 - Dog에만 종속됨
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog("바둑이"));
// dogs.add(new Wolf("늑대")); 훈련 가능한 Wolf가 생겨도 추가 불가!
// ✅ 인터페이스 타입 사용 - 훈련 가능한 건 뭐든 OK
List<Trainable> trainees = new ArrayList<>();
trainees.add(new Dog("바둑이"));
trainees.add(new Wolf("늑대")); // Wolf가 Trainable이면 추가 가능!
trainees.add(new Dolphin("돌고래")); // Dolphin도 Trainable이면 추가 가능!
for (Trainable t : trainees) {
t.train("앉아"); // 종류에 상관없이 동일하게 처리
}
자료구조의 List 또한 인터페이스이다. ArrayList, LinkedList는 List 인터페이스를 구현한 구체 클래스
// List 선언할 때도 인터페이스 타입 권장
List<String> list = new ArrayList<>(); // ✅ 좋은 방식
ArrayList<String> list = new ArrayList<>(); // ❌ 구체 타입에 종속
// 나중에 LinkedList로 바꿔도 선언부만 그대로 유지됨
List<String> list = new LinkedList<>(); // ✅ 선언부 변경 없음!
인터페이스를 구현하는 구현 클래스는 반드시 인터페이스의 모든 추상 메서드를 실체 메서드로 구현해야 한다.
추상 메서드를 전부 구현하면, 일반 클래스로 선언하고, 인스턴스화 가능하다.
모든 추상 메서드가 구현되지 않으면, 추상 클래스로 선언하고, 미구현 메서드는 자식 클래스에게 위임해야 한다.
interface Trainable {
void train(String command);
}
interface Petable {
void pet();
}
// ✅ 두 인터페이스의 추상 메서드 전부 구현
class Dog implements Trainable, Petable {
public void train(String command) { System.out.println(command + " 완료!"); }
public void pet() { System.out.println("꼬리를 흔듭니다!"); }
}
// ❌ pet() 빠짐 → 컴파일 에러!
class Cat implements Trainable, Petable {
public void train(String command) { System.out.println("..."); }
// pet() 빠짐!
}
// ✅ 일부만 구현하려면 abstract 선언
abstract class Cat implements Trainable, Petable {
public void train(String command) { System.out.println("..."); }
// pet()은 미구현 → Cat을 상속받는 자식 클래스에게 위임
}
이는 구현이 없는 메서드가 호출되는 상황을 컴파일 시점에 미리 막기 위한 규칙이다.
// 만약 이게 허용된다면?
class Cat implements Trainable, Petable {
public void train(String command) { ... }
// pet() 없음
public static void main(String[] args) {
Cat cat = new Cat();
cat.pet(); // 😱 구현이 없는데 호출됨 → 런타임 에러 대참사!
}
}