공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리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정리‹ 이전다음 ›

3. 코드 예제

예제 1: 결제 게이트웨이 전략 패턴

OrderService가 PaymentGateway 인터페이스만 의존하고, 구현체를 갈아 끼워도 서비스 코드가 바뀌지 않음을 보여준다.

java
record PaymentResult(boolean success, String transactionId) {}

interface PaymentGateway {
    PaymentResult pay(long amount);

    default boolean supports(long amount) { return amount > 0; }

    default String name() { return getClass().getSimpleName(); }
}

class CardGateway implements PaymentGateway {
    @Override
    public PaymentResult pay(long amount) {
        return new PaymentResult(true, "CARD-" + amount);
    }
}

class BankTransferGateway implements PaymentGateway {
    @Override
    public PaymentResult pay(long amount) {
        return new PaymentResult(true, "BANK-" + amount);
    }

    @Override
    public boolean supports(long amount) { return amount >= 10_000; }   // 1만원 미만 이체 불가
}

class OrderService {
    private final PaymentGateway gateway;   // 인터페이스 타입만 의존

    OrderService(PaymentGateway gateway) { this.gateway = gateway; }

    String placeOrder(String orderId, long amount) {
        if (!gateway.supports(amount)) {
            return orderId + " 실패: " + gateway.name() + "는 " + amount + "원 결제 불가";
        }
        PaymentResult r = gateway.pay(amount);
        return orderId + " 결제 " + (r.success() ? "성공" : "실패") + " tx=" + r.transactionId();
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new OrderService(new CardGateway()).placeOrder("O-1", 5_000));
        System.out.println(new OrderService(new BankTransferGateway()).placeOrder("O-2", 5_000));
        System.out.println(new OrderService(new BankTransferGateway()).placeOrder("O-3", 50_000));
        // 람다로 즉석 구현체: 테스트용 "항상 성공" 게이트웨이
        System.out.println(new OrderService(amount -> new PaymentResult(true, "FAKE")).placeOrder("O-4", 1));
        // 출력:
        // O-1 결제 성공 tx=CARD-5000
        // O-2 실패: BankTransferGateway는 5000원 결제 불가
        // O-3 결제 성공 tx=BANK-50000
        // O-4 결제 성공 tx=FAKE
    }
}

예제 2: 템플릿 메서드로 데이터 내보내기

내보내기 흐름은 DataExporter.export()에 고정하고, CSV와 JSON은 각 단계만 다르게 구현한다.

java
import java.util.List;
import java.util.stream.Collectors;

record Order(String id, String product, int qty) {}

abstract class DataExporter {
    public final String export(List<Order> orders) {   // 템플릿 메서드: 순서 고정
        StringBuilder sb = new StringBuilder();
        sb.append(header());
        for (Order o : orders) sb.append('\n').append(row(o));
        sb.append(footer());
        return sb.toString();
    }

    protected abstract String header();
    protected abstract String row(Order o);
    protected String footer() { return ""; }   // 훅 메서드: 기본은 없음
}

class CsvExporter extends DataExporter {
    @Override protected String header() { return "id,product,qty"; }
    @Override protected String row(Order o) { return o.id() + "," + o.product() + "," + o.qty(); }
}

class JsonExporter extends DataExporter {
    @Override protected String header() { return "["; }
    @Override protected String row(Order o) {
        return "  {\"id\":\"%s\",\"product\":\"%s\",\"qty\":%d}".formatted(o.id(), o.product(), o.qty());
    }
    @Override protected String footer() { return "\n]"; }
}

public class Main {
    public static void main(String[] args) {
        var orders = List.of(new Order("A1", "키보드", 2), new Order("A2", "마우스", 1));
        System.out.println(new CsvExporter().export(orders));
        System.out.println(new JsonExporter().export(orders));
        // 출력:
        // id,product,qty
        // A1,키보드,2
        // A2,마우스,1
        // [
        //   {"id":"A1","product":"키보드","qty":2}
        //   {"id":"A2","product":"마우스","qty":1}
        // ]
    }
}

예제 3: 다중 구현과 default 충돌 해결

한 클래스가 두 인터페이스를 구현하며, 같은 시그니처의 default 메서드를 명시적으로 선택하는 방법을 보여준다.

java
interface Loggable {
    default String describe() { return "Loggable"; }
}

interface Auditable {
    default String describe() { return "Auditable"; }
    default String auditTag() { return "[AUDIT]"; }
}

class Transaction implements Loggable, Auditable {
    private final long amount;
    Transaction(long amount) { this.amount = amount; }

    @Override
    public String describe() {   // 두 default가 충돌 → 반드시 오버라이딩
        return Loggable.super.describe() + "+" + Auditable.super.describe() + " " + amount + "원";
    }
}

public class Main {
    public static void main(String[] args) {
        Transaction t = new Transaction(30_000);
        System.out.println(t.auditTag() + " " + t.describe());
        // 출력: [AUDIT] Loggable+Auditable 30000원

        Loggable l = t;      // 업캐스팅: 어느 인터페이스 타입으로 봐도 Transaction의 describe가 실행
        System.out.println(l.describe());
        // 출력: Loggable+Auditable 30000원
    }
}

예제 4: 알림 발송기 — 인터페이스 + 추상 클래스 조합

계약은 인터페이스로, 공통 로직(재시도, 로그)은 추상 클래스로, 채널별 차이는 구체 클래스로 나누는 실무 구조다.

java
import java.util.List;

interface Notifier {
    boolean send(String to, String message);
}

abstract class RetryingNotifier implements Notifier {
    private final int maxAttempts;
    protected RetryingNotifier(int maxAttempts) { this.maxAttempts = maxAttempts; }

    @Override
    public final boolean send(String to, String message) {   // 공통: 재시도 골격
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            if (doSend(to, message)) {
                System.out.println(channel() + " 발송 성공 (" + attempt + "회차) → " + to);
                return true;
            }
            System.out.println(channel() + " 발송 실패 (" + attempt + "회차)");
        }
        return false;
    }

    protected abstract boolean doSend(String to, String message);
    protected abstract String channel();
}

class EmailNotifier extends RetryingNotifier {
    EmailNotifier() { super(1); }
    @Override protected boolean doSend(String to, String msg) { return to.contains("@"); }
    @Override protected String channel() { return "EMAIL"; }
}

class SmsNotifier extends RetryingNotifier {
    private int calls = 0;
    SmsNotifier() { super(3); }
    @Override protected boolean doSend(String to, String msg) { return ++calls >= 2; }   // 첫 시도는 실패하는 척
    @Override protected String channel() { return "SMS"; }
}

public class Main {
    public static void main(String[] args) {
        List<Notifier> notifiers = List.of(new EmailNotifier(), new SmsNotifier());
        for (Notifier n : notifiers) {
            n.send(n instanceof EmailNotifier ? "user@shop.com" : "010-1234-5678", "주문이 완료되었습니다");
        }
        // 출력:
        // EMAIL 발송 성공 (1회차) → user@shop.com
        // SMS 발송 실패 (1회차)
        // SMS 발송 성공 (2회차) → 010-1234-5678
    }
}

예제 5: 함수형 인터페이스로 할인 정책 조합

메서드 하나짜리 전략은 람다로 넘기고, default 메서드로 정책을 합성(compose)하는 방법을 보여준다.

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

    default DiscountPolicy andThen(DiscountPolicy next) {   // 정책 합성
        return price -> next.apply(this.apply(price));
    }

    static DiscountPolicy none() { return price -> price; }
}

public class Main {
    public static void main(String[] args) {
        DiscountPolicy tenPercent = price -> price * 90 / 100;
        DiscountPolicy coupon5000 = price -> Math.max(0, price - 5_000);

        long price = 50_000;
        System.out.println(DiscountPolicy.none().apply(price));                 // 출력: 50000
        System.out.println(tenPercent.apply(price));                            // 출력: 45000
        System.out.println(tenPercent.andThen(coupon5000).apply(price));        // 출력: 40000
        System.out.println(coupon5000.andThen(tenPercent).apply(price));        // 출력: 40500 (순서에 따라 다름)
    }
}

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\intermediate\02_abstract_interface
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: 결제 게이트웨이 전략 패턴
  • 예제 2: 템플릿 메서드로 데이터 내보내기
  • 예제 3: 다중 구현과 default 충돌 해결
  • 예제 4: 알림 발송기 — 인터페이스 + 추상 클래스 조합
  • 예제 5: 함수형 인터페이스로 할인 정책 조합
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제