티스토리 뷰

 

객체지향 프로그래밍(OOP)으로 아키텍처를 설계하다 보면 어김없이 마주치는 고민이 있습니다. "여기에 인터페이스를 써야 할까, 아니면 추상 클래스를 써야 할까?" 두 개념 모두 '추상화(Abstraction)'를 통해 다형성을 구현하고 유연한 코드를 작성하게 해준다는 공통점이 있지만, 그 목적과 내부적으로 작동하는 철학은 완전히 다릅니다. 이 차이를 명확히 이해하지 못하면, 코드가 확장될수록 유지보수가 불가능한 거대한 상속의 늪에 빠지게 됩니다.

현대 소프트웨어 생태계에서 느슨한 결합도(Loose Coupling)는 시스템의 생존을 결정짓는 핵심 요소입니다. 오늘은 이 두 가지 추상화 도구의 본질적인 차이를 파헤치고, 실제 비즈니스 로직에 어떻게 적용해야 하는지 실전 예제와 함께 살펴보겠습니다.

1. 핵심 개념 설명 (Deep Dive)

추상 클래스와 인터페이스의 가장 큰 차이는 **'존재의 목적'**에 있습니다.

추상 클래스 (Abstract Class): "나는 무엇인가? (IS-A)"

추상 클래스는 미완성 설계도입니다. 객체의 **'정체성'**을 정의하며, 하위 클래스들이 공유해야 할 공통적인 상태(필드)와 동작(메서드)을 물려주는 데 집중합니다. 수식으로 표현하자면 집합론적 관점에서 부분집합 $ChildClass \subseteq ParentClass$ 의 강한 결속 관계를 가집니다.

  • 비유(Analogy): 자동차의 '플랫폼(뼈대)'을 떠올려 보세요. 현대자동차의 E-GMP 플랫폼은 전기차의 뼈대(추상 클래스)입니다. 이 뼈대에는 배터리 위치, 바퀴 축 같은 물리적 상태가 이미 구현되어 있습니다. 아이오닉5나 EV6(구현 클래스)는 이 뼈대를 그대로 물려받고(상속), 껍데기와 세부 모터 출력(추상 메서드 구현)만 다르게 설계합니다.

인터페이스 (Interface): "나는 무엇을 할 수 있는가? (CAN-DO)"

인터페이스는 엄격한 '계약(Contract)'입니다. 객체가 어떤 상태를 가지고 있는지, 속이 어떻게 생겼는지는 전혀 관심이 없습니다. 오직 **'어떤 행동을 할 수 있는지'**에만 초점을 맞춥니다.

  • 비유(Analogy): 'USB-C 포트 규격'과 같습니다. 스마트폰이든, 선풍기든, 키보드든(전혀 다른 클래스들) USB-C 규격(인터페이스)을 준수하기만 하면 전력을 공급받거나 데이터를 교환할 수 있습니다. 각 기기가 내부적으로 어떻게 작동하는지는 중요하지 않습니다.

2. 풍부한 실전 예제 (Hands-on)

실제 이커머스 서비스의 결제 시스템을 설계한다고 가정해 보겠습니다. 결제 수단에는 신용카드, 카카오페이, 그리고 비트코인이 있습니다.

모든 결제 수단은 결제 금액과 통화(Currency) 정보를 가지며, 결제 로깅을 남겨야 합니다. 하지만 환불(Refund) 기능은 신용카드와 카카오페이에만 있고, 스마트 컨트랙트로 처리되는 비트코인 결제에는 불가능하다고 가정해 봅시다.

Java
 
// 1. 추상 클래스: 결제 수단들의 공통된 '상태'와 '공통 로직'을 정의합니다. (IS-A)
public abstract class AbstractPayment {
    // 하위 클래스가 공유하는 상태(State)
    protected double amount;
    protected String currency;
    protected String transactionId;

    public AbstractPayment(double amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    // 공통 기능: 모든 결제는 동일한 방식으로 로깅됩니다.
    public void logTransaction() {
        System.out.println("[LOG] Transaction " + transactionId + ": " + amount + " " + currency);
    }

    // 하위 클래스에서 반드시 구현해야 하는 핵심 비즈니스 로직
    public abstract void processPayment();
}

// 2. 인터페이스: '환불 가능함'이라는 특수한 '행동'을 정의합니다. (CAN-DO)
public interface Refundable {
    // 환불 로직의 계약 (상태를 가질 수 없음)
    void processRefund(String transactionId);
}

// 3. 구현 클래스 A (신용카드): 결제 수단이면서(extends) 환불이 가능합니다(implements).
public class CreditCardPayment extends AbstractPayment implements Refundable {
    
