이번 장의 한 문장은 이거예요 — "구현은 자식에게 맡기고, 있어야 할 기능만 강제한다." 완성된 부품이 아니라 "이런 기능은 반드시 있어야 해"라고 규칙만 정해 두는 설계도를 배웁니다. 천천히 따라오면 어렵지 않아요.
new로 직접 만들 수 없는지 설명할 수 있어요.interface로 선언하고 implements로 구현할 수 있어요.지금까지 배운 클래스는 완성된 부품이었어요. new로 찍어내면 바로 쓸 수 있었죠. 그런데 팀으로 프로그램을 만들다 보면 이런 상황이 자주 생겨요 — "이 부품에는 이런 기능이 꼭 있어야 하는데, 구체적으로 어떻게 만들지는 담당자마다 다르다."
이렇게 "규칙(있어야 할 기능)"과 "구현(실제 내용)"을 분리하면, 코드를 쓰는 쪽은 규칙만 믿고 프로그램을 짤 수 있어요. 예를 들어 "이 물체는 draw()를 가진다"는 것만 약속되면, 그게 원이든 사각형이든 상관없이 draw()를 호출하면 되죠. (이건 CH 12 다형성과 짝을 이루는 이야기예요.)
추상 메서드(abstract method)는 이름·매개변수·반환타입만 선언하고, 본문 { }이 없는 메서드예요. "이런 기능이 있어야 한다"는 선언만 하고, 실제로 어떻게 할지는 비워 둔 거죠. 세미콜론 ;으로 끝냅니다.
이런 추상 메서드를 하나라도 가진 클래스는 반드시 추상 클래스(abstract class)여야 해요. 클래스 앞에도 abstract를 붙입니다.
// abstract 붙은 추상 클래스 — "동물"이라는 설계도
abstract class Animal {
String name; // ✅ 일반 필드 가질 수 있음
Animal(String name) { // ✅ 생성자도 가질 수 있음 (자식이 씀)
this.name = name;
}
// 추상 메서드 — 본문이 없고 세미콜론으로 끝! "울음소리는 있어야 한다"만 강제
abstract void sound();
// ✅ 일반(구현된) 메서드도 함께 가질 수 있음
void breathe() {
System.out.println(name + "이(가) 숨을 쉰다.");
}
}
추상 클래스의 가장 중요한 특징 두 가지예요.
Animal a = new Animal("동물"); ❌ → 컴파일 에러! (sound()가 비어 있으니 실행할 수 없죠.)
그럼 어떻게 쓰냐고요? 상속받은 자식을 만들어서, 자식으로 객체를 생성해요. 부모 타입 변수에 담는 건 됩니다(다형성): Animal a = new Dog("바둑이"); ✅
extends로 상속받은 자식은, 부모의 모든 추상 메서드를 오버라이드(구현)해야 해요. 그래야 비로소 "완성된 클래스"가 되어 new로 찍어낼 수 있어요.
만약 하나라도 구현하지 않으면? 그 자식도 아직 미완성이므로 자식도 abstract로 선언해야 하고, 아니면 컴파일 에러가 납니다.
// Animal을 상속받은 자식 — 추상 메서드 sound()를 반드시 구현
class Dog extends Animal {
Dog(String name) {
super(name); // 부모 생성자 호출
}
@Override
void sound() { // ✅ 추상 메서드를 구현(오버라이드)했으니 완성됨
System.out.println(name + ": 멍멍!");
}
}
class Cat extends Animal {
Cat(String name) { super(name); }
@Override
void sound() { // ✅ 고양이는 자기 방식대로 구현
System.out.println(name + ": 야옹~");
}
}
public class Main {
public static void main(String[] args) {
// Animal a = new Animal("x"); // ❌ 추상 클래스는 직접 new 불가!
Animal a = new Dog("바둑이"); // ✅ 자식으로 생성, 부모 타입에 담음
a.sound(); // 바둑이: 멍멍!
a.breathe(); // 바둑이이(가) 숨을 쉰다.
Animal b = new Cat("나비");
b.sound(); // 나비: 야옹~
}
}
new 직접 불가, 자식이 빈 칸을 채워야 완성돼요.인터페이스(interface)는 class 대신 interface 키워드로 만들어요. 전통적으로 인터페이스 안의 메서드는 전부 추상 메서드(본문 없음)이고, 필드는 상수(public static final)만 가질 수 있어요. 한마디로 "할 수 있어야 하는 기능 목록"이에요.
implements한 클래스가 그 규칙을 채워 구현해요. 콘센트 규격처럼 "이 모양에 맞추면 꽂힌다"는 약속이라고 보면 돼요.implements).// interface 키워드로 선언 — "날 수 있다"는 능력의 규칙
interface Flyable {
void fly(); // 자동으로 public abstract — 본문 없음
}
// 클래스는 implements 로 인터페이스를 구현한다
class Bird implements Flyable {
@Override
public void fly() { // ✅ 반드시 구현, public 이어야 함
System.out.println("새가 날개짓으로 난다.");
}
}
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("비행기가 엔진으로 난다.");
}
}
implements — 상속의 extends와 헷갈리지 마세요! 인터페이스는 implements로 "구현"합니다.public abstract라, 구현할 때 public을 붙여야 합니다.)public static final 상수가 돼요. 일반 인스턴스 변수는 못 가져요.
그래서 인터페이스는 "이 물건은 fly() 를 할 수 있다"처럼 능력(can-do)을 규정하는 데 잘 어울려요.
interface Config {
int MAX = 100; // 자동으로 public static final int MAX = 100;
// int count; // ❌ 상수라 값이 반드시 있어야 함 (일반 필드 불가)
}
public class Main {
public static void main(String[] args) {
Flyable f = new Bird(); // ✅ 인터페이스 타입에 구현체를 담음(다형성)
f.fly(); // 새가 날개짓으로 난다.
f = new Airplane();
f.fly(); // 비행기가 엔진으로 난다.
System.out.println(Config.MAX); // 100 (상수는 이렇게 접근)
}
}
자바에서 클래스는 부모를 딱 하나만 상속할 수 있어요(단일 상속). class A extends B extends C처럼 부모를 여러 개 두는 건 불가능해요. 부모가 여럿이면 "같은 이름의 메서드가 충돌"하는 등 복잡한 문제가 생기기 때문이에요.
하지만 인터페이스는 여러 개를 동시에 implements 할 수 있어요. 인터페이스는 "규칙 목록"일 뿐 구현 충돌이 (전통적으로) 없으니까요. 그래서 인터페이스가 자바의 다중 상속을 대체합니다.
// 규칙(능력) 인터페이스 두 개
interface Swimmable { void swim(); }
interface Flyable { void fly(); }
class Animal { String name; } // 평범한 부모 클래스
// extends 는 하나, implements 는 여러 개! (extends 를 먼저 씀)
class Duck extends Animal implements Swimmable, Flyable {
@Override public void swim() { System.out.println("오리가 헤엄친다."); }
@Override public void fly() { System.out.println("오리가 난다."); }
}
public class Main {
public static void main(String[] args) {
Duck d = new Duck();
d.swim(); // 오리가 헤엄친다.
d.fly(); // 오리가 난다.
Swimmable s = d; // ✅ 능력 타입으로도 볼 수 있음
s.swim();
}
}
extends를 먼저, implements를 나중에 씁니다.class Duck extends Animal implements Swimmable, Flyable { ... },로 여러 개를 나열할 수 있어요. 상속(extends)은 클래스니까 딱 하나만!
💡 헷갈리면: "클래스는 extends 하나, 인터페이스는 implements 여러 개." 이 한 줄만 기억하세요.
원래 인터페이스의 메서드는 전부 추상(본문 없음)이었어요. 그런데 Java 8부터 인터페이스 안에 default 키워드를 붙여 기본 구현(본문 있는 메서드)을 제공할 수 있게 됐어요.
interface Greetable {
void hello(); // 여전히 추상 — 구현 강제
// Java 8+ : default 메서드는 본문을 가진다 (구현체가 없어도 그냥 씀)
default void bye() {
System.out.println("안녕히 가세요! (기본 인사)");
}
// static 메서드도 가능 — 인터페이스 이름으로 직접 호출
static void info() {
System.out.println("Greetable 인터페이스입니다.");
}
}
class Person implements Greetable {
@Override
public void hello() { // 추상 메서드는 반드시 구현
System.out.println("안녕하세요!");
}
// bye() 는 구현 안 해도 됨 → 인터페이스의 default 구현을 그대로 사용
}
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello(); // 안녕하세요!
p.bye(); // 안녕히 가세요! (기본 인사) ← default 사용
Greetable.info(); // static 은 인터페이스 이름으로 호출
}
}
forEach 같은 기능을 default로 추가할 수 있었던 것도 이 덕분이에요.
default = 인터페이스가 주는 "기본 구현(있어도 되고 오버라이드해도 됨)". 도입 버전은 Java 8. static 메서드는 인터페이스 이름으로 직접 호출하는 유틸 성격이에요.| 구분 | 추상 클래스 (abstract class) | 인터페이스 (interface) |
|---|---|---|
| 키워드 | abstract class | interface |
| 사용 키워드 | extends (상속) | implements (구현) |
| 개수 제한 | 하나만 상속 (단일 상속) | 여러 개 구현 가능 (다중) |
| 일반 필드 | ✅ 가질 수 있음 | ❌ 상수(public static final)만 |
| 구현된 메서드 | ✅ 일반 메서드 가능 | △ default/static만 (Java 8+) |
| 생성자 | ✅ 가짐 (자식이 super로 씀) | ❌ 없음 |
| 의미(관점) | is-a — "~는 ~의 한 종류다" | can-do — "~할 수 있다" |
Dog·Cat은 모두 Animal의 한 종류이고 name 필드·breathe()를 공유.Bird·Airplane은 종류가 전혀 다르지만 둘 다 fly() 할 수 있음.
💡 애매하면 인터페이스부터 고려하세요. 다중 구현이 되어 더 유연하고, default 메서드로 기본 구현도 줄 수 있어서 현대 자바에서 선호되는 편이에요.
class Duck extends Animal implements Swimmable, Flyable처럼요).@FunctionalInterface // 추상 메서드가 하나임을 표시(선택)
interface Calc {
int apply(int a, int b); // 추상 메서드 하나
}
// 지금은 이렇게 구현하지만...
Calc add = new Calc() {
public int apply(int a, int b) { return a + b; }
};
// 나중엔 람다로 이렇게: Calc add = (a, b) -> a + b; (고급 챕터에서!)
지금은 "추상 메서드 하나짜리 인터페이스는 특별하게 쓰인다" 정도만 기억해 두면 충분해요. default·static 메서드는 몇 개 있어도 함수형 인터페이스 자격에 영향 없어요(추상 메서드 개수만 셈).
;으로 끝). 하나라도 있으면 그 클래스는 추상 클래스(abstract).new로 직접 생성 불가. 상속받은 자식이 추상 메서드를 반드시 구현해야 완성돼요. 안 하면 자식도 abstract거나 컴파일 에러.interface)는 implements로 구현. 메서드는 (전통적으로) 추상, 필드는 상수(public static final)만.extends 하나)이지만 인터페이스는 여러 개 구현(implements 다중) → 다중 상속 대체.