문득 퇴근 후 개발 공부를 하다가 'interface 가 있는데 abstract 가 왜 필요할까?' 라는 궁금증에 정리해보았습니다.
"문법은 알겠고, abstract 붙이면 상속받아서 구현해야 하고, new 로 객체 생성을 못하고... 그러면 인터페이스랑 똑같네?"
만약 위와 같은 의문이 드셨으면 제가 이번에 정리한 글을 보시면 왜 abstract 를 사용하는지 알 수 있습니다.
이 글을 통해 abstract 의 문법적 기초부터, 실무에서 인터페이스 대신 추상 클래스를 선택해야 하는 경우를 정리 해 보겠습니다.
추상의 개념
미술에서 추상화(Abstract Art) 는 피카소의 그림처럼 형태를 알아보기 힘들게 핵심만 남겨두는 것입니다.
소프트웨어에서도 똑같습니다. 추상화란 "지엽적이고 구체적인 세부 사항은 숨기고, 핵심적인 개념과 공통적인 특징만 뽑아내는 것" 을 의미합니다.
자바에서 abstract 키워드는 "이 클래스는 아직 미완성이니, 직접 객체로 만들지 말고 나를 상속받아서 구체적으로 완성해라" 라는 일종의 가이드라인이자 강제제약입니다.
왜 사용할까?
"그냥 일반 클래스로 만들고 자식들이 알아서 오버라이딩하게 두면 안되나요? 왜 굳이 미완성으로 만들어서 제약을 거나요?"
추상 클래스를 사용하는 진짜 이유는 크게 두가지입니다.
실수를 방지하는 강제성
만약 일반 클래스에 껍데기만 있는 메서드를 만들어두면, 동료 개발자가 이를 상속받았을 때 실수로 메서드를 구현하지 않고 그냥 넘어갈 수 있습니다. 이 경우 런타임에 아무일도 일어나지 않거나 엉뚱한 버그가 발생하게 됩니다.
하지만 abstract 를 붙여 추상 메서드로 선언하면, 자식 클래스에서 이를 구현하지 않았을 때 컴파일러가 에러를 뿜어냅니다. 시스템의 안전성을 극대화 시켜 주는 것입니다.
중복 코드 제거와 공통 규격 생성
여러 클래스를 만들다 보면 반드시 공통으로 들어가는 중복 코드가 생깁니다.
추상 클래스는 "공통적인 실행 흐름과 데이터(상태) 는 내가 다 가지고 있을 테니, 너희 자식들은 진짜 달라지는 핵심 알맹이만 구현해" 할 때 최고의 효율을 발휘하게 됩니다.
실무 코드(템플릿 메서드 패턴)
자바 웹 개발에서 가장 많이 쓰이는 구조인 '백화점 상품 결제 시스템' 을 예시로 들어보겠습니다.
우리 시스템에는 신용카드 결제(CardPayment) 와 네이버페이 결제(NaverPayPayment) 두 가지 방식이 있다고 가정해보겠습니다.
추상 클래스를 활용한 결제 시스템 설계
// 결제 틀 정의
public abstract class PaymentProcessor {
// 공통 상태를 가질 수 있음 (인터페이스와의 큰 차이점!)
protected String companyName = "우리 백화점";
// 템플릿 메서드: 결제의 전체적인 흐름을 제어함 (final로 흐름 변조 방지)
public final void processPayment(int amount) {
validate(amount);
executePayment(amount);
logResult();
}
// 공통 메서드
private void validate(int amount) {
if (amount <= 0) throw new IllegalArgumentException("금액이 올바르지 않습니다.");
System.out.println("[" + companyName + "] 결제 검증 완료: " + amount + "원");
}
// 추상 메서드
protected abstract void executePayment(int amount);
// 공통 메서드
private void logResult() {
System.out.println("결제 완료 로그를 DB에 저장하고 알림을 보냅니다.\n");
}
}
이제 이 미완성 설계도를 상속받아 구체적인 결제 수단들을 만듭니다.
// 신용카드 결제 구현체
public class CardPayment extends PaymentProcessor {
@Override
protected void executePayment(int amount) {
// 신용카드 API 연동 핵심 로직만 작성
System.out.println("[신용카드] PG사 요청 전송 -> " + amount + "원 승인 완료");
}
}
// 3. 네이버페이 결제 구현체
public class NaverPayPayment extends PaymentProcessor {
@Override
protected void executePayment(int amount) {
// 네이버페이 API 연동 핵심 로직만 작성
System.out.println("[네이버페이] 인증 토큰 확인 -> " + amount + "원 포인트 차감 완료");
}
}
public class Main {
public static void main(String[] args) {
// PaymentProcessor processor = new PaymentProcessor(); // 에러! 추상 클래스는 new 불가
PaymentProcessor card = new CardPayment();
card.processPayment(50000);
PaymentProcessor naver = new NaverPayPayment();
naver.processPayment(30000);
}
}
만약 새로운 결제 수단인 '카카오페이 결제' 가 추가되어도, 개발자는 전체흐름을 새로 짤 필요가 전혀 없습니다.
그저 PaymentProcessor 를 상속받아 executePayment 메서드 안에 카카오페이 API 호출 코드 딱 한 단락만 채워 넣으면 끝납니다.
이 설계 방식을 디자인 패턴에서는 템플릿 메서드 패턴이라고도 부릅니다.
interface 에 default 가 있는데?
자바 8 부터 인터페이스에도 default 메서드가 들어가서 위 코드처럼 바디( {} ) 가 있는 메서드를 넣을 수 있으니 abstract 는 필요없는거 아니냐? 라는 의문이 들 수 있습니다.
결론부터 말씀드리자면, 둘은 태생적인 목적과 존재 이유가 완전히 다릅니다.
abstract 는 상속(Is-A) 관계를 통한 확장 및 코드 재사용이 용이하며 가장 큰 특징인 인스턴스 변수를 가질수가 있습니다.
또한 자바 특성상 단일 상속만 가능합니다.
하지만 인터페이스는 행위(Can-Do) 에 대한 규격과 계약만을 정의하며 변수를 가질 수가 없습니다.
interface 는 public static final 의 상수만 정의할 수 있습니다.
"상태" 를 가질 수 있는가?
자바 8, 9 를 거치며 인터페이스가 아무리 발전했어도 인터페이스는 '인스턴스 변수' 를 가질 수 없습니다.
위의 결제 예시 코드에서 protected String companyName; 같은 상태값을 저장하고 자식들이 이 필드를 공유하거나 변경하게 하려면 오직 추상 클래스만 가능합니다. 인터페이스는 상태를 가질 수 없으므로 상태를 제어하는 공통 로직을 온전히 담기가 어렵습니다.
소스코드 뜯어보기
자바 소스코드를 뜯어보면 Collection 인터페이스가 있고, 그 아래에 공통 로직을 모아둔 AbstractCollection, AbstractList 추상 클래스가 있으며, 이를 상속받아 우리가 매일 쓰는 ArrayList 가 구현되어 있습니다. 자바 언어의 창시자들도 인터페이스와 추상 클래스를 융합해서 최고의 효율을 내도록 설계해 둔 것이죠.
abstract 키워드는 단순히 '생성 못하는 클래스' 가 아닙니다. 동료 개발자들과 협업에서 "이 프로세스 흐름은 절대 건드리지 말고, 너희는 요 알맹이만 안전하게 구현해줘" 라고 코드로 대화하는 강력한 소통 도구이기도 합니다.
'Java' 카테고리의 다른 글
| [ 주니어 탈출기 ] 고수준 컴포넌트와 저수준 컴포넌트 (0) | 2026.06.02 |
|---|---|
| 1급 컬렉션 (0) | 2026.05.26 |
| [ 주니어 탈출기 ] 도메인(Domain) 개념 완벽 정리 (0) | 2026.05.18 |
| 좌충우돌 원격 서버 배포 및 Jenkins CI/CD 구축기 (0) | 2026.04.28 |
| 빈 검증에 대하여 (1) | 2026.03.23 |