    private String cardNumber;

    public CreditCardPayment(double amount, String currency, String cardNumber) {
        super(amount, currency); // 부모(추상 클래스)의 상태 초기화
        this.cardNumber = cardNumber;
    }

    @Override
    public void processPayment() {
        this.transactionId = "CC-" + System.currentTimeMillis();
        // PG사 API 호출 등 복잡한 신용카드 승인 로직...
        System.out.println("Credit card payment processed. ID: " + this.transactionId);
        logTransaction(); // 부모의 공통 메서드 재사용
    }

    @Override
    public void processRefund(String transactionId) {
        System.out.println("Refunding credit card transaction: " + transactionId);
    }
}

// 4. 구현 클래스 B (비트코인): 결제 수단이지만(extends), 환불은 불가능합니다(인터페이스 미구현).
public class CryptoPayment extends AbstractPayment {
    
    private String walletAddress;

    public CryptoPayment(double amount, String currency, String walletAddress) {
        super(amount, currency);
        this.walletAddress = walletAddress;
    }

    @Override
    public void processPayment() {
        this.transactionId = "CRYPTO-" + System.currentTimeMillis();
        System.out.println("Crypto payment processed via Smart Contract.");
        logTransaction(); 
    }
    // 환불 로직이 없으므로 Refundable을 구현하지 않음.
}

💡 트러블슈팅(Troubleshooting) 팁

  • 다중 상속의 늪 (The Diamond Problem): "왜 자바 같은 언어는 클래스의 다중 상속을 막았을까요?" 만약 CreditCardPayment가 Payment와 Reward라는 두 개의 추상 클래스를 상속받을 수 있고, 두 클래스 모두 calculate()라는 메서드를 가지고 있다면, 컴파일러는 어느 쪽의 메서드를 실행해야 할지 알 수 없습니다. 반면, 인터페이스는 구현체 없이 선언만 있기 때문에 여러 개를 구현(implements Refundable, Cancellable)해도 충돌이 발생하지 않습니다.
  • 인터페이스의 무분별한 사용: 모든 것을 인터페이스로 빼려고 하면 코드가 파편화되어 추적하기 힘들어집니다. 위 예제에서 logTransaction() 같은 공통 상태 기반의 로직을 인터페이스로 구현하려 했다면, 모든 결제 클래스마다 똑같은 로그 코드를 중복해서 작성해야 했을 것입니다.

3. 장단점 및 고려사항 (Trade-offs)

구분 추상 클래스 (Abstract Class) 인터페이스 (Interface)
목적 관련성이 높은 클래스 간의 코드 재사용 및 뼈대 제공 관련 없는 클래스 간의 공통 동작(계약) 정의
상태(State) 인스턴스 변수(필드)를 가질 수 있음 원칙적으로 상태를 가질 수 없음 (상수만 가능)
다중 상속 불가능 (단일 상속만 지원) 가능 (여러 개의 인터페이스 구현 가능)
결합도 높음 (부모의 변화가 자식에게 치명적 영향) 낮음 (구현체들끼리 서로 독립적)
고려사항 상속 트리가 깊어지면 유지보수가 매우 어려워지는 '취약한 기반 클래스 문제' 발생 위험 (Java 8 이전) 새로운 메서드를 추가하면 이를 구현하는 모든 클래스가 깨짐

참고: Java 8부터 인터페이스에 default 메서드가 도입되면서 인터페이스도 일부 로직을 가질 수 있게 되었지만, 여전히 상태(인스턴스 변수)를 가질 수는 없다는 점에서 추상 클래스와의 본질적인 경계는 유지됩니다.


4. 결론 및 요약

결론적으로, 두 기술을 선택하는 기준은 다음과 같습니다.

  1. "이 객체들이 본질적으로 같은 종류이며, 공통된 상태(데이터)와 흐름을 공유하는가?" →   추상 클래스를 선택하세요.
  2. "서로 다른 종류의 객체들이지만, 시스템 내에서 동일한 역할을 수행(특정 행동)해야 하는가?" →  인터페이스를 선택하세요.

실제 현업에서는 위 결제 시스템 예제처럼, 추상 클래스로 핵심 도메인의 뼈대를 잡고, 인터페이스로 부가적인 기능을 확장(플러그인)하는 하이브리드 방식이 가장 강력하고 우아한 아키텍처를 만들어냅니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함