공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리0/9 완료
  • 01상속, 다형성, 오버라이딩
  • 02추상 클래스 vs 인터페이스
  • 03예외 처리
  • 04java.lang 심화
  • 05컬렉션 프레임워크 딥다이브
  • 06메서드 활용 패턴 (중급)
  • 07Object 메서드와 비교
  • 08java.time 실무 날짜 계산
  • 09HTTP 와 JSON 기초
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 중급 › 02 / 9

추상 클래스 vs 인터페이스

섹션 7진행 0 / 9
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 추상 클래스: 미완성 뼈대

추상 클래스(abstract class)는 인스턴스를 만들 수 없는 클래스다. abstract 키워드를 붙이며, 본문 없는 추상 메서드(abstract method)를 가질 수 있다. 자식 클래스는 모든 추상 메서드를 구현해야만 구체 클래스(concrete class, 인스턴스화 가능한 클래스)가 된다.

java
abstract class Report {
    abstract String body();                      // 자식이 반드시 구현
    String render() {                            // 공통 구현
        return "=== 리포트 ===\n" + body() + "\n=== 끝 ===";
    }
}

왜 인스턴스화를 막는가? Report는 "리포트라는 개념"이지 어떤 구체적인 리포트가 아니다. new Report()가 가능하면 body()를 호출했을 때 실행할 코드가 없다. 추상 클래스는 "이 클래스는 완성품이 아니라 뼈대다"라는 설계 의도를 컴파일러가 강제하는 장치다.

추상 클래스가 가질 수 있는 것:

구성 요소 가능 여부 비고
인스턴스 필드 (상태) 가능 자식과 공유되는 상태
생성자 가능 자식 생성자에서 super(...)로 호출
추상 메서드 가능 0개여도 됨 (그래도 인스턴스화 불가)
구체 메서드 가능 공통 로직
static 메서드 가능
private 메서드 가능

2.2 템플릿 메서드 패턴

추상 클래스의 가장 대표적인 활용법이다. 알고리즘의 골격(순서)은 부모가 고정하고, 각 단계의 세부 구현은 자식에게 위임한다. 골격을 정의한 메서드를 템플릿 메서드라고 부르며, 보통 final로 선언해 자식이 순서를 바꾸지 못하게 한다.

java
abstract class DataExporter {
    // 템플릿 메서드: 순서 고정
    public final void export(List<Order> orders) {
        String header = header();
        String rows = orders.stream().map(this::row).collect(Collectors.joining("\n"));
        write(header + "\n" + rows);
        afterExport();                // 훅(hook) 메서드: 기본 구현 있음, 필요하면 오버라이딩
    }
    protected abstract String header();
    protected abstract String row(Order o);
    protected abstract void write(String content);
    protected void afterExport() {}   // 훅: 아무것도 안 함이 기본
}

왜 이 패턴이 유용한가? "CSV 내보내기"와 "JSON 내보내기"는 헤더 형식, 행 형식, 저장 방식만 다르고 전체 흐름은 같다. 흐름이 여러 클래스에 복사되어 있으면 흐름을 바꿀 때(예: 내보내기 전에 권한 검사 추가) 모든 복사본을 고쳐야 한다. 템플릿 메서드는 흐름을 한 곳에 모은다.

Spring의 JdbcTemplate, RestTemplate, Servlet의 HttpServlet.service() → doGet()/doPost()가 전부 이 패턴이다.

2.3 인터페이스: 순수한 계약

인터페이스(interface)는 "이 타입은 이런 메서드를 제공한다"는 계약(contract)만 정의한다. 전통적으로는 구현이 전혀 없었지만, JDK 8부터 default/static 메서드, JDK 9부터 private 메서드가 추가되면서 제한적인 구현을 가질 수 있게 되었다.

java
interface PaymentGateway {
    int MAX_AMOUNT = 10_000_000;                     // public static final 상수 (자동)

    PaymentResult pay(long amount);                  // public abstract (자동)

    default boolean supports(long amount) {          // 기본 구현, 구현체가 오버라이딩 가능
        return amount > 0 && amount <= MAX_AMOUNT;
    }

    static PaymentGateway noop() {                   // 유틸리티/팩토리
        return amount -> new PaymentResult(true, "NOOP");
    }

    private void log(String msg) {                   // default 메서드들 사이의 중복 제거용 (JDK 9+)
        System.out.println("[GW] " + msg);
    }
}

인터페이스 멤버의 암묵적 수식어:

멤버 암묵적 수식어 설명
필드 public static final 상수만 가능. 인스턴스 상태 불가
추상 메서드 public abstract 본문 없음
default 메서드 public 본문 있음. 구현체에서 오버라이딩 가능
static 메서드 public 인터페이스 이름으로 호출. 상속 안 됨
private 메서드 — 인터페이스 내부에서만 사용 (JDK 9+)

왜 인터페이스에는 인스턴스 필드가 없는가? 인터페이스는 다중 구현이 가능하다. 두 인터페이스가 각각 인스턴스 필드를 가지면 하나의 객체 안에서 두 필드의 초기화 순서, 메모리 레이아웃, 충돌 문제가 생긴다. C++의 다중 상속이 겪는 "다이아몬드 문제"를 피하기 위해 Java는 인터페이스에서 상태를 제거했다.

2.4 default 메서드가 등장한 이유와 충돌 규칙

JDK 8에서 Collection에 stream()을 추가해야 했다. 그런데 Collection은 인터페이스이고, 전 세계에 수만 개의 구현체가 있다. 추상 메서드를 추가하면 그 구현체들이 전부 컴파일 에러가 난다. 그래서 기존 구현체를 깨지 않고 인터페이스에 메서드를 추가하는 수단으로 default 메서드가 도입되었다.

다중 구현에서 같은 시그니처의 default 메서드가 충돌하면 어떻게 되는가?

상황 결과
클래스가 직접 구현 (또는 부모 클래스에 있음) 클래스 것이 이긴다 (class wins)
두 인터페이스 중 하나가 다른 것을 상속 더 구체적인(하위) 인터페이스 것이 이긴다
무관한 두 인터페이스가 같은 default 제공 컴파일 에러 — 구현 클래스가 직접 오버라이딩해서 해결해야 함
java
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B {
    @Override
    public String hello() { return A.super.hello() + B.super.hello(); }   // 명시적 선택
}

2.5 추상 클래스 vs 인터페이스 비교

기준 추상 클래스 인터페이스
관계 의미 is-a (Dog is an Animal) can-do (Dog can Swim, Comparable)
상태(인스턴스 필드) 가질 수 있음 불가 (상수만)
생성자 있음 없음
다중 상속 불가 (단일 상속) 가능 (여러 개 implements)
메서드 구현 자유롭게 default/static/private만
접근 제어자 모두 가능 사실상 public (private 메서드 예외)
설계 의도 밀접한 클래스들의 공통 뼈대와 상태 공유 무관한 클래스들에 공통 능력 부여
진화(메서드 추가) 구체 메서드 추가는 안전 default 메서드로 추가 가능
예시 AbstractList, HttpServlet, InputStream List, Comparable, Runnable

선택 기준 요약:

  • 서로 무관한 클래스들이 공통 능력을 가져야 한다 → 인터페이스 (Comparable은 String, Integer, LocalDate 등 전혀 다른 클래스들이 구현)
  • 상태나 생성자 로직을 공유해야 한다 → 추상 클래스
  • 둘 다 필요하다 → 인터페이스로 계약 정의 + 추상 클래스로 골격 구현 제공 (List 인터페이스 + AbstractList 골격 + ArrayList 구체). JDK 컬렉션 프레임워크가 정확히 이 구조다.
  • 확신이 없다 → 인터페이스. 나중에 골격 구현이 필요하면 추상 클래스를 추가하면 되지만, 추상 클래스에서 인터페이스로 바꾸는 것은 이미 상속한 클래스들이 있어 어렵다.

2.6 전략 패턴: 런타임에 알고리즘 교체

전략 패턴(strategy pattern)은 알고리즘을 인터페이스로 캡슐화하고, 사용하는 쪽은 인터페이스 타입만 바라보게 하여 구현을 런타임에 교체하는 설계다.

text
┌──────────────┐  uses   ┌────────────────────┐
│ OrderService │ ──────▶ │ «interface»        │
│ - gateway    │         │ PaymentGateway     │
└──────────────┘         │ + pay(amount)      │
                         └─────────┬──────────┘
                    ┌──────────────┼──────────────┐
             ┌──────┴──────┐ ┌─────┴──────┐ ┌─────┴──────┐
             │ CardGateway │ │ BankGateway│ │ PointGateway│
             └─────────────┘ └────────────┘ └────────────┘

OrderService는 PaymentGateway만 알고, 어떤 구현체가 들어오는지는 생성자 주입(constructor injection, 생성자 매개변수로 의존 객체를 받는 것)으로 결정된다. 새 결제 수단은 구현체 클래스 하나를 추가하는 것으로 끝난다. 이것이 개방-폐쇄 원칙(OCP, 확장에는 열려 있고 수정에는 닫혀 있어야 한다)의 실현이다.

2.7 함수형 인터페이스 맛보기

추상 메서드가 정확히 하나인 인터페이스를 함수형 인터페이스(functional interface)라고 하며, 람다식으로 구현체를 만들 수 있다. @FunctionalInterface 애노테이션을 붙이면 추상 메서드가 둘 이상이 될 때 컴파일 에러로 막아 준다.

java
@FunctionalInterface
interface DiscountPolicy {
    long apply(long price);
}

DiscountPolicy none    = price -> price;
DiscountPolicy tenPct  = price -> price * 90 / 100;
DiscountPolicy fixed   = price -> Math.max(0, price - 5_000);

람다는 "이름 없는 구현 클래스의 인스턴스"를 짧게 쓰는 문법이다. 전략 패턴에서 전략이 메서드 하나짜리라면 클래스를 만들 필요 없이 람다로 넘기면 된다. JDK가 제공하는 Runnable, Comparator<T>, Function<T,R>, Predicate<T>, Supplier<T>, Consumer<T>가 대표적인 함수형 인터페이스다. 자세한 내용은 람다/스트림 레슨에서 다룬다.

핵심 원리
  • 2.1 추상 클래스: 미완성 뼈대
  • 2.2 템플릿 메서드 패턴
  • 2.3 인터페이스: 순수한 계약
  • 2.4 default 메서드가 등장한 이유와 충돌 규칙
  • 2.5 추상 클래스 vs 인터페이스 비교
  • 2.6 전략 패턴: 런타임에 알고리즘 교체
  • 2.7 함수형 인터페이스 맛보기
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제