
인터페이스는 왜 필요할까를 구현 분리 한 줄로만 설명하면 설계의 핵심을 놓치기 쉽습니다. 물론 구현 분리도 맞는 말입니다. 하지만 실무에서 더 중요한 이유는 따로 있습니다. 인터페이스는 클래스 이름을 가리는 장식이 아니라 이 객체가 어떤 역할을 맡는지 먼저 정하게 만드는 도구입니다. 그래서 설계가 조금 더 선명해지고, 변경 비용도 줄어들고, 테스트도 쉬워집니다.
역할

인터페이스의 핵심은 구현체가 아니라 역할입니다. 예를 들어 주문 완료 후 알림을 보내야 한다고 해보겠습니다. 이때 중요한 질문은 “이 클래스가 Email인가 Slack인가”가 아닙니다. 먼저 나와야 하는 질문은 “주문 서비스가 정말 알아야 하는 것은 무엇인가”입니다.
주문 서비스 입장에서는 알림을 보낼 수 있다는 사실만 알면 충분할 수 있습니다.
interface Notifier {
void send(String message);
}
class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("EMAIL :: " + message);
}
}
class SlackNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SLACK :: " + message);
}
}
class OrderService {
private final Notifier notifier;
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
public void completeOrder() {
notifier.send("order completed");
}
}여기서 중요한 것은 OrderService가 Email 전송 방식까지 알지 않아도 된다는 점입니다. 즉, 인터페이스는 구현을 감추는 것보다 먼저 의존하는 쪽이 꼭 알아야 할 것만 남기게 만든다는 데 가치가 있습니다.
경계
인터페이스가 있으면 클래스 사이 경계가 분명해집니다. 경계가 흐리면 한 클래스가 다른 클래스의 세부 구현까지 자연스럽게 파고들기 쉽습니다. 그러면 처음에는 편해 보여도 나중에는 변경 하나가 여러 군데로 번집니다.
반대로 인터페이스가 역할 경계를 잡아주면, 사용하는 쪽은 “무엇을 요청할 수 있는지”만 알고, 구현하는 쪽은 “어떻게 처리할지”를 내부에서 바꿀 수 있습니다.
인터페이스의 진짜 장점은 구현 교체 자체보다 변경이 퍼지는 범위를 줄여주는 데 있습니다.
변경 비용
변경 비용 관점에서 보면 인터페이스의 의미가 더 잘 보입니다. 만약 주문 서비스가 처음부터 EmailNotifier에 직접 의존하고 있다면, 나중에 Slack 알림이나 푸시 알림이 추가될 때 호출하는 쪽 코드도 같이 흔들릴 가능성이 큽니다. 하지만 역할 계약에 의존하고 있으면 구현체를 바꾸는 변화가 더 좁은 범위 안에 머물 수 있습니다.
class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS :: " + message);
}
}이때 바뀌는 것은 대부분 구현 선택 지점입니다. 서비스 본문 로직까지 함께 뜯어고칠 가능성은 훨씬 줄어듭니다. 이런 구조는 알림뿐 아니라 결제, 저장소, 로그, 외부 API 호출처럼 나중에 바뀔 수 있는 영역에서 특히 힘을 발휘합니다.
테스트
테스트 가능성도 인터페이스의 큰 장점입니다. 테스트가 쉽다는 말은 단순히 Mockito를 쓰기 좋다는 뜻이 아닙니다. 더 중요한 것은 핵심 로직을 검증할 때 외부 구현체를 꼭 같이 끌고 오지 않아도 된다는 점입니다.
class FakeNotifier implements Notifier {
private String lastMessage;
@Override
public void send(String message) {
this.lastMessage = message;
}
public String getLastMessage() {
return lastMessage;
}
}이런 테스트 대역이 가능하면 주문 서비스가 정말 알림을 보내는지, 어떤 메시지를 만드는지만 가볍게 확인할 수 있습니다. 메일 서버나 슬랙 API 같은 바깥 환경을 테스트마다 끌고 올 필요가 없습니다. 즉, 인터페이스는 테스트 코드를 예쁘게 만들기 위한 장식이 아니라 핵심 로직을 주변 기술에서 분리해서 검증 가능하게 만드는 장치이기도 합니다.
협업
협업 구조에서도 인터페이스는 꽤 유용합니다. 한 팀은 도메인 로직을 만들고, 다른 팀은 인프라 구현을 맡는 상황을 떠올려보면 이해가 쉽습니다. 이때 역할 계약이 먼저 정리되어 있으면 각 팀이 같은 목표를 보면서도 작업을 병렬로 진행하기가 좋아집니다.
도메인 팀은 “무엇이 필요하다”를 인터페이스로 표현하고, 구현 팀은 그 계약을 만족하는 실제 코드를 붙이면 됩니다. 역할을 먼저 정해두면 의사소통 비용이 줄어든다는 점이 핵심입니다.
오해
구현체가 하나면 의미 없다
이 말은 반만 맞습니다. 당장 구현체가 하나뿐이라도, 경계를 분명히 두는 가치가 큰 지점이라면 인터페이스는 여전히 의미가 있을 수 있습니다. 예를 들어 도메인 로직이 외부 결제 모듈과 분리되어야 한다면, 지금 구현체가 하나여도 역할 계약이 설계상 중요할 수 있습니다.
반대로 앞으로도 바뀔 일 없고, 경계 분리 이점도 거의 없고, 그냥 내부에서만 쓰는 단순 헬퍼라면 인터페이스가 없어도 됩니다. 핵심은 구현체 개수보다 경계의 가치입니다.
인터페이스를 만들면 무조건 유연하다
이것도 아닙니다. 아무 생각 없이 UserService, UserServiceImpl처럼 한 쌍을 습관적으로 만들면 파일만 늘고 구조는 더 읽기 어려워질 수 있습니다. 역할 차이도 없고, 대체 가능성도 없고, 테스트 이점도 없는데 인터페이스만 따로 두면 형식만 남습니다.
즉, 인터페이스는 유연함을 자동 생성해 주는 버튼이 아닙니다. 역할 분리 이유가 분명할 때만 가치가 커집니다.
불필요한 경우
- 단순한 내부 구현이고 바뀔 가능성이 거의 없다
- 호출하는 쪽과 구현하는 쪽 경계를 굳이 나눌 이유가 없다
- 테스트 대역이 필요할 정도의 외부 의존성도 없다
- 오히려 클래스 하나로 읽는 편이 더 자연스럽다
이럴 때는 클래스 하나로 시작하는 편이 더 낫습니다. 처음부터 모든 곳에 인터페이스를 깔아두는 습관은 설계를 가볍게 만드는 것이 아니라, 오히려 판단을 미루는 방식이 될 수 있습니다.
추상 클래스
인터페이스는 역할 계약에 더 가깝고, 추상 클래스는 공통 상태와 공통 흐름을 묶는 데 더 가깝습니다. Oracle의 Abstract Methods and Classes, Interfaces, Default Methods 문서도 이 차이를 설명합니다.
공통 필드, 생성자, 기본 동작 골격이 중요하면 추상 클래스가 더 자연스러울 수 있습니다. 반대로 서로 다른 구현체가 같은 역할만 맞추면 되고, 여러 타입 조합이나 교체 가능성이 중요하면 인터페이스가 더 잘 맞습니다.
즉, 인터페이스는 “부모 클래스 대신 최신식으로 쓰는 것”이 아닙니다. 설계에서 역할 중심으로 경계를 잡고 싶을 때 특히 유용한 도구입니다.
체크리스트
- 이 클래스의 핵심은 구체 구현이 아니라 역할인가?
- 사용하는 쪽이 구현 세부를 몰라도 되는가?
- 나중에 구현체가 바뀌거나 추가될 가능성이 있는가?
- 테스트에서 외부 의존성을 분리하면 이득이 큰가?
- 팀이나 모듈 사이 경계를 먼저 정해두는 편이 좋은가?
이 질문에 여러 개가 “예”라면 인터페이스가 꽤 좋은 선택일 수 있습니다. 반대로 거의 모두 “아니오”라면 클래스로 바로 가도 괜찮습니다.
정리
인터페이스는 구현체 숨기기 도구 정도로만 보면 가치가 작아 보일 수 있습니다. 하지만 실제로는 역할을 먼저 보게 만들고, 의존성 방향을 정리하고, 변경 비용을 줄이고, 테스트 가능한 구조를 만드는 데 큰 도움을 줍니다. 그래서 인터페이스를 쓸지 말지는 “구현체가 몇 개인가”보다 이 경계를 분리하는 것이 설계에 정말 이득인가로 판단하는 편이 더 좋습니다.
같이 읽으면 좋은 글은 객체지향은 왜 클래스 많이 만드는 기술이 아닐까, 좋은 객체지향 설계는 의존성을 어떻게 다룰까, 객체지향에서 책임을 잘 나누는 기준, 추상 클래스와 인터페이스 차이입니다.