자바 백엔드 입문 3편. 인터페이스·구현체·다형성·추상 클래스 비교를 USB 포트 비유로 풀고, 왜 이게 Spring DI(의존성 주입)의 토대가 되는지까지 한 흐름으로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 3편이에요. 2편에서 클래스로 객체를 찍어낸다는 그림을 잡았다면, 3편은 한 단계 더 추상적인 개념 — 인터페이스와 다형성을 잡습니다. Spring을 본격적으로 다루는 5편부터 "왜 Spring이 구현체 대신 인터페이스를 주입하는가" 가 자주 등장하는데, 그 의문이 풀리는 글이에요.
인터페이스가 어렵게 들리는 이유
처음 "인터페이스" 라는 단어를 들으면 머리에 그림이 안 떠올라요. 두 가지가 원인이에요.
첫째, 단어 자체가 추상적이거든요. 클래스는 "붕어빵 틀" 처럼 비유가 쉬운데, 인터페이스는 "규격" 이라는 단어가 와 닿지 않아요.
둘째, "어차피 클래스로 만들면 되는데 왜 굳이 인터페이스를?" 가 안 보입니다. 이 질문은 "교체 가능성" 이라는 한 단어로 풀려요 — 같은 규격을 따른다면 안에 들어가는 실물이 무엇이든 바꿔 끼울 수 있다는 뉘앙스.
이 글에서는 인터페이스 = USB 포트 규격, 구현체 = USB 포트에 꽂히는 장치 비유로 풀어 갑니다. 끝까지 따라오시면 "Spring이 왜 인터페이스를 그렇게 좋아하는지" 가 한 번에 박혀요.
인터페이스 = USB 포트 규격
인터페이스(Interface) 는 "이런 메서드들을 가져야 한다" 는 계약서예요. 안의 구현은 비어 있고, 메서드 이름·매개변수·반환 타입만 적혀 있어요.
public interface Weapon {
void attack();
int getDamage();
}
이게 인터페이스예요. "이 인터페이스를 따르는 무기는 attack() 과 getDamage() 를 반드시 가져야 한다" 는 규격만 정의했고, "실제로 어떻게 공격하는지" 는 비어 있어요.
USB 포트 비유로 풀어볼게요. USB 포트는 인터페이스예요. "이런 모양의 단자와 이런 핀 배열을 갖는다" 는 규격만 정의했지, "이 포트에 무엇을 꽂으면 무엇이 일어난다" 는 안 정해져 있어요. 마우스를 꽂으면 마우스 동작, 키보드를 꽂으면 키보드 동작, USB 메모리를 꽂으면 저장 매체 동작 — 꽂히는 장치에 따라 결과가 달라지는데, USB 포트 자체는 그걸 모릅니다.
구현체 = USB 포트에 꽂히는 실제 장치들
인터페이스를 따르는 클래스를 구현체(Implementation) 라고 불러요. implements 키워드로 "나 이 인터페이스 따른다" 고 선언해요.
public class Sword implements Weapon {
@Override
public void attack() {
System.out.println("칼을 휘두른다");
}
@Override
public int getDamage() {
return 20;
}
}
public class Bow implements Weapon {
@Override
public void attack() {
System.out.println("화살을 쏜다");
}
@Override
public int getDamage() {
return 15;
}
}
Sword도 Bow도 둘 다 Weapon 인터페이스를 따라요. 둘 다 attack() 과 getDamage() 를 가지지만, 안의 동작은 완전히 달라요. USB 포트로 치면 "마우스도 키보드도 같은 USB 단자에 꽂히지만, 안에서 하는 일은 완전히 다른" 그림이에요.
여기서 시험 함정이 하나 있어요. "인터페이스는 메서드 본문을 가질 수 없다" 가 자바 7까지의 룰이었어요. 자바 8부터는 default 메서드라는 게 추가되어 인터페이스 안에도 메서드 본문을 박을 수 있게 됐어요. 시험·면접에서 "인터페이스에 절대 메서드 본문이 없다" 가 보기로 나오면 X — 자바 8 이후 OK라는 게 정답이에요.
한 줄 정리 — 인터페이스 = USB 포트 규격, 구현체(implements) = 그 규격을 따르는 실제 장치. implements 키워드로 "나 이 인터페이스 따른다" 선언.
다형성 — 한 변수에 마우스도 키보드도 들어간다
여기서 자바 객체지향의 핵심 개념 하나가 등장해요. 다형성(Polymorphism). "하나의 변수가 여러 다른 객체를 담을 수 있다" 는 능력이에요.
Weapon w; // 인터페이스 타입 변수
w = new Sword();
w.attack(); // "칼을 휘두른다"
w = new Bow();
w.attack(); // "화살을 쏜다"
w 의 타입은 Weapon (인터페이스)인데, 실제로 들어가는 객체는 Sword 일 수도 Bow 일 수도 있어요. "USB 포트 변수에 마우스를 꽂든 키보드를 꽂든 둘 다 OK" 인 셈이에요. 변수 자체는 어느 무기가 들어 있는지 신경 안 써요 — 그저 Weapon 인터페이스가 약속한 attack() 만 호출할 수 있으면 충분.
다형성의 진짜 힘은 함수 매개변수에서 빛나요.
public void useWeapon(Weapon w) { // 인터페이스를 매개변수로
w.attack();
}
useWeapon(new Sword()); // OK
useWeapon(new Bow()); // OK
useWeapon(new Hammer()); // 새로운 무기를 추가해도 OK — useWeapon 코드 안 고침
새로운 무기를 만들 때 useWeapon 함수를 한 줄도 안 고쳐도 돼요. 인터페이스만 따르면 자동으로 끼워서 동작합니다. 이 "기존 코드 안 건드리고 새 구현체 끼워 넣는" 능력이 객체지향의 가장 큰 무기예요.
Open-Closed Principle — "확장에는 열려 있고 수정에는 닫혀 있다". 새 기능을 추가할 때 기존 코드 수정 없이 새 구현체만 만들면 끝나는 설계 원칙이에요. 다형성이 이 원칙을 가능하게 합니다.
인터페이스 vs 추상 클래스 — 비슷한데 다른 둘
자바에는 인터페이스와 비슷한 추상 클래스(Abstract Class) 라는 게 또 있어요. 둘 다 "미완성 설계도" 라는 점은 같은데, 결정적으로 다른 부분이 있어요.
| 구분 | 인터페이스 | 추상 클래스 |
|---|---|---|
| 키워드 | interface + implements | abstract class + extends |
| 다중 상속 | 여러 개 동시 implements OK | 한 개만 extends 가능 |
| 필드 | 상수(public static final)만 | 일반 필드 가능 |
| 메서드 본문 | 자바 8+ default 메서드만 | 일반 메서드 본문 가능 |
| 비유 | "규격" — 무엇을 할 수 있는가 | "미완성 부모 클래스" — 공통 코드 + 일부 빈 구멍 |
여기서 시험 함정 정말 자주 나와요. "인터페이스는 다중 implements 가능, 클래스 상속은 단일 extends만 가능". 자바가 다중 상속을 막은 이유는 "다이아몬드 문제" — 두 부모 클래스에 같은 메서드가 있으면 어느 걸 따라야 하는가의 충돌 — 때문이에요. 인터페이스는 메서드 본문이 비어 있어서(주로) 이 문제가 없어 다중 implements가 허용됩니다.
언제 인터페이스 vs 언제 추상 클래스? 단순한 룰 한 줄 — 공통 코드를 공유해야 하면 추상 클래스, "이걸 할 수 있다" 만 정의하면 인터페이스. 90% 케이스에서는 인터페이스가 더 자주 쓰여요.
왜 인터페이스가 Spring DI의 토대인가
5편부터 본격적으로 다룰 Spring의 의존성 주입(DI, Dependency Injection) 미리보기 한 줄. Spring이 가장 잘하는 일은 "인터페이스 변수에 적절한 구현체를 자동으로 끼워 넣어주는" 거예요.
@Service
public class OrderService {
private final PaymentGateway paymentGateway; // 인터페이스 타입
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void placeOrder() {
paymentGateway.pay(10000);
}
}
PaymentGateway 는 인터페이스예요. 구현체는 KakaoPayGateway 일 수도 TossPayGateway 일 수도 있어요. Spring이 시작 시점에 "PaymentGateway 자리에 어떤 구현체를 끼워 넣을지" 알아서 정해서 주입해줍니다. 그래서 OrderService 는 "카카오페이를 쓸지 토스를 쓸지" 신경 안 써요. 그냥 "PaymentGateway 라는 인터페이스 따르는 무언가가 들어 있구나" 만 알면 됩니다.
이게 다형성의 실전 활용이에요. 인터페이스가 박혀 있어야 "실제 결제 게이트웨이를 카카오페이에서 토스로 바꿔도 OrderService 한 줄도 안 고쳐도 되는" 마법이 가능해집니다.
한 줄 정리 — 인터페이스 + 다형성 + DI = "기존 코드 안 건드리고 구현체만 바꿔 끼우는" Spring의 핵심 무기.
시험 직전 한 번 더 — 인터페이스·다형성 입문자가 매번 헷갈리는 것
- 인터페이스 = USB 포트 규격, 구현체 = 그 포트에 꽂히는 실제 장치
interface키워드로 선언,implements키워드로 구현- 한 클래스가 여러 인터페이스 동시 구현 가능 (다중 implements)
- 클래스 상속은 단일 extends만 가능 (다이아몬드 문제 회피)
- 인터페이스 안의 메서드는 자동으로
public abstract(생략 가능) - 인터페이스 안의 필드는 자동으로
public static final(상수만 가능) - 자바 8부터 인터페이스에
default메서드 본문 박을 수 있음 - "인터페이스는 무조건 메서드 본문 없다" 는 자바 7까지 룰 — 자바 8 이후 X
- 다형성(Polymorphism) = 하나의 변수가 여러 다른 객체를 담을 수 있는 능력
- 다형성의 핵심 활용 = 함수 매개변수에 인터페이스 박기
- OCP(Open-Closed Principle) = 새 구현체 추가 시 기존 코드 안 고치는 설계
- 추상 클래스(
abstract class) = 인터페이스와 비슷하지만 일반 메서드·필드 가능 - 추상 클래스는 단일 상속만 가능, 인터페이스는 다중 구현 가능
- 공통 코드 공유 = 추상 클래스, "무엇을 할 수 있다" 만 = 인터페이스
- 90% 케이스에서 추상 클래스보다 인터페이스가 더 자주 쓰임
@Override어노테이션 = "이 메서드는 부모 인터페이스/클래스의 메서드를 구현 중" 표시@Override박으면 컴파일러가 메서드 시그니처 매칭 검증해줘 오타 방지- Spring DI = 인터페이스 타입 필드에 자동으로 구현체 주입
- 인터페이스 박는 진짜 이유 = "구현체 교체 가능성". 한 마디로 "갈아 끼울 수 있게"
@Service·@Component·@Repository모두 Spring이 "이 클래스로 객체 찍어내라" 표시
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: