1. 한눈에 보는 비교 (Interface vs abstract)
인터페이스와 추상 클래스, 막상 비교하려면 헷갈리는 두 개념의 문법적 차이를 표로 정리했습니다.
| 구분 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 사용 키워드 | abstract |
interface |
| 사용 가능 변수 | 제한 없음 | static final (상수) |
| 접근 제어자 | 제한 없음 (public, private, protected, default) |
public |
| 사용 가능 메서드 | 제한 없음 | abstract, default, static, private |
| 상속 키워드 | extends |
implements |
| 다중 상속 여부 | 불가능 | 가능 클래스에 다중구현, 인터페이스 끼리 다중 상속 |
| 공통점 | 1. 추상 메서드를 가지고 있어야 한다. 2. 인스턴스화 할 수 없다. ( new 생성자 사용 불가)3. 구현체(자식 클래스)의 인스턴스를 사용해야 한다. 4. 상속/구현 받은 클래스는 추상 메서드를 반드시 구현(Override) 해야 한다. |
|
2. 인터페이스 (Interface)
인터페이스는 구현 객체가 "같은 동작을 한다"는 것을 보장하기 위한 설계도입니다.
상속 관계에 얽매이지 않고 자유롭게 기능을 장착할 수 있습니다.
핵심 특징
- 추상화의 극치: 모든 메서드는 기본적으로
public abstract입니다. (Java 8부터 default/static 메서드 가능) - 상수만 허용: 일반 변수는 가질 수 없으며, 모든 필드는
public static final상수입니다. - 다중 구현 가능: 클래스는 여러 인터페이스를 동시에 구현(implements)할 수 있어 유연합니다.
- 행위 중심 (Can-do): "무엇을 할 수 있는가"에 초점을 둡니다. 보통
~able로 네이밍합니다. (예:Runnable,Serializable)
간단 예시
// '날 수 있는' 기능 정의
interface Flyable {
void fly(); // public abstract 생략 가능
}
// '수영할 수 있는' 기능 정의
interface Swimmable {
void swim();
}
// 오리는 날 수도 있고, 수영할 수도 있음 (다중 구현)
class Duck implements Flyable, Swimmable {
@Override
public void fly() { System.out.println("오리가 날아갑니다."); }
@Override
public void swim() { System.out.println("오리가 수영합니다."); }
}
3. 추상 클래스 (Abstract Class)
추상 클래스는 하위 클래스들의 "공통점을 모아둔 미완성 클래스"입니다. 클래스 간의 명확한 계층 구조를 만들 때 사용합니다.
핵심 특징
- 단일 상속: 일반 클래스와 마찬가지로 하나만 상속(extends)받을 수 있습니다.
- 일반 멤버 보유: 추상 메서드 외에도 생성자, 일반 변수(필드), 일반 메서드를 가질 수 있습니다.
- 코드 중복 제거: 공통된 필드와 기능을 부모 클래스에 정의하여 자식 클래스의 코드 중복을 줄입니다.
- 족보 중심 (Is-a): "무엇인가"에 초점을 둡니다. 서로 연관된 클래스들의 뼈대 역할을 합니다.
간단 예시
// 동물이라는 공통 분모 (미완성 설계도)
abstract class Animal {
String name; // 상태(변수)를 가질 수 있음
// 공통 기능 (일반 메서드)
void eat() { System.out.println("밥을 먹습니다."); }
// 자식마다 다른 기능 (추상 메서드)
abstract void sound();
}
class Dog extends Animal {
@Override
void sound() { System.out.println("멍멍!"); }
}
class Cat extends Animal {
@Override
void sound() { System.out.println("야옹!"); }
}
5. 그래서 언제, 무엇을 써야 할까?
이론적인 차이점은 알겠지만, 막상 코드를 짤 때 "지금 뭘 써야 하지?" 고민될 때가 많습니다. 상황별 선택 가이드를 제시합니다.
🅰️ 추상 클래스 (Abstract Class)
- "IS-A" 관계일 때: "강아지는 동물이다", "자동차는 탈것이다"처럼 명확한 계층 구조가 필요할 때.
- 코드 중복을 줄일 때: 여러 자식 클래스에서 공통으로 사용하는 변수(필드)나 메서드(구현부)가 많을 때.
- 접근 제어자가 중요할 때:
public이외에protected나private멤버가 필요할 때.
🅱️ 인터페이스 (Interface)
- "CAN-DO" 관계일 때: "날 수 있다(Flyable)", "저장할 수 있다(Saveable)"처럼 기능을 장착해주고 싶을 때.
- 서로 다른 조상을 가질 때: 상속 계보와 상관없이 공통 기능을 부여하고 싶을 때. (예:
Bird와Airplane은 다르지만 둘 다Flyable) - 다중 구현이 필요할 때: 하나의 클래스가 여러 가지 역할을 수행해야 할 때.
6. 실전 패턴: 인터페이스와 추상 클래스의 조합 (Mixin)
사실 실무에서는 이 둘을 함께 사용하는 경우가 많습니다. 인터페이스의 "유연함(다중 상속)"과 추상 클래스의 "편리함(중복 제거)"을 모두 챙기는 강력한 설계 패턴입니다.
예시: 게임 캐릭터 만들기 (Game Character)
게임의 모든 캐릭터는 공격(Attack)하고 이동(Move)할 수 있어야 합니다. (인터페이스 역할)
하지만, 모든 캐릭터는 체력(HP)과 레벨(Level)이라는 공통 속성을 가집니다. (추상 클래스 역할)
Step 1. 인터페이스로 설계도(기능) 정의
interface Attackable {
void attack(); // 모든 캐릭터는 공격해야 함 (강제성)
}
interface Movable {
void move(); // 모든 캐릭터는 움직여야 함
}
Step 2. 추상 클래스로 공통 멤버(뼈대) 구현
인터페이스에는 변수(상태)를 담을 수 없으니, 추상 클래스를 중간에 둬서 공통 변수를 관리합니다.
// 인터페이스를 구현(implements)하면서, 공통 기능은 미리 만들어두는 추상 클래스
abstract class GameCharacter implements Attackable, Movable {
// 1. 공통 필드 (중복 제거)
protected int hp;
protected int level;
// 2. 공통 메서드 (중복 제거)
public void levelUp() {
this.level++;
System.out.println("레벨 업! 현재 레벨: " + this.level);
}
// attack()과 move()는 자식마다 다르므로 추상 메서드로 남겨둠 (구현 강제)
}
Step 3. 실제 클래스 구현 (단순화)
이제 실제 캐릭터 클래스들은 hp, level 변수를 일일이 선언할 필요 없이, 핵심 로직에만 집중하면 됩니다.
class Warrior extends GameCharacter {
@Override
public void attack() {
System.out.println("칼로 공격합니다!");
}
@Override
public void move() {
System.out.println("뛰어서 이동합니다.");
}
}
class Wizard extends GameCharacter {
@Override
public void attack() {
System.out.println("마법 불꽃 발사!");
}
@Override
public void move() {
System.out.println("텔레포트로 이동합니다.");
}
}
7. 결론: 다형성(Polymorphism)의 중요성
우리가 인터페이스와 추상 클래스를 조합해서 복잡하게 설계한 진짜 이유는 바로 "다형성을 200% 활용하기 위해서"입니다.
이제 우리는 전사(Warrior)든 마법사(Wizard)든 상관없이, 모두 GameCharacter라는 하나의 타입으로 묶어서 관리할 수 있습니다.
다형성 활용 코드 예시
public class GameMain {
public static void main(String[] args) {
// 1. 다형성: 서로 다른 객체를 '부모 타입'으로 묶음
List<GameCharacter> party = new ArrayList<>();
party.add(new Warrior());
party.add(new Wizard());
// 2. 일괄 처리: 구체적인 타입(전사인지 마법사인지)을 몰라도 됨
for (GameCharacter character : party) {
character.move(); // 공통 기능 (추상 클래스에서 상속)
character.attack(); // 개별 기능 (오버라이딩된 메서드 실행)
// 전사는 "칼 공격", 마법사는 "마법 공격"이 자동으로 나감!
}
}
}
이 패턴(Interface + Abstract)의 장점 요약
- - 설계의 표준화 (Interface): "모든 캐릭터는 반드시 공격해야 한다"는 규칙을 강제하여, 개발자가 실수로 기능을 누락하는 것을 방지합니다.
- - 코드 중복 제거 (Abstract Class): 체력(HP), 레벨(Level) 처럼 모든 캐릭터가 갖는 공통 코드를 부모 클래스 한곳에서만 관리하면 되므로 유지보수가 매우 쉬워집니다.
- - 확장성 (Polymorphism): 나중에
Archer(궁수)클래스가 추가되어도, 기존의GameMain코드는 한 줄도 수정할 필요가 없습니다. 그냥 리스트에 추가만 하면 됩니다. (OCP 원칙 준수)
자바의 꽃은 객체지향이고, 객체지향의 완성은 다형성입니다.
인터페이스로 "큰 그림(설계)"을 그리고, 추상 클래스로 "공통 분모(구현)"를 묶어주는 이 패턴을 익혀두면, 좋을 것 같습니다.
📚 Reference & Source
이 포스팅은 "요즘 공부 도와주는 갓 인파"님의 블로그 내용을 바탕으로 정리했습니다.
더 깊이 있는 내용과 예제는 아래 원문에서 확인하실 수 있습니다.
'Language > Java' 카테고리의 다른 글
| [JAVA] Stack 구현 & Stack Class의 문제점 (feat. Deque) (0) | 2025.12.19 |
|---|---|
| [JAVA] Collection Framework & Collections Class _ Part 1 (0) | 2025.12.16 |
| [JAVA] Getter/Setter 이중성 & 문제점 Refactoring (1) | 2025.12.12 |
| [JAVA] 접근제한자 & 캡슐화 (0) | 2025.12.12 |
| [JAVA] 객체 지향 프로그래밍 4대 원칙 (0) | 2025.12.12 |
