인터페이스 vs 추상클래스¶
PHASE 2 | Java 핵심 — 객체지향
키워드:interface,abstract class,implements,extends,default 메서드,is-a,can-do,다중 구현,단일 상속,템플릿 메서드 패턴
인터페이스 (Interface)¶
구현을 강제하는 계약서. 무엇을 해야 하는지만 정의하고, 어떻게 할지는 구현체가 결정한다.
public interface Flyable {
void fly(); // 구현 강제
default void land() { // 기본 구현 제공 (선택적 오버라이딩)
System.out.println("착륙한다");
}
}
public class Bird implements Flyable {
public void fly() {
System.out.println("날개로 난다");
}
// land()는 오버라이딩 안 해도 동작함
}
인터페이스 필드는 상수만 가능¶
인터페이스는 객체를 만들 수 없어서 인스턴스 필드가 의미가 없다.
따라서 모든 필드는 자동으로 public static final (상수)로 처리된다.
public interface Constants {
int MAX_SIZE = 100;
// 위 코드는 아래와 동일
// public static final int MAX_SIZE = 100;
}
default 메서드란?¶
Java 8 이전에는 인터페이스에 구현 코드를 넣을 수 없었다.
인터페이스에 메서드를 추가하면 모든 구현체가 깨지는 문제가 있었기 때문에 default 메서드가 도입됐다.
public interface Flyable {
void fly();
default void land() { // 기본 구현 제공
System.out.println("착륙한다");
}
}
구현체가
default메서드를 오버라이딩하지 않으면 기본 동작이 실행된다.
오버라이딩하면 자신의 구현으로 덮어쓸 수 있다.
실무에서 인터페이스 사용 예시¶
Spring에서 Service 계층을 인터페이스로 정의하고 구현체를 주입받는 패턴이 일반적이다.
public interface UserService {
void join(String username);
}
@Service
public class UserServiceImpl implements UserService {
public void join(String username) {
// 실제 구현
}
}
@RestController
public class UserController {
private final UserService userService; // 구현체가 뭔지 몰라도 됨
public UserController(UserService userService) {
this.userService = userService;
}
}
Controller는
UserService인터페이스만 알면 된다.
구현체가 바뀌어도 Controller 코드는 수정할 필요가 없다.
테스트 시 Mock 구현체로 쉽게 교체할 수 있어 테스트가 편해진다.
추상클래스 (Abstract Class)¶
미완성 클래스. 공통 구현은 부모에 두고, 일부만 자식이 구현하도록 강제한다.
public abstract class Animal {
private String name; // 공통 필드
public Animal(String name) { // 생성자 (자식이 super()로 호출)
this.name = name;
}
public void eat() { // 공통 구현
System.out.println("먹는다");
}
public abstract void sound(); // 구현 강제
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // 부모 생성자 호출
}
public void sound() {
System.out.println("멍멍");
}
}
추상클래스가 생성자를 가질 수 있는 이유¶
추상클래스는 직접 new로 객체를 만들 수는 없다.
하지만 자식 클래스가 객체를 생성할 때 super()로 부모 생성자를 호출하기 때문에
공통 초기화 로직을 부모 생성자에 담을 수 있다.
추상클래스 자체는 인스턴스화 불가 → 그러나 자식 객체 생성 시 부모 생성자가 실행됨.
실무에서 추상클래스 사용 예시¶
결제 흐름처럼 전체 흐름은 같고 일부 단계만 다를 때 템플릿 메서드 패턴으로 활용한다.
public abstract class PaymentTemplate {
// 전체 흐름 고정 (템플릿)
public final void pay(int amount) {
validate(amount); // 공통 로직
processPayment(amount); // 구현체마다 다름
sendReceipt(); // 공통 로직
}
private void validate(int amount) {
if (amount <= 0) throw new IllegalArgumentException("금액 오류");
}
protected abstract void processPayment(int amount); // 구현 강제
private void sendReceipt() {
System.out.println("영수증 발송");
}
}
public class CardPayment extends PaymentTemplate {
protected void processPayment(int amount) {
System.out.println("카드로 " + amount + "원 결제");
}
}
public class BankTransfer extends PaymentTemplate {
protected void processPayment(int amount) {
System.out.println("계좌이체로 " + amount + "원 결제");
}
}
전체 결제 흐름(검증 → 결제 → 영수증)은 부모가 고정하고,
실제 결제 방식만 자식이 구현한다. 중복 코드 없이 흐름을 재사용할 수 있다.
인터페이스 vs 추상클래스 비교¶
| 인터페이스 | 추상클래스 | |
|---|---|---|
| 목적 | 기능 계약 (무엇을) | 공통 구현 공유 (어떻게) |
| 관계 | can-do (~할 수 있다) | is-a (~이다) |
| 상속 | 다중 구현 가능 | 단일 상속만 가능 |
| 필드 | 상수(public static final)만 가능 |
일반 필드 가능 |
| 생성자 | 없음 | 있음 |
| 메서드 | 추상 메서드 + default 메서드 | 추상 + 일반 메서드 모두 가능 |
| 실무 활용 | Service 계층 추상화, DI | 템플릿 메서드 패턴 |
사용 기준¶
인터페이스 — can-do 관계¶
서로 다른 계층의 클래스에 공통 기능을 부여하거나, 구현체를 교체 가능하게 설계할 때 사용한다.
Bird → Flyable (새는 날 수 있다)
Airplane → Flyable (비행기도 날 수 있다)
인터페이스 이름은
Flyable,Serializable,Runnable처럼 ~able 형태로 짓는 것이 Java 컨벤션이다.
추상클래스 — is-a 관계¶
같은 종류의 계층 구조에서 공통 코드를 공유하거나, 전체 흐름을 고정하고 일부만 다르게 구현할 때 사용한다.
Dog is an Animal (개는 동물이다)
Cat is an Animal (고양이는 동물이다)
면접 포인트¶
Q. 인터페이스와 추상클래스의 차이는?
인터페이스는 can-do 관계로 서로 다른 클래스에 공통 기능을 부여할 때 사용하고, 다중 구현이 가능하다.
추상클래스는 is-a 관계로 같은 계층의 클래스들이 공통 코드를 공유할 때 사용하며, 단일 상속만 가능하다.
Q. 언제 인터페이스를, 언제 추상클래스를 쓰나요?
구현체를 교체 가능하게 설계하거나, 서로 다른 종류의 클래스에 공통 기능을 부여할 때 → 인터페이스.
전체 흐름은 같고 일부 단계만 다르게 구현해야 할 때 → 추상클래스 (템플릿 메서드 패턴).
Q. Java 8의 default 메서드가 왜 생겼나요?
인터페이스에 메서드를 추가하면 모든 구현체가 깨지는 문제를 해결하기 위해 도입됐다.
default 메서드로 기본 구현을 제공하면 기존 구현체를 수정하지 않아도 된다.
Q. Spring에서 인터페이스를 왜 쓰나요?
Controller가 Service 인터페이스만 알면 되기 때문에 구현체가 바뀌어도 Controller 코드를 수정할 필요가 없다.
테스트 시 Mock 구현체로 쉽게 교체할 수 있어 단위 테스트가 편해진다.