Abstract Class
추상 클래스
인스턴스화할 수 없는 클래스. 공통 인터페이스 정의. 상속을 통해 구현.
추상 클래스
인스턴스화할 수 없는 클래스. 공통 인터페이스 정의. 상속을 통해 구현.
Abstract Class(추상 클래스)는 직접 인스턴스를 생성할 수 없고, 반드시 상속을 통해서만 사용할 수 있는 클래스입니다. 하나 이상의 추상 메서드(abstract method)를 포함하며, 자식 클래스가 이 메서드들을 반드시 구현하도록 강제합니다. Java에서는 abstract 키워드로, Python에서는 abc 모듈의 ABC 클래스와 @abstractmethod 데코레이터로 정의합니다.
추상 클래스의 핵심 목적은 "템플릿 메서드 패턴" 구현입니다. 공통 로직은 추상 클래스에 구현하고, 변경되는 부분만 추상 메서드로 선언하여 자식 클래스에서 오버라이드하게 합니다. 예를 들어 결제 시스템에서 PaymentProcessor 추상 클래스가 검증-처리-완료 흐름을 정의하고, 각 결제 수단(카드, 계좌이체, 간편결제)별로 실제 처리 로직만 구현하는 방식입니다.
Interface와의 차이점을 이해하는 것이 중요합니다. Interface는 100% 추상적이어서 구현 코드가 없지만, Abstract Class는 일부 구현을 포함할 수 있습니다. Java 8 이후 Interface에 default 메서드가 추가되어 경계가 흐려졌지만, 상태(필드)를 가질 수 있는 것은 여전히 Abstract Class만의 특징입니다. 실무에서는 "is-a" 관계면 Abstract Class, "can-do" 관계면 Interface를 선택합니다.
대규모 프로젝트에서 추상 클래스는 코드 재사용성과 유지보수성을 크게 향상시킵니다. Spring Framework의 AbstractController, 안드로이드의 BaseActivity 패턴처럼 공통 기능을 한 곳에 모으고, 팀 전체가 일관된 구조로 개발할 수 있게 합니다. 단, 상속은 강한 결합을 만들기 때문에 과도한 상속 계층은 피하고, 가능하면 컴포지션을 우선 고려해야 합니다.
// Java - 결제 시스템 추상 클래스 예제
public abstract class PaymentProcessor {
protected String merchantId;
protected double amount;
public PaymentProcessor(String merchantId, double amount) {
this.merchantId = merchantId;
this.amount = amount;
}
// 템플릿 메서드: 전체 결제 흐름 정의
public final PaymentResult process() {
validate(); // 1. 검증 (공통)
PaymentResult result = executePayment(); // 2. 실제 결제 (추상)
saveTransaction(result); // 3. 저장 (공통)
return result;
}
// 공통 구현
protected void validate() {
if (amount <= 0) {
throw new IllegalArgumentException("결제 금액은 0보다 커야 합니다");
}
System.out.println("검증 완료: " + amount + "원");
}
protected void saveTransaction(PaymentResult result) {
System.out.println("거래 저장: " + result.getTransactionId());
}
// 추상 메서드: 자식 클래스에서 반드시 구현
protected abstract PaymentResult executePayment();
public abstract String getPaymentMethod();
}
// 구체 클래스: 카드 결제
public class CardPayment extends PaymentProcessor {
private String cardNumber;
public CardPayment(String merchantId, double amount, String cardNumber) {
super(merchantId, amount);
this.cardNumber = cardNumber;
}
@Override
protected PaymentResult executePayment() {
// 실제 카드 결제 로직
String txId = "CARD-" + System.currentTimeMillis();
System.out.println("카드 결제 처리: " + cardNumber.substring(0, 4) + "****");
return new PaymentResult(txId, true, amount);
}
@Override
public String getPaymentMethod() {
return "CREDIT_CARD";
}
}
// 사용 예시
PaymentProcessor payment = new CardPayment("SHOP001", 50000, "1234567890123456");
PaymentResult result = payment.process();
// 출력:
// 검증 완료: 50000.0원
// 카드 결제 처리: 1234****
// 거래 저장: CARD-1706123456789
"결제 모듈 설계할 때 PaymentProcessor를 추상 클래스로 만들면 좋겠습니다. 검증이랑 로깅은 공통으로 부모에 두고, 실제 결제 로직만 각 PG사별로 구현하면 됩니다. 나중에 토스페이먼츠 추가해도 추상 메서드만 구현하면 바로 붙일 수 있어요."
"Abstract Class와 Interface의 선택 기준은 '관계의 성격'입니다. Animal 추상 클래스는 Dog 'is-a' Animal 관계를 표현하고, Flyable 인터페이스는 Bird 'can-do' fly 능력을 표현합니다. 또한 추상 클래스는 상태를 가질 수 있어서 protected 필드로 자식 클래스에 공유할 수 있지만, 자바에서는 단일 상속 제한이 있다는 단점도 있습니다."
"이 process() 메서드에 final 붙이신 거 좋네요. 템플릿 메서드 패턴에서는 자식 클래스가 전체 흐름을 오버라이드 못하게 막아야 합니다. 근데 validate()는 protected로 열어둬서 필요하면 확장할 수 있게 하셨네요. 이 균형이 딱 좋습니다."
AbstractBase → AbstractMiddle → AbstractDetail → ConcreteClass처럼 3단계 이상 상속은 디버깅 악몽입니다. 상속 깊이는 2단계 이내로 유지하고, 필요하면 컴포지션으로 리팩토링하세요.
모든 메서드가 추상이면 Interface를 쓰세요. Abstract Class의 가치는 '공통 구현 + 확장 포인트' 조합에 있습니다. 추상 메서드는 진짜 변경이 필요한 부분만 선언하세요.
런타임에야 에러가 나서 버그 발견이 늦어집니다. ABC와 @abstractmethod를 쓰면 인스턴스 생성 시점에 바로 TypeError가 발생해서 빠른 피드백을 받을 수 있습니다.
Java에서 전체 흐름을 정의하는 템플릿 메서드는 final로 선언하세요. 자식 클래스가 실수로 전체 로직을 덮어쓰는 것을 방지하고, "이 메서드는 변경하지 마세요"라는 의도를 명확히 전달합니다.