결론부터 말하자면, 특정 기능(결제, 배송 등)을 단순히 규약(메서드 시그니처) 으로만 정의할지, 아니면 공통 로직을 일부 구현해놓을 필요가 있는지를 판단해보면 된다.
1. 추상 클래스를 고려해볼 만한 경우
- 공통된 코드, 상태가 반드시 존재하고, 이를 그대로 재사용하고 싶을 때
- 예: 결제 로직에서 모든 결제 수단에 공통적으로 적용해야 하는 메서드가 있다면(수수료 계산의 기본 로직, 로그 기록 등).
- “is-a” 관계로 명확히 묶이는 클래스들이고, 단일 상속 구조로 관리해도 무방할 때
- 자식 클래스가 반드시 상속을 받아야 하며, 다른 클래스를 중복해서 상속받을 필요가 없을 때.
2. 인터페이스를 고려해볼 만한 경우
- 다양한 클래스가 동일한 기능(메서드 시그니처) 을 각자 다르게 구현해야만 할 때
- 예: 결제 수단이 전혀 공통 로직 없이, 단지
pay() 메서드만 달리 구현하는 상황.
- 상속과 무관하게, 다른 클래스(이미 다른 상위 클래스를 상속 중인)라도 ‘이 기능을 구현’할 수 있게 하고 싶을 때
- 자바는 클래스 단일 상속이므로, 이미 상속받은 부모가 있는 클래스가 또 다른 추상 클래스를 상속받기는 불가능하지만, 인터페이스는 여러 개
implements 가능.
판단 기준 정리
-
공통 코드(필드·메서드)를 제공할 필요가 있는가?
- 있다면 → 추상 클래스 고려
- 없다면 → 인터페이스로 간단히 규약만 정의
-
여러 개의 부모(추상 클래스)를 동시에 상속받아야 할 가능성은?
- 자바는 단일 상속만 지원하므로, 이미 다른 클래스를 상속받는 중이면 추상 클래스를 더 이상 상속 못 한다.
- 이 경우 인터페이스를 선택해 다중 구현이 가능하도록 하는 게 이점이 될 수 있음.
-
현실 세계 모델링(‘is-a’ vs ‘can do’)
- 추상 클래스: “결제는 곧 하나의 결제 방식”처럼 클래스 간 상속 계층을 더 강조(“is-a” 관계).
- 인터페이스: “모든 결제 수단은 결제 기능을 수행할 수 있다”처럼, 행위를 구현(“can do” 관계).
예시로 본다면
- 결제 로직에서 공통할 수밖에 없는 코드(예: 결제 기록, 기본 에러 처리, 결제 시점 로그 남기기 등)를 자주 쓰고, 상속받는 클래스들이 이것을 그대로 재사용해야 한다면 → 추상 클래스가 더 편하다.
- 결제 수단마다 거의 전혀 다른 형태로 동작하고, 공통 로직도 별로 없으며, 오직 “
pay() 메서드가 있다”만 보장하면 되는 경우 → 인터페이스로 충분하다.
요약
- 추상 클래스와 인터페이스 둘 다 “추상화”를 제공하지만, 부분 구현(필드·메서드)을 제공해야 하는지, 다중 상속이 필요한지에 따라 선택이 달라진다.
- “결제”를 추상 클래스로 만들려면, 여러 결제 방식들(카드, 모바일, 쿠폰 등)이 공통 메서드를 상당 부분 공유할 필요가 있어야 한다.
- 만약 특정 결제 수단이 이미 다른 클래스를 상속받고 있다면 추상 클래스를 추가로 상속받기 어렵다. 이럴 땐 인터페이스가 낫다.
즉,
1) 상속 구조에서 공통 로직·필드가 필요하면 추상 클래스,
2) 특정 행위만 규약하고 다중 구현이 필요하거나 이미 상속 트리가 있는 경우에는 인터페이스를 쓰면 된다.