인터페이스를 “추상 클래스랑 비슷한 문법” 정도로 기억하기 쉽습니다.
하지만 인터페이스는 단순 문법이 아니라, 변경 비용을 통제하고 테스트/확장을 가능하게 하는 설계 도구입니다.
DB가 바뀌거나, 외부 API가 교체되거나, 캐시 / 메시징이 추가되더라도 서비스 로직이 크게 흔들리지 않게 만드는 핵심 장치가 바로 “계약”입니다.
인터페이스는 그 계약을 코드 레벨에서 가장 명확하게 표현하는 방법 중 하나입니다.
즉, 인터페이스는 “기능 목록”이 아니라 의존성을 역전시키는 경계입니다.
상위 계층(Service/Domain)이 하위 구현(DB/Redis/외부 API)의 구체 타입에 직접 의존하지 않고, 인터페이스(계약)에 의존하게 만들면 구현을 교체하거나 테스트를 구성하기가 훨씬 쉬워집니다.
인터페이스는 객체지향에서 변경에 강한 코드를 만들기 위한 핵심 장치입니다.
코드는 “구현체 클래스”가 아니라 “인터페이스(계약)”에 의존하게 만들고, 실제 동작은 구현체가 책임지도록 분리합니다.
이렇게 하면 구현을 바꿔도(예: ArrayList → LinkedList), 사용하는 쪽 코드는 거의 바꾸지 않아도 됩니다. 즉, 인터페이스는 결합도를 낮추고, 확장성/테스트 용이성을 높입니다.
“인터페이스는 코드를 분리하는 문법이 아니라,
변경과 테스트를 설계하는 계약이다.”
// 1) 계약: 무엇을 할 수 있는지 정의 interface SmsSender { void send(String phone, String message); } // 2) 구현 A: Vendor1 class Vendor1SmsSender implements SmsSender { public void send(String phone, String message) { // vendor1 API 호출 (생략) } } // 3) 구현 B: Vendor2 (교체/추가 가능) class Vendor2SmsSender implements SmsSender { public void send(String phone, String message) { // vendor2 API 호출 (생략) } } // 4) 상위 계층은 인터페이스에만 의존 class SignUpService { private final SmsSender smsSender; public SignUpService(SmsSender smsSender) { this.smsSender = smsSender; } public void sendVerificationCode(String phone) { String code = "123456"; // 생성 로직 생략 smsSender.send(phone, "[인증코드] " + code); } }
💡 TIP / 백엔드 실무 포인트
“SmsSender 인터페이스만 만들면 끝”이 아닙니다. 실무에서는 실패 처리 전략이 계약에 같이 들어가야 안정적입니다.
예: 타임아웃/재시도/백오프, 실패 시 예외 타입, 장애 전파 범위(서킷 브레이커), 멱등성 키 등
👍 GOOD
- 작고 명확한 기능 단위로 분리됨
- 호출자가 실패/예외 규약을 이해하기 쉬움
- 구현 교체가 자연스럽고 테스트가 쉬움
- 비즈니스 용어(도메인)가 이름에 반영됨
👎 BAD
- “만능 인터페이스”로 모든 기능을 몰아넣음
- 구현체마다 의미가 다른 메서드(계약 불일치)
- 내부 구현 디테일(테이블/벤더 에러)을 외부가 알아야 함
- 서비스 코드가 특정 구현체로 캐스팅해서 사용
💡 TIP / “인터페이스를 언제 만들까?”
인터페이스는 “처음부터 무조건”이 아니라, 변경이 실제로 발생하거나 발생 가능성이 큰 지점에 두는 게 효율적입니다.
대표: 외부 연동(Client), 저장소(Repository/Storage), 정책(Policy), 메시징(Publisher), 캐시(CacheStore)
예를 들어 결제 로직이 특정 PG사의 SDK에 직접 묶여 있다면, PG를 바꾸는 순간 코드 곳곳이 수정됩니다. 반대로 “PaymentGateway” 같은 인터페이스로 감싸면, PG 변경은 구현체 교체로 끝납니다.
자바에서의 인터페이스
자바에서 interface는 “메서드 시그니처(무엇을 제공하는지)”를 정의합니다. 구현체 클래스는 이 계약을 반드시 구현해야 하며, 호출자는 구현체가 무엇인지 몰라도 인터페이스 타입으로 동일한 방식으로 사용할 수 있습니다.
1
여러 구현 전략이 공존하는 지점 예: 캐시 전략(Local/Redis), 저장 전략(File/S3)처럼 전략이 갈릴 때 “전략 인터페이스”가 유리하다.
// Java - 인터페이스(계약) public interface Notifier { void send(String to, String message); } // 구현체 1: 이메일 public class EmailNotifier implements Notifier { @Override public void send(String to, String message) { System.out.println("Email to " + to + ": " + message); } } // 구현체 2: SMS public class SmsNotifier implements Notifier { @Override public void send(String to, String message) { System.out.println("SMS to " + to + ": " + message); } } // 사용하는 쪽(호출자)은 Notifier만 알면 된다. public class AlarmService { private final Notifier notifier; public AlarmService(Notifier notifier) { this.notifier = notifier; } public void alert(String user, String msg) { notifier.send(user, msg); } }
✅ 핵심 요약
- ✔️ 인터페이스는 구현을 숨기고 계약을 고정해 변경 비용을 줄이는 백엔드 설계 도구다.
- ✔️ 실무에서는 외부 연동/스토리지/캐시/메시징처럼 “바뀔 수 있는 곳”에서 효과가 크다.
- ✔️ 좋은 인터페이스는 작고 명확하며, 반환/예외 규약까지 포함해 호출자가 쉽게 사용할 수 있다.
- ✔️ “너무 이른 추상화”는 오히려 복잡도를 늘릴 수 있으니, 변경 가능성이 큰 지점부터 적용하는 것이 현실적이다.