
인터페이스 분리 원칙(ISP)은 SOLID 중에서도 실무 체감이 늦게 오는 편입니다. 하지만 큰 인터페이스가 호출자와 구현자를 한꺼번에 불편하게 만들기 시작하면, 그때부터는 설계 비용이 꽤 분명하게 보입니다.
이 글에서는 fat interface가 왜 문제인지, 언제 ISP가 실제로 중요해지는지, 역할 기반 인터페이스와 client-specific contract를 어떤 기준으로 적용하면 좋은지 실무 관점에서 정리하겠습니다.
ISP 한 줄 정리
핵심은 단순합니다. 클라이언트가 쓰지 않는 메서드까지 의존하게 만들지 말자는 것입니다. 메서드 수가 많다는 사실 자체보다, 서로 다른 호출자가 서로 다른 이유로 바뀌는 기능을 한 계약에 같이 묶어둘 때 문제가 커집니다.
즉, ISP는 인터페이스를 무조건 잘게 쪼개라는 말이 아닙니다. 변경 이유가 다른 호출자를 같은 계약에 억지로 묶지 말라는 원칙에 더 가깝습니다.
왜 ISP는 자주 놓쳐질까
- 처음에는 작았던 인터페이스에 기능을 하나씩 덧붙이기 쉽다
- 구현체 중심으로 보면 호출자별 사용 차이가 잘 보이지 않는다
- 오늘의 빠른 추가가 내일의 테스트 비용과 변경 비용으로 돌아온다
- SRP나 DIP보다 클라이언트 관점의 경계를 읽어야 해서 체감이 늦다
대부분의 팀이 처음부터 큰 인터페이스를 설계하는 것은 아닙니다. 문제는 운영 중에 새 요구사항이 들어올 때마다 기존 계약에 메서드 하나씩 추가하는 방식이 가장 쉬워 보인다는 점입니다. 그 결과 어느 순간 조회, 수정, 관리자 작업, 배치 작업이 한 인터페이스에 같이 들어가게 됩니다.
큰 인터페이스의 비용
억지 메서드
조회 전용 구현이나 읽기 전용 어댑터처럼 일부 역할만 자연스러운 구현체도 있습니다. 그런데 계약이 지나치게 넓으면 이런 구현체는 자신과 맞지 않는 메서드까지 억지로 구현해야 합니다. 그 결과 빈 구현, 예외 던지기, 의미 없는 no-op 메서드가 생기기 쉽습니다.
무거운 테스트 더블
내가 검증하려는 것은 조회 하나인데 목 객체가 취소, 정산, 관리자 기능까지 모두 흉내 내야 한다면 이미 계약이 너무 넓다는 뜻일 수 있습니다. 작은 계약은 테스트를 단순하게 만들고, 큰 계약은 테스트가 필요 없는 동작까지 알게 만듭니다.
넓은 변경 영향
관리자 기능 하나를 바꿨는데도 조회 클라이언트, 테스트 코드, 어댑터, 문서가 함께 흔들린다면 계약 경계가 잘못 잡혔을 가능성이 큽니다. 특히 여러 모듈이나 팀이 함께 쓰는 공용 계약에서는 이 비용이 더 크게 드러납니다.
이름이 흐려짐
이름은 OrderService인데 실제로는 조회, 취소, 정산, 통계, 내보내기까지 다 들어 있으면 역할보다 구현 덩어리를 그대로 노출하는 이름이 됩니다. 이런 계약은 읽는 사람에게 API 의도보다 내부 조직도를 먼저 보여줍니다.
fat interface 신호
- 특정 클라이언트가 인터페이스 메서드 중 일부만 반복해서 사용한다
- 구현체에서 UnsupportedOperationException이나 빈 구현이 자주 나온다
- 테스트 더블을 만들 때 필요 없는 메서드를 많이 채워야 한다
- 메서드 묶음마다 호출 주체와 변경 이유가 다르다
- 인터페이스 이름이 너무 포괄적이라 역할이 잘 드러나지 않는다
중요한 것은 메서드 수가 아니라, 사용 패턴이 갈라졌는데도 여전히 하나의 계약으로 묶여 있다는 점입니다.
역할 기반 분리
역할 기반 인터페이스는 구현 클래스 중심이 아니라 사용 역할 중심으로 계약을 자르는 접근입니다. 예를 들어 문서 저장소를 읽기와 쓰기로 나누면 조회 화면은 읽기 계약만 알면 되고, 수정 화면이나 배치는 쓰기 계약까지 의존하면 됩니다.
public interface DocumentReader {
Document findById(String id);
List<Document> search(String keyword);
}
public interface DocumentWriter {
void save(Document document);
void delete(String id);
}이 구조의 장점은 단순히 메서드 수를 줄이는 데 있지 않습니다. 읽기 클라이언트는 읽기 변경에만, 쓰기 클라이언트는 쓰기 변경에만 더 가깝게 묶이게 됩니다.
client-specific contract
같은 도메인을 다뤄도 호출자마다 필요한 기능이 다르면, 클라이언트별 계약이 더 자연스럽습니다. 결제 시스템을 예로 들면 웹 결제 화면, 정산 배치, 운영 도구는 모두 다른 기능 집합을 필요로 합니다.
public interface PaymentRequester {
PaymentResult requestPayment(PaymentCommand command);
PaymentResult getPaymentResult(String paymentId);
}
public interface SettlementProcessor {
List<Settlement> findSettlements(LocalDate date);
void markSettled(String settlementId);
}
public interface PaymentAdminActions {
void refund(String paymentId);
void forceCancel(String paymentId);
}포인트는 클래스 수를 늘리는 것이 아닙니다. 웹 화면, 배치, 운영 도구가 서로 다른 속도로 변한다면 계약도 그 경계를 반영해야 한다는 것입니다. 이 감각은 DIP를 실무적으로 이해하는 글과도 연결됩니다.
ISP가 중요한 순간
- 읽기와 쓰기의 기술적 성격이 다를 때
- 일반 사용자, 관리자, 배치처럼 사용자 역할이 다를 때
- 테스트 준비 비용이 눈에 띄게 커질 때
- 모듈 경계나 팀 경계가 있어 계약 변경 전파가 비쌀 때
반대로 호출자가 거의 하나이고, 메서드들이 항상 함께 쓰이고, 구현체도 같은 기능 집합을 자연스럽게 제공한다면 무리하게 쪼갤 필요는 없습니다. ISP의 목적은 파일 수를 늘리는 것이 아니라 변경 영향 범위를 줄이는 것입니다.
언제 나눌까
- 이 메서드 묶음은 같은 클라이언트들이 함께 쓰는가
- 이 메서드들은 같은 이유로 함께 바뀌는가
- 일부 구현체가 특정 메서드를 자연스럽게 지원하지 못하는가
- 테스트 더블이 불필요하게 무거워졌는가
- 분리 후 인터페이스 이름이 더 선명해지는가
이 질문에 여러 개가 그렇다면 분리 가치가 높습니다. 반대로 분리 후에도 모든 호출자가 여전히 같은 조합으로 묶여 사용한다면, 그 분리는 설계 개선보다 구조 분산에 가깝습니다.
자주 나오는 오해
작을수록 좋은가
작다고 항상 좋은 것은 아닙니다. 한 클라이언트가 항상 같이 쓰는 메서드까지 인위적으로 찢어놓으면 사용성이 나빠지고 탐색 비용이 늘어납니다. ISP의 기준은 최소 단위가 아니라 자연스러운 의존성 경계입니다.
하나면 위반인가
아닙니다. 하나의 인터페이스가 충분히 응집적이고 같은 클라이언트들이 같은 이유로 함께 쓴다면 문제 없습니다. 중요한 것은 개수가 아니라 계약의 방향입니다.
구현체 하나면 불필요한가
구현체 수는 판단 요소 중 하나일 뿐입니다. 구현체가 하나여도 클라이언트 경계를 좁히고 테스트 대상을 작게 만드는 데 인터페이스가 의미 있을 수 있습니다. 이 부분은 인터페이스는 왜 필요할까 글과도 맞닿아 있습니다.
DIP도 해결될까
좋게 나눈 인터페이스가 항상 좋은 추상화는 아닙니다. 이름, 책임, 안정성까지 함께 봐야 합니다. 그래서 ISP는 DIP를 돕지만, 자동으로 대신해주지는 않습니다.
실무 판단 기준
- 구현체보다 호출자부터 본다
- 쓰지 않는 메서드 의존이 생기면 분리를 검토한다
- 읽기/쓰기, 사용자/관리자, 실시간/배치처럼 변화 축이 다른지 본다
- 빈 구현이나 예외 던지기가 반복되면 계약 설계를 다시 본다
- 분리 후 이름이 더 선명해지는지 확인한다
짧게 정리하면 구현체보다 클라이언트를 먼저 보라는 뜻에 가깝습니다. ISP는 인터페이스를 무조건 잘게 나누는 기술이 아니라, 호출자에게 필요한 만큼만 약속하게 만드는 원칙입니다.
정리
인터페이스 분리 원칙(ISP)은 처음엔 추상적으로 들리지만, 실제로는 꽤 현실적인 원칙입니다. 큰 인터페이스는 불필요한 의존성, 테스트 비용 증가, 변경 영향 확대로 이어지기 쉽습니다.
그래서 중요한 질문은 메서드가 몇 개냐가 아니라, 서로 다른 클라이언트가 서로 다른 이유로 바뀌는 기능을 한 계약에 같이 묶고 있느냐입니다.
관련 글로는 SOLID 원칙, 실무에서는 어떻게 봐야 할까, 리스코프 치환 원칙(LSP)은 왜 자주 오해될까, 의존 역전 원칙(DIP)을 실무적으로 이해하는 기준, 상속보다 조합이 더 나은 순간을 이어서 보면 흐름이 더 잘 잡힙니다.
외부 근거를 함께 보고 싶다면 Oracle의 Interfaces, Abstract Methods and Classes, Multiple Inheritance of State, Implementation, and Type, Default Methods 문서를 참고하면 좋습니다.