자바에서 객체 지향 프로그래밍을 제대로 활용하기 위해 꼭 이해하고 넘어가야 할 두 가지 중요한 개념이 있습니다. 바로 추상 클래스(Abstract Class) 와 인터페이스(Interface) 입니다.
추상 클래스는 미완성 설계도입니다.
여러 클래스에 공통적으로 사용되는 로직을 정의하되, 일부는 구현하지 않고 자손 클래스에서 반드시 구현하도록 강제할 수 있습니다.
즉, 클래스 간의 공통 로직은 미리 작성하고,
구체적인 동작은 자식 클래스에서 작성하게 하는 '틀(template)'입니다.
| 항목 | 설명 |
|---|---|
| 정의 방식 | abstract class 클래스명 |
| 인스턴스 생성 | ❌ 직접 생성 불가능 |
| 상속 여부 | ✅ 반드시 상속을 통해 사용 |
| 추상 메서드 | 하나 이상 포함 가능 |
| 목적 | 공통 기능 제공 + 강제 구현 유도 |
| 키워드 | abstract 사용 |
추상 클래스의 핵심은 바로 추상 메서드입니다.
abstract class Animal {
abstract void sound(); // 추상 메서드
}
메서드 선언부만 존재하고 구현부(body)는 없습니다.
상속받는 자식 클래스는 반드시 이 메서드를 오버라이딩해서 구현해야 합니다.
오버라이딩하지 않으면, 자식 클래스도 abstract로 선언해야 합니다.
추상 클래스를 사용해서 여러 종류의 유닛에게 공통적인 move() 동작을 강제하는 예제입니다.
public class Main {
public static void main(String[] args) {
Unit[] group = { new Marine(), new Tank(), new Dropship() };
for (int i = 0; i < group.length; i++)
group[i].move(100, 200); // 모두 move 동작이 다르게 정의됨!
}
}
추상 클래스와 자식 클래스들
abstract class Unit {
int x, y;
abstract void move(int x, int y); // 자식 클래스에서 구현해야 함
void stop() {
System.out.println("Unit stopped");
}
}
class Marine extends Unit {
void move(int x, int y) {
System.out.println("Marine[x=" + x + ",y=" + y + "]");
}
void stimPack() {}
}
class Tank extends Unit {
void move(int x, int y) {
System.out.println("Tank[x=" + x + ",y=" + y + "]");
}
void changeMode() {}
}
class Dropship extends Unit {
void move(int x, int y) {
System.out.println("Dropship[x=" + x + ",y=" + y + "]");
}
void load() {}
void unload() {}
}
공통된 행동은 추상 클래스에 미리 구현해두고, 각 클래스별로 달라야 하는 행동은 추상 메서드로 선언해두면 좋습니다.
프레임워크나 템플릿 메서드 패턴에서 많이 사용됩니다.
| 사용 목적 | 예시 |
|---|---|
| 여러 클래스가 공통적으로 사용하는 필드/메서드 정의 | 모든 동물이 eat(), sleep()을 가지고 있음 |
| 공통 로직은 부모에, 구체 로직은 자식에게 맡기고 싶을 때 | move()는 공통이지만, 각 유닛의 움직임 방식은 다름 |
| 상속을 강제하고 싶은 경우 | 모든 유닛은 반드시 move()를 구현해야 함 |
자바에서 인터페이스(interface)는 클래스보다 더 높은 수준의 추상화를 제공하는 일종의 "기본 설계도"입니다.
자바에서 interface 키워드로 선언되는 일종의 추상 클래스입니다.
그 자체로는 완성된 기능이 아니며, 다른 클래스가 해당 인터페이스를 구현함으로써 완성됩니다.
일반적으로 표준화된 명세, 다형성을 통한 유연한 코드 설계, 유지보수성 향상 등을 목적으로 사용됩니다.
interface 인터페이스이름 {}
class 클래스이름 implements 인터페이스이름 {
// 추상 메서드 구현 필수
}
자바 인터페이스는 다음과 같은 특징과 제한을 가집니다:
| 항목 | 설명 |
|---|---|
| 구성 요소 | 추상 메서드, 상수만 가능 (JDK 1.8부터는 default/static 메서드도 허용) |
| 메서드 | 모든 메서드는 public abstract (생략 가능) |
| 변수 | 모든 변수는 public static final (생략 가능) |
| 인스턴스 생성 | 불가능 |
| 제어자 | 대부분 생략 가능하며 컴파일러가 자동으로 추가 |
인터페이스는 오직 다른 인터페이스만 상속 가능하며, 다중 상속이 허용됩니다.
Object 클래스와 같은 최고 조상이 없습니다.
interface Movable {
void move(int x, int y);
}
interface Attackable {
void attack(Unit u);
}
interface Fightable extends Movable, Attackable {}
클래스가 인터페이스를 구현하려면 implements 키워드를 사용합니다.
클래스는 확장한다는 의미의 키워드 extends 를 사용하지만 인터페이스는 구현한다는 의미의 키워드 implements 를 사용합니다.
모든 추상 메서드를 반드시 구현해야 합니다.
class Fighter implements Fightable {
public void move(int x, int y) { /* 구현 */ }
public void attack(Unit u) { /* 구현 */ }
}
일부만 구현할 경우 추상 클래스로 선언해야 합니다.
abstract class Fighter implements Fightable {
public void move(int x, int y) { /* 구현 */ }
}
클래스 상속(extends)과 인터페이스 구현(implements)은 동시에 가능합니다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { /* 구현 */ }
public void attack(Unit u) { /* 구현 */ }
}
인터페이스 타입의 참조 변수로 구현체를 참조할 수 있어 유연한 코드 구현이 가능합니다.
Fightable f = new Fighter(); // 다형성
인터페이스는 매개변수 타입으로도 사용 가능합니다.
메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 합니다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { }
public void attack(Fightable f) { }
}
Fighter f = new Fighter();
f.attack(new Fighter());
리턴 타입으로 사용하면 다양한 구현체를 리턴할 수 있습니다.
리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미합니다.
Fightable createFighter() {
return new Fighter();
}
인터페이스에는 추상 메서드만 선언할 수 있는데, JDK1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었습니다.
static 메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 별 문제없이 인터페이스에 추가할 수 있습니다.
interface MyInterface {
static void staticMethod() {
System.out.println("static method");
}
}
조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 합니다.
디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 됩니다.
디폴트 메서드는 앞에 키워드 default 를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통 {}이 있어야 합니다. 디폴트 메서드 역시 접근 제어자가 public 이며, 생략 가능합니다.
interface MyInterface {
default void method1() {
System.out.println("method1() in Myinterface");
}
default void method2() {
System.out.println("method2() in Myinterface");
}
}
인터페이스에 디폴트 메서드를 추가하면 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것입니다.
여러 인터페이스의 디폴트 메서드 간의 충돌 : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
디폴트 메서드와 조상 클래스의 메서드 간의 충돌 : 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
class Main {
public static void main(String[] args) {
Child3 c = new Child3();
c.method1(); // method1() in Child3
c.method2(); // method2() in Parent3
MyInterface.staticMethod(); // staticMethod() in Myinterface
MyInterface2.staticMethod(); // staticMethod() in Myinterface2
}
}
class Child3 extends Parent3 implements MyInterface, MyInterface2 {
public void method1() {
System.out.println("method1() in Child3");
}
}
class Parent3 {
public void method2() {
System.out.println("method2() in Parent3");
}
}
interface MyInterface {
default void method1() {
System.out.println("method1() in Myinterface");
}
default void method2() {
System.out.println("method2() in Myinterface");
}
static void staticMethod() {
System.out.println("staticMethod() in Myinterface");
}
}
interface MyInterface2 {
default void method1() {
System.out.println("method1() in Myinterface2");
}
static void staticMethod() {
System.out.println("staticMethod() in Myinterface2");
}
}
| 항목 | 인터페이스 (interface) | 추상 클래스 (abstract class) |
|---|---|---|
| 목적 | 기능 명세 (무엇을 할 수 있는가) 정의 | 공통 로직을 상속을 통해 재사용하기 위해 사용 |
| 인스턴스 생성 | 불가능 | 불가능 |
| 키워드 | interface | abstract class |
| 상속/구현 | implements 키워드로 구현 | extends 키워드로 상속 |
| 메서드 구성 | 모든 메서드는 기본적으로 abstract (JDK 1.8 이후 default, static 허용) | 추상 메서드 + 일반 메서드 모두 가능 |
| 변수 구성 | 상수만 가능 (public static final, 생략 가능) | 멤버 변수 선언 가능, 접근제어자 사용 가능 |
| 다중 상속 | 다중 구현 가능 | 다중 상속 불가 |
| 사용 용도 | 서로 관련 없는 클래스들 간의 공통된 동작 정의 | 비슷한 객체들의 공통 기능을 묶을 때 사용 |
| 결합도 | 느슨한 결합 (low coupling) | 클래스 간 강한 결합 가능 |
| 예시 | Comparable, Runnable | HttpServlet, AbstractList 등 |
✅ 정리:
인터페이스는 “무엇을 할 수 있는가”에 중점
추상 클래스는 “어떻게 동작할 것인가”에 공통 구현 포함
추상 클래스는 직접 인스턴스를 만들 수 없고, 자식 클래스에서 상속하여 구현해야 함.
추상 메서드는 선언만 하고 구현하지 않으며, 자식 클래스에서 반드시 구현해야 함.
자바에서는 공통 로직을 효율적으로 관리하고 다형성을 유도할 때 추상 클래스가 자주 쓰임.
인터페이스는 인스턴스를 만들 수 없고, 클래스에서 implements 키워드로 구현해야 함.
인터페이스의 모든 메서드는 선언만 존재하며, 구현 클래스에서 반드시 구현해야 함.
(단, JDK 1.8 이후부터 default, static 메서드는 구현 가능)
자바에서는 기능 명세를 정의하고, 다형성과 느슨한 결합을 유도할 때 인터페이스가 자주 사용됨.
여러 인터페이스를 동시에 구현 가능하여, 다중 상속의 효과를 낼 수 있음.
💡 인터페이스는 “무엇을 할 수 있는가”를 명확히 정의하고, 그 구현은 클래스에 맡기는 유연한 구조를 만든다.
자바에서 추상 클래스와 인터페이스는 "설계"라는 측면에서 매우 중요한 개념입니다.
추상 클래스는 공통된 기능을 묶고 재사용하기 위한 기반이 되고,
인터페이스는 클래스 간의 규약을 정의하고 느슨한 결합을 유도합니다.
현업에서는 이 두 개념을 혼용하거나 상황에 따라 적절히 선택해서 사용합니다.
예를 들어, 공통 로직이 있다면 추상 클래스를,
다양한 구현체가 동일한 기능을 제공해야 한다면 인터페이스를 선택하는 식이죠.