콘텐츠로 이동

추상화 (Abstraction)

왜 쓰는지

구현의 세부사항은 자주 변경되지만, 역할(행동)은 상대적으로 안정적입니다: - "결제 수단"이 필요 (신용카드, 휴대폰, 계좌이체) - 각 결제 수단의 구현은 다르지만, "결제한다"는 행동은 같음 - 새로운 결제 수단이 추가되어도 기존 코드는 변경 없음

핵심: 추상화는 구현이 아닌 역할(행동)을 중심으로 설계하여, 변경에 강하고 확장에 유연한 구조를 만들기 위함입니다. OCP(Open–Closed Principle)의 핵심입니다.

어떻게 쓰는지

추상 클래스 (Abstract Class)

// 부모: 공통 인터페이스 + 일부 구현 제공
public abstract class PaymentMethod {
    private String accountNumber;

    // 역할만 정의 (구현 강제)
    public abstract void pay(int amount);

    public abstract boolean validate();

    // 공통 구현 제공
    public void logTransaction(int amount) {
        System.out.println("거래 기록: " + amount + "원");
    }
}

// 자식 1: CreditCard
public class CreditCard extends PaymentMethod {
    @Override
    public void pay(int amount) {
        if (amount > 1000000) {
            System.out.println("한도 초과");
            return;
        }
        System.out.println("신용카드 결제: " + amount + "원");
    }

    @Override
    public boolean validate() {
        return true;  // 신용카드 유효성 검사
    }
}

// 자식 2: PhonePay
public class PhonePay extends PaymentMethod {
    @Override
    public void pay(int amount) {
        System.out.println("휴대폰 결제: " + amount + "원");
    }

    @Override
    public boolean validate() {
        return true;  // 휴대폰 유효성 검사
    }
}

인터페이스 (Interface)

// 순수 역할 정의 (구현 없음)
public interface PaymentMethod {
    void pay(int amount);
    boolean validate();
}

// 구현 1
public class CreditCard implements PaymentMethod {
    @Override
    public void pay(int amount) {
        System.out.println("신용카드 결제: " + amount + "원");
    }

    @Override
    public boolean validate() {
        return true;
    }
}

// 구현 2
public class PhonePay implements PaymentMethod {
    @Override
    public void pay(int amount) {
        System.out.println("휴대폰 결제: " + amount + "원");
    }

    @Override
    public boolean validate() {
        return true;
    }
}

추상화의 활용 (OCP 준수)

// 추상화에 의존하므로 구현 변경에 영향 없음
public class ShoppingCart {
    private PaymentMethod paymentMethod;

    public ShoppingCart(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public void checkout(int totalAmount) {
        if (!paymentMethod.validate()) {
            System.out.println("결제 수단 검증 실패");
            return;
        }
        paymentMethod.pay(totalAmount);
    }
}

// 사용: ShoppingCart는 어떤 결제 수단도 수용
ShoppingCart cart = new ShoppingCart(new CreditCard());
cart.checkout(50000);

// 나중에 새로운 결제 수단 추가 (ShoppingCart는 수정 없음)
ShoppingCart cart2 = new ShoppingCart(new Bitcoin());
cart2.checkout(50000);

언제 쓰는지

상황 선택 이유
역할이 명확함 ✅ 추상화 인터페이스/추상클래스로 정의
구현이 자주 변함 ✅ 추상화 기존 코드 수정 최소화
다양한 구현 필요 ✅ 인터페이스 다중 구현 가능
공통 구현 많음 ✅ 추상클래스 코드 중복 제거
단순한 경우 ❌ 구체 클래스 과설계 방지

장점

장점 설명
OCP 준수 확장에 열려있고, 변경에 닫혀있음
결합도 감소 구체 구현이 아닌 추상화에 의존
테스트 용이 Mock 객체 주입으로 단위 테스트 가능
확장성 새로운 구현 추가 시 기존 코드 변경 불필요
유지보수 개선 변경 포인트 명확화

단점

단점 설명
과설계 단순한 경우까지 인터페이스 강제
복잡도 증가 계층이 많아지면 코드 추적 어려움
성능 오버헤드 다형성으로 인한 메서드 호출 비용
학습곡선 SOLID 원칙 이해 필요

특징

1. 인터페이스 vs 추상클래스

항목 인터페이스 추상클래스
구현 제공 안 함 부분 제공 가능
상태 필드 불가 필드 가능
상속 다중 구현 가능 단일 상속
용도 역할 계약 공통 개념 + 구현
접근제어 public만 다양함

2. 추상 메서드 vs 구체 메서드

public abstract class Shape {
    // 추상 메서드: 자식이 반드시 구현해야 함
    public abstract double getArea();

    // 구체 메서드: 공통 구현
    public void display() {
        System.out.println("넓이: " + getArea());
    }
}

public class Circle extends Shape {
    private double radius;

    @Override
    public double getArea() {
        return 3.14 * radius * radius;
    }
    // display()는 자동으로 상속됨
}

3. 템플릿 메서드 패턴

public abstract class DataProcessor {
    // 템플릿: 알고리즘의 구조는 정의하고, 세부는 자식이 구현
    public final void process(String data) {
        String validated = validate(data);
        String transformed = transform(validated);
        save(transformed);
    }

    protected abstract String validate(String data);
    protected abstract String transform(String data);
    protected abstract void save(String data);
}

public class JsonProcessor extends DataProcessor {
    @Override
    protected String validate(String data) {
        // JSON 검증
        return data;
    }

    @Override
    protected String transform(String data) {
        // JSON 파싱
        return data;
    }

    @Override
    protected void save(String data) {
        // DB에 저장
    }
}

4. Java 8+ 인터페이스 default 메서드

public interface PaymentMethod {
    // 추상 메서드
    void pay(int amount);

    // default 메서드 (구현 제공, 오버라이드 가능)
    default boolean validate() {
        return true;  // 기본 구현
    }

    // static 메서드
    static PaymentMethod createCreditCard() {
        return new CreditCard();
    }
}

주의할 점

❌ 추상 메서드 구현 안 함

public abstract class PaymentMethod {
    public abstract void pay(int amount);
}

// ❌ 컴파일 에러: 추상 메서드 미구현
public class CreditCard extends PaymentMethod {
    // pay() 구현 안 함
}

// 또는 CreditCard도 abstract 클래스여야 함

✅ 올바른 방식:

public class CreditCard extends PaymentMethod {
    @Override
    public void pay(int amount) {
        System.out.println("신용카드 결제");
    }
}

⚠️ Java 8+ default 메서드의 다이아몬드 문제

interface A {
    default void method() { System.out.println("A"); }
}

interface B extends A {
    default void method() { System.out.println("B"); }
}

interface C extends A {
    default void method() { System.out.println("C"); }
}

// ❌ 어느 default 메서드를 사용할지 모호
class D implements B, C {
    // 컴파일 에러: 명시적 해결 필요
}

// ✅ 명시적 해결
class D implements B, C {
    @Override
    public void method() {
        B.super.method();  // B의 메서드 사용
    }
}

⚠️ 추상 클래스는 인스턴스 생성 불가

// ❌ 불가능
PaymentMethod pm = new PaymentMethod();

// ✅ 자식 클래스로만 인스턴스 생성
PaymentMethod pm = new CreditCard();

베스트 프랙티스

권장 방식 이유
역할 이름으로 추상화 PaymentMethod, NotificationSender처럼 사용하는 쪽의 관점이 드러남
구현이 2개 이상이거나 바뀔 가능성이 있을 때 도입 불필요한 인터페이스 남발을 줄일 수 있음
인터페이스는 작게 유지 구현 클래스가 필요 없는 메서드까지 강제로 구현하지 않게 함
호출 코드는 구체 클래스보다 추상 타입에 의존 구현체 교체와 테스트 대체가 쉬워짐
공통 상태가 필요하면 추상 클래스 검토 필드와 공통 알고리즘이 필요한 경우 인터페이스보다 자연스러움
default 메서드는 호환성 보강용으로 제한 인터페이스가 비대한 구현 클래스처럼 변하는 것을 막음

추상화는 미리 많이 만드는 기술이 아니라, 변경 가능성이 보일 때 경계를 세우는 기술입니다.

처음부터 모든 클래스에 인터페이스를 만들기보다, 외부 시스템 연동, 정책 교체, 테스트 대체처럼 실제 이득이 있는 지점부터 적용합니다.

실무에서는?

위치 추상화 방식
결제/알림/파일 저장소 외부 구현이 바뀔 수 있으므로 인터페이스 뒤에 숨김
Repository 저장 방식 변경 가능성을 인터페이스로 분리
정책 객체 할인, 수수료, 권한 판단처럼 규칙이 여러 개인 경우 전략 인터페이스 사용
Service 의존성 구현체를 직접 생성하지 않고 생성자 주입으로 받음
테스트 코드 인터페이스를 Fake/Mock 구현으로 바꿔 빠르게 검증
라이브러리 API 외부에 공개되는 계약은 작고 안정적으로 유지

정리

항목 설명
추상화 구현 아닌 역할 중심 설계
인터페이스 순수 역할 정의
추상클래스 역할 + 공통 구현
OCP 확장에 열려있고 변경에 닫혀있음
주의 과설계 방지, 복잡도 관리

관련 파일: - OOP 개요 — 객체지향 4원칙 흐름 - 상속 — 추상화의 기반 - 다형성 — 추상화의 활용 - 캡슐화 — 정보 은닉