F-lab Java 1주차 / Phase 3 / Unit 3.4 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 3.3 (LSP), Unit 2.4 (다형성)
다음 Unit: 3.5 — DIP (의존 역전 원칙) — SOLID의 마지막이 Unit의 의미: 거대 인터페이스의 함정을 해결.
"사용하지 않는 메서드에 의존하지 마라" — ISP의 핵심.
SRP의 인터페이스 버전이자 LSP를 안전하게 만드는 도구.
맥가이버 칼 (Swiss Army Knife):
전용 도구:
언제 무엇이?:
자바에서:
→ 대부분의 경우 전용 도구가 더 좋음 (ISP의 정신).
거대 인터페이스 = "만능 직원" 채용 공고:
"코딩, 디자인, 영업, 마케팅, 회계, 청소를 모두 할 수 있는 사람"
이런 사람을 찾을 수 있을까?
작은 인터페이스 = "전문가" 채용 공고:
"코딩 전문가" / "디자인 전문가" / "영업 전문가"
각자 한 가지를 잘함:
→ 이게 ISP. 한 인터페이스에 너무 많은 책임을 넣지 말 것.
"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."
ISP의 핵심:
비유 정리:
| 비유 요소 | ISP 적용 |
|---|---|
| 맥가이버 칼 | 거대 인터페이스 (안티패턴) |
| 전용 도구 | 작은 인터페이스 (좋은 설계) |
| 만능 직원 채용 | 거대 인터페이스 강제 |
| 전문가 채용 | 인터페이스 분리 |
ISP (Interface Segregation Principle) — Uncle Bob이 1990년대 정립.
원래 정의:
"Clients should not be forced to depend on methods they do not use."
("클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다.")
핵심 개념: Fat Interface (뚱뚱한 인터페이스) 의 안티패턴.
ISP의 등장 배경에는 실제 사례가 있습니다.
Xerox 의 새 프린터 시스템 (1990년대):
Job 이 모든 작업을 표현문제 발생:
Job 에 변경 → 다른 작업 코드 모두 재컴파일 필요Uncle Bob의 해결:
Job 인터페이스를 작업별 작은 인터페이스로 분리PrintJob, CopyJob, FaxJob, ScanJob 등→ 이 경험을 바탕으로 ISP가 정립.
ISP는 인터페이스 레벨의 SRP 라고 볼 수 있습니다:
| SRP | ISP | |
|---|---|---|
| 적용 대상 | 클래스 | 인터페이스 |
| 핵심 | 한 책임 | 한 책임 (인터페이스로) |
| 관점 | 변경 이유 | 클라이언트 사용 패턴 |
| 위반 안티패턴 | God Class | Fat Interface |
→ 두 원칙이 한 사고의 다른 표현. SRP는 클래스에, ISP는 인터페이스에.
LSP가 깨지는 흔한 이유 = Fat Interface:
// Fat Interface
public interface Bird {
void fly();
void swim();
void run();
}
// Penguin: fly() 못 함 → LSP 위반
public class Penguin implements Bird {
public void fly() { throw new UnsupportedOperationException(); }
public void swim() { ... }
public void run() { ... }
}
ISP로 분리 → LSP 자연스럽게 만족:
public interface Walking { void walk(); }
public interface Flying { void fly(); }
public interface Swimming { void swim(); }
public class Penguin implements Walking, Swimming { ... }
public class Eagle implements Walking, Flying { ... }
public class Duck implements Walking, Flying, Swimming { ... }
→ ISP가 LSP를 자연스럽게 보장.
"ISP는 'Fat Interface' 의 부담을 줄이는 원칙이다."
거대 인터페이스는 구현 클래스에게 사용하지 않는 메서드 구현 부담 을 강요한다. 이는 LSP 위반 (
UnsupportedOperationException), 변경 영향 폭발, 협업 충돌 등의 문제를 일으킨다.인터페이스를 클라이언트가 실제로 사용하는 단위로 분리 하면 이 부담이 사라지고, 시스템이 유연해진다. SRP의 사고를 인터페이스 레벨로 확장한 원칙.
ISP를 위반했을 때의 구체적 문제를 ILIC 시나리오로 보겠습니다.
처음에는 단순하게 시작:
public interface FareService {
Fare create(FareRequest request);
Fare findById(Long id);
List<Fare> findAll();
void update(Long id, FareRequest request);
void delete(Long id);
}
시간이 지나면서 메서드가 추가:
// ❌ Fat Interface
public interface FareService {
// CRUD
Fare create(FareRequest request);
Fare findById(Long id);
List<Fare> findAll();
List<Fare> findByCustomer(Long customerId);
List<Fare> findByDateRange(LocalDate from, LocalDate to);
void update(Long id, FareRequest request);
void delete(Long id);
// 상태 관리
void submit(Long id);
void approve(Long id);
void cancel(Long id);
void refund(Long id);
// 계산
int calculateTotal(Fare fare);
int calculateDiscount(Fare fare, Customer customer);
int calculateTax(Fare fare);
// 통계
int getTotalRevenueByMonth(YearMonth month);
int getTotalRevenueByCustomer(Long customerId);
Map<FareStatus, Integer> getStatusDistribution();
// 보고서
byte[] generateMonthlyReport(YearMonth month);
byte[] generatePdfReport(Long id);
void emailReport(Long id, String email);
// 일괄 처리
void bulkImport(List<FareRequest> requests);
void bulkExport(String format);
void bulkDelete(List<Long> ids);
// 외부 연동
void syncWithExternalSystem(Long id);
void notifyCustomer(Long id);
// ... 50개 메서드 ❌
}
public class FareServiceImpl implements FareService {
// 50개 메서드 모두 구현해야 함 ❌
public Fare create(...) { ... }
public Fare findById(...) { ... }
// ...
public void syncWithExternalSystem(...) { ... }
// → 50번의 구현 ❌
}
→ 새 구현체 만들 때마다 50개 메서드 강제.
// 보고서만 처리하는 특수 구현
public class ReportOnlyFareService implements FareService {
@Override
public byte[] generateMonthlyReport(YearMonth month) {
// 진짜 구현
}
@Override
public Fare create(FareRequest request) {
throw new UnsupportedOperationException(); // ❌
}
@Override
public void delete(Long id) {
throw new UnsupportedOperationException(); // ❌
}
// 47개 메서드 모두 UnsupportedOperationException ❌
}
→ LSP 직접 위반. 부모(인터페이스) 자리에 자식 넣으면 폭탄.
Fare 클래스에 새 필드 추가 → FareService 의 여러 메서드 시그니처 변경 → 모든 구현체 수정.
→ 재컴파일/재배포 비용 ↑.
// 이 컨트롤러는 보고서 기능만 필요한데
@RestController
public class ReportController {
private final FareService fareService; // 50개 메서드 다 의존
public void generateReport(Long id) {
byte[] report = fareService.generatePdfReport(id);
// ... 50개 메서드 중 1개만 사용
}
}
→ 불필요한 의존. 보고서 변경 시 무관한 부분도 영향.
// 보고서 컨트롤러 테스트
@Test
void 보고서_생성_테스트() {
FareService mock = mock(FareService.class);
// 50개 메서드 모두 mock 가능 — 너무 큰 mock
when(mock.generatePdfReport(anyLong())).thenReturn(new byte[10]);
// 사용하지 않는 메서드까지 mock 신경 써야
}
→ mock 설정 복잡, 테스트 의도 흐림.
신입: "FareService 가 뭐 하나요?"
선임: "음... 50개 메서드를 봐야 알 수 있어요"
신입: "어디서부터 봐야 하나요?"
선임: "관련 부분만 봐도 되긴 한데, 전체 맥락도 알아야..."
→ 이해 비용 ↑.
// 책임별로 인터페이스 분리
// 1. CRUD
public interface FareCrudService {
Fare create(FareRequest request);
Fare findById(Long id);
List<Fare> findAll();
void update(Long id, FareRequest request);
void delete(Long id);
}
// 2. 검색
public interface FareSearchService {
List<Fare> findByCustomer(Long customerId);
List<Fare> findByDateRange(LocalDate from, LocalDate to);
}
// 3. 상태 관리
public interface FareStatusService {
void submit(Long id);
void approve(Long id);
void cancel(Long id);
void refund(Long id);
}
// 4. 계산
public interface FareCalculator {
int calculateTotal(Fare fare);
int calculateDiscount(Fare fare, Customer customer);
int calculateTax(Fare fare);
}
// 5. 통계
public interface FareStatisticsService {
int getTotalRevenueByMonth(YearMonth month);
int getTotalRevenueByCustomer(Long customerId);
Map<FareStatus, Integer> getStatusDistribution();
}
// 6. 보고서
public interface FareReportService {
byte[] generateMonthlyReport(YearMonth month);
byte[] generatePdfReport(Long id);
void emailReport(Long id, String email);
}
// 7. 일괄 처리
public interface FareBulkService {
void bulkImport(List<FareRequest> requests);
void bulkExport(String format);
void bulkDelete(List<Long> ids);
}
효과:
| 문제 | 해결 |
|---|---|
| 구현 부담 폭증 | 필요한 인터페이스만 구현 |
| UnsupportedOperationException | 불필요한 메서드 자체가 없음 |
| 변경 영향 폭발 | 변경된 인터페이스만 영향 |
| 의존성 폭발 | 클라이언트는 필요한 것만 의존 |
| 테스트 어려움 | 작은 mock |
| 학습 부담 | 작은 단위로 이해 |
→ 이게 ISP의 진짜 가치.
ISP의 사고방식:
"이 인터페이스를 누가 사용하는가? 각 사용자는 무엇을 필요로 하는가?"
→ 클라이언트의 사용 패턴 으로 인터페이스 분리.
누가 FareService를 사용하나?
- FareController (CRUD)
- ReportController (보고서)
- AdminController (관리, 통계)
- BatchService (일괄 처리)
- ExternalSyncService (외부 연동)
FareController:
- create(), findById(), findAll(), update(), delete()
ReportController:
- generatePdfReport(), emailReport()
AdminController:
- getTotalRevenueByMonth(), getStatusDistribution()
BatchService:
- bulkImport(), bulkExport()
→ 각 클라이언트가 사용하는 메서드 그룹 을 식별.
각 그룹을 별도 인터페이스 로:
public interface FareCrudService {
Fare create(FareRequest request);
Fare findById(Long id);
// ...
}
public interface FareReportService {
byte[] generatePdfReport(Long id);
void emailReport(Long id, String email);
}
public interface FareStatisticsService { ... }
public interface FareBulkService { ... }
선택지 1 — 한 클래스가 여러 인터페이스 구현:
@Service
public class FareServiceImpl implements
FareCrudService, FareSearchService, FareStatusService {
// 관련된 책임을 한 클래스에 모음
}
선택지 2 — 여러 클래스로 분리:
@Service
public class FareCrudServiceImpl implements FareCrudService { ... }
@Service
public class FareReportServiceImpl implements FareReportService { ... }
@Service
public class FareStatisticsServiceImpl implements FareStatisticsService { ... }
→ 구현 분리는 SRP 의 영역, 인터페이스 분리는 ISP 의 영역.
@RestController
public class ReportController {
private final FareReportService reportService; // 보고서만 의존 ✅
public void generate(Long id) {
reportService.generatePdfReport(id);
}
}
@RestController
public class FareController {
private final FareCrudService crudService; // CRUD만 의존
private final FareStatusService statusService; // 상태만 의존
}
→ 각 클라이언트가 자기가 필요한 것만 의존.
자바 컬렉션도 처음엔 거대 인터페이스로 시작했다면 어땠을까?
// 가상의 Fat Interface
public interface Collection<E> {
void add(E e);
void remove(E e);
void addToFirst(E e); // List만 필요
boolean isUnique(); // Set만 필요
void offer(E e); // Queue만 필요
// ...
}
→ Set이 addToFirst() 강제 구현 등 문제 폭발.
자바의 실제 설계:
public interface Collection<E> {
boolean add(E e);
boolean remove(Object o);
int size();
// 공통 동작만
}
public interface List<E> extends Collection<E> {
E get(int index);
void add(int index, E element); // 위치 지정
// List만의 특성
}
public interface Set<E> extends Collection<E> {
// 중복 제거 — 시그니처 동일, 의미 다름
}
public interface Queue<E> extends Collection<E> {
boolean offer(E e);
E poll();
// Queue만의 특성
}
→ 공통은 부모 인터페이스, 특수는 자식 인터페이스.
// 자기 자신과 비교
public interface Comparable<T> {
int compareTo(T other);
}
// 외부에서 두 객체 비교
public interface Comparator<T> {
int compare(T a, T b);
}
→ 두 가지 다른 책임 을 별도 인터페이스로 분리.
public interface Iterable<T> {
Iterator<T> iterator(); // 반복 가능한 것
}
public interface Iterator<T> {
boolean hasNext();
T next();
// 실제 반복 동작
}
→ "반복 가능" 과 "반복 동작" 을 분리.
Spring 자체가 ISP의 거대한 응용:
// 마커 인터페이스 — 능력 표현
public interface Aware { }
public interface ApplicationContextAware extends Aware { ... }
public interface BeanNameAware extends Aware { ... }
public interface InitializingBean { ... }
public interface DisposableBean { ... }
→ 각 인터페이스가 한 가지 능력 을 표현.
// 필요한 능력만 구현
@Component
public class MyBean implements ApplicationContextAware, InitializingBean {
// 이 두 가지 능력만 가짐
}
Lombok의 @Service, @RequiredArgsConstructor 사용 시 자동 의존성 주입에 주의:
@Service
@RequiredArgsConstructor
public class ReportController {
private final FareService fareService; // ❌ Fat Interface 의존
// 보고서 메서드만 사용하는데 50개 메서드 다 의존
}
ISP 적용:
@Service
@RequiredArgsConstructor
public class ReportController {
private final FareReportService reportService; // ✅ 필요한 것만
}
ISP는 설계 원칙 이라 직접적 JVM 동작은 없습니다. 대신 컴파일 단위와 의존성 관점에서 봅니다.
Fat Interface 사용 시:
[FareService.java] (50개 메서드)
↑ 의존
[Controller A] [Controller B] [Service C] ... [10개 클라이언트]
FareService 변경 → 모든 클라이언트 재컴파일.
ISP 적용 시:
[FareCrudService] [FareReportService] [FareStatisticsService] ...
↑ ↑ ↑
[Controller A] [Report Controller] [Admin Controller]
각 인터페이스 변경 → 해당 클라이언트만 재컴파일.
→ 재컴파일 영향 최소화.
// 한 클래스가 여러 인터페이스 구현 가능
public class FareServiceImpl implements
FareCrudService,
FareSearchService,
FareStatusService { ... }
JVM 관점:
FareCrudService crud = new FareServiceImpl();
FareSearchService search = new FareServiceImpl();
// 같은 인스턴스, 다른 인터페이스 타입
→ 다중 인터페이스 구현 = ISP의 자연스러운 결과.
Spring은 타입 기반 으로 빈을 주입:
@Service
public class FareServiceImpl implements
FareCrudService, FareReportService { ... }
@RestController
public class ReportController {
private final FareReportService reportService; // ← 이 타입만 봄
// Spring: "FareReportService 구현체는? FareServiceImpl"
// → 같은 인스턴스를 다른 타입으로 주입
}
→ 인터페이스 분리해도 인스턴스는 하나.
Java 8 default 메서드로 인터페이스에 기본 구현 가능:
public interface Walker {
void walk();
default void rest() { // 기본 구현
System.out.println("쉬는 중");
}
}
ISP 적용 시 활용:
public interface FareReportService {
byte[] generatePdfReport(Long id);
default String getDefaultReportFormat() {
return "PDF"; // 모든 구현체에 공통
}
}
→ ISP + default 메서드 의 조합.
자바 컴파일러가 ISP를 강제하지는 않지만, 다음을 통해 간접 강제:
public interface Big {
void m1(); void m2(); void m3(); /* ... */ void m50();
}
public class Impl implements Big {
// 50개 메서드 모두 구현 안 하면 컴파일 에러
}
→ Fat Interface는 자동으로 부담 발생.
@Service
public class Controller {
private final BigInterface big;
// BigInterface 변경 → Controller 재컴파일
}
→ Fat Interface 의존 = 재컴파일 영향.
Before — Fat Interface:
public interface FareService {
// CRUD
Fare create(...);
Fare findById(...);
void update(...);
void delete(...);
// 보고서
byte[] generatePdfReport(...);
void emailReport(...);
// 통계
int getMonthlyRevenue(...);
// 일괄 처리
void bulkImport(...);
// 50개 메서드
}
After — ISP 적용:
// 1. CRUD 인터페이스
public interface FareCrudService {
Fare create(FareRequest request);
Fare findById(Long id);
List<Fare> findAll();
void update(Long id, FareRequest request);
void delete(Long id);
}
// 2. 보고서 인터페이스
public interface FareReportService {
byte[] generatePdfReport(Long id);
void emailReport(Long id, String email);
}
// 3. 통계 인터페이스
public interface FareStatisticsService {
int getMonthlyRevenue(YearMonth month);
Map<FareStatus, Integer> getStatusDistribution();
}
// 4. 일괄 처리 인터페이스
public interface FareBulkService {
void bulkImport(List<FareRequest> requests);
void bulkExport(String format);
}
// 구현체 — 관련 책임 묶음
@Service
@RequiredArgsConstructor
public class FareServiceImpl implements FareCrudService, FareStatisticsService {
// CRUD + 통계는 같은 데이터 출처 → 한 클래스
}
@Service
@RequiredArgsConstructor
public class FareReportServiceImpl implements FareReportService {
// 보고서는 별도 (PDF 생성 등 다른 의존성)
}
@Service
@RequiredArgsConstructor
public class FareBulkServiceImpl implements FareBulkService {
// 일괄 처리도 별도
}
클라이언트 — 필요한 것만 의존:
@RestController
@RequiredArgsConstructor
public class FareController {
private final FareCrudService crudService;
}
@RestController
@RequiredArgsConstructor
public class ReportController {
private final FareReportService reportService;
}
@RestController
@RequiredArgsConstructor
public class AdminController {
private final FareCrudService crudService;
private final FareStatisticsService statsService;
}
Before — Fat Interface (LSP 위반):
public interface PaymentMethod {
void process(int amount);
void refund(int amount);
void scheduleRecurring(int amount, Period period);
void splitPayment(List<Integer> amounts);
}
public class GiftCardPayment implements PaymentMethod {
public void process(...) { /* OK */ }
public void refund(...) {
throw new UnsupportedOperationException(); // LSP 위반
}
public void scheduleRecurring(...) {
throw new UnsupportedOperationException(); // LSP 위반
}
public void splitPayment(...) {
throw new UnsupportedOperationException(); // LSP 위반
}
}
After — ISP 적용:
// 모든 결제
public interface Payable {
void process(int amount);
}
// 환불 가능
public interface Refundable {
void refund(int amount);
}
// 정기 결제 가능
public interface Recurring {
void scheduleRecurring(int amount, Period period);
}
// 분할 결제 가능
public interface Splittable {
void splitPayment(List<Integer> amounts);
}
// 각 결제 수단 — 자기 능력만 구현
public class CreditCardPayment implements Payable, Refundable, Recurring {
// 신용카드는 모두 가능
}
public class GiftCardPayment implements Payable {
// 기프트카드는 결제만 (LSP 위반 없음)
}
public class BankTransferPayment implements Payable, Refundable {
// 계좌이체는 결제 + 환불만
}
// 사용 시 — 타입으로 안전성 보장
public class CheckoutService {
public void pay(Payable payment, int amount) {
payment.process(amount); // 모두 안전
}
public void refund(Refundable payment, int amount) {
payment.refund(amount); // 환불 가능한 것만 ✅
// GiftCardPayment는 컴파일 에러
}
}
→ ISP가 LSP를 자연스럽게 보장.
Before — Fat Interface:
public interface User {
void login();
void logout();
void createPost();
void deletePost();
void deleteUser();
void modifySystemConfig();
}
public class GuestUser implements User {
public void login() { /* OK */ }
public void logout() { /* OK */ }
public void createPost() {
throw new UnsupportedOperationException();
}
public void deletePost() {
throw new UnsupportedOperationException();
}
public void deleteUser() {
throw new UnsupportedOperationException();
}
public void modifySystemConfig() {
throw new UnsupportedOperationException();
}
}
After — ISP 적용:
public interface Authenticatable {
void login();
void logout();
}
public interface PostCreator {
void createPost();
void deletePost();
}
public interface UserAdmin {
void deleteUser();
}
public interface SystemAdmin {
void modifySystemConfig();
}
// 권한 조합으로 사용자 표현
public class GuestUser implements Authenticatable {
// 로그인/로그아웃만
}
public class RegularUser implements Authenticatable, PostCreator {
// 로그인 + 게시물
}
public class Moderator implements Authenticatable, PostCreator, UserAdmin {
// 로그인 + 게시물 + 사용자 관리
}
public class Administrator implements Authenticatable, PostCreator, UserAdmin, SystemAdmin {
// 모든 권한
}
→ 권한이 타입으로 표현됨. 컴파일 시점에 권한 검증.
// 기본 알림
public interface NotificationSender {
void send(String message, String recipient);
}
// 즉시 알림
public interface InstantNotification extends NotificationSender {
long getMaxLatencyMillis(); // 1초 이내 등
}
// 예약 알림
public interface ScheduledNotification extends NotificationSender {
void schedule(String message, String recipient, LocalDateTime when);
}
// 일괄 알림
public interface BulkNotification extends NotificationSender {
void sendBulk(String message, List<String> recipients);
}
// 구현체 — 자기 능력만
@Component
public class SmsSender implements InstantNotification {
public void send(...) { ... }
public long getMaxLatencyMillis() { return 1000; }
// 예약/일괄 X
}
@Component
public class EmailSender implements InstantNotification, BulkNotification, ScheduledNotification {
// 모든 기능 지원
}
@Component
public class PushNotificationSender implements InstantNotification, BulkNotification {
// 즉시 + 일괄, 예약 X
}
// ❌ Fat Interface
public interface Validator {
void validateName(String name);
void validateEmail(String email);
void validatePhone(String phone);
void validateAddress(String address);
void validateBusinessNumber(String number); // 기업만
void validateStudentId(String id); // 학생만
void validateVisaNumber(String visa); // 외국인만
}
public class IndividualCustomerValidator implements Validator {
public void validateName(...) { /* OK */ }
public void validateEmail(...) { /* OK */ }
public void validatePhone(...) { /* OK */ }
public void validateAddress(...) { /* OK */ }
public void validateBusinessNumber(...) {
throw new UnsupportedOperationException(); // ❌
}
public void validateStudentId(...) {
throw new UnsupportedOperationException(); // ❌
}
public void validateVisaNumber(...) {
throw new UnsupportedOperationException(); // ❌
}
}
해결:
public interface BasicValidator {
void validate(String value);
}
@Component("nameValidator")
public class NameValidator implements BasicValidator { ... }
@Component("emailValidator")
public class EmailValidator implements BasicValidator { ... }
@Component("businessNumberValidator")
public class BusinessNumberValidator implements BasicValidator { ... }
// 각 고객 종류별로 필요한 검증기 조합
@Service
public class IndividualCustomerService {
private final List<BasicValidator> validators; // 일반 고객용 검증기들
}
@Service
public class CorporateCustomerService {
private final List<BasicValidator> validators; // 기업 고객용 (사업자번호 포함)
}
→ 검증기를 작은 단위로 분리 + 조합으로 다양한 시나리오.
// ❌ 과도한 분리
public interface Nameable { String getName(); }
public interface Aged { int getAge(); }
public interface Emailable { String getEmail(); }
public interface Phoned { String getPhone(); }
public interface Addressed { String getAddress(); }
public class Customer implements Nameable, Aged, Emailable, Phoned, Addressed {
// 5개 인터페이스 ❌
}
→ 인터페이스 폭발. 단순 정보 조회를 5개 인터페이스로?
원칙: 클라이언트 관점에서 분리. 같은 클라이언트가 같이 사용하는 메서드는 같이.
// 인터페이스는 분리
public interface FareCrudService { ... }
public interface FareReportService { ... }
// 그러나 구현은 한 클래스에
@Service
public class FareServiceImpl implements
FareCrudService, FareReportService, /* 모든 인터페이스 */ {
// 1000줄 ❌ — God Class
}
→ ISP 적용했지만 SRP 위반. 두 원칙은 함께 가야 함.
// ❌ 미래 변경 가능성을 고려 안 함
public interface CustomerCreate { Customer create(...); }
public interface CustomerUpdate { void update(...); }
public interface CustomerDelete { void delete(...); }
// 만약 미래에 모든 변경 작업에 검증이 추가된다면?
// 3개 인터페이스 모두 수정 ❌
해결: 지금 명백히 다른 클라이언트 일 때만 분리.
public interface FareService {
void method1();
default void method2() { ... } // default
default void method3() { ... } // default
// 50개 default 메서드 ❌
}
문제:
해결: 진짜 필요한 default만, 나머지는 인터페이스 분리.
public interface Payable {
void process(int amount);
}
public class CheckoutService {
public void process(Payable payment, int amount) {
payment.process(amount);
// ❌ ISP 깨는 instanceof
if (payment instanceof Refundable) {
((Refundable) payment).refund(amount);
}
}
}
→ ISP 적용했지만 사용처에서 다시 통합. 의미 없음.
해결: 사용처에서 타입으로 강제:
public void processAndRefund(Refundable payment, int amount) {
payment.process(amount);
payment.refund(amount);
}
// ❌ 도메인 + 기술 혼재
public interface FareService {
Fare create(FareRequest request); // 도메인
void saveToDatabase(Fare fare); // 기술
void publishKafkaEvent(Fare fare); // 기술
Fare findById(Long id); // 도메인
}
해결: 도메인 인터페이스와 기술 인터페이스 분리:
public interface FareService {
Fare create(FareRequest request); // 도메인
Fare findById(Long id);
}
public interface FareRepository { // 기술
void save(Fare fare);
Fare findById(Long id);
}
public interface FareEventPublisher { // 기술
void publishCreated(Fare fare);
}
→ DIP (다음 Unit) 의 핵심.
// ❌ 너무 일반화
public interface Doable<T, R> {
R doIt(T input);
}
→ 의미 없는 인터페이스. 어떤 클라이언트도 의도 파악 불가.
해결: 명확한 도메인 의미 가진 인터페이스:
public interface FareCalculator {
int calculate(Fare fare);
}
[SRP] — 클래스 책임 분리
↓
[OCP] — 확장 가능
↓
[LSP] — 안전한 다형성
↓
[ISP] ★ ← 지금 여기 — 인터페이스 책임 분리
↓
[DIP] — 추상화 의존
→ ISP는 SRP의 인터페이스 버전. LSP를 자연스럽게 보장.
[SRP] — "한 클래스, 한 책임"
↓ 인터페이스로 확장
[ISP] — "한 인터페이스, 한 책임"
↓ 자식이 약속 지킴
[LSP] — "안전한 대체"
세 원칙이 서로를 강화:
| Phase 2 학습 | ISP 적용 |
|---|---|
| Unit 2.4 (다형성) | 인터페이스가 다형성의 도구 |
| Unit 2.5 (instanceof) | 인터페이스로 타입 분리 → instanceof 감소 |
| Unit 2.6 (Anonymous) | 작은 인터페이스 = 람다 친화적 |
→ Java 8 함수형 인터페이스 = ISP의 극단적 형태 (메서드 1개).
3주차 (제네릭/람다):
Function, Consumer, Supplier) = ISP의 극단5주차 (Spring DI):
@Component + 인터페이스 분리 = 자연스러운 ISP5주차 (디자인 패턴):
11-12주차 (JPA):
JpaRepository, CrudRepository, PagingAndSortingRepository = ISP 계층18주차 (Spring Security):
UserDetails, Authentication, GrantedAuthority 분리 = ISP→ 자바 생태계 전반에 ISP 적용.
| 질문 | 이 Unit에서의 답 |
|---|---|
| "ISP가 뭔가요?" | 클라이언트는 사용하지 않는 메서드에 의존하면 안 됨 |
| "Fat Interface의 문제?" | UnsupportedOperationException, 의존성 폭발 등 6가지 |
| "ISP와 SRP 관계?" | ISP는 SRP의 인터페이스 버전 |
| "ISP와 LSP 관계?" | ISP가 LSP를 자연스럽게 보장 |
| "Java 표준의 ISP 사례?" | Collection 계층 (List/Set/Queue), Comparable/Comparator 등 |
1️⃣ ISP는 "Fat Interface 의 부담을 줄이는" 원칙이다.
Uncle Bob의 정의: "Clients should not be forced to depend on methods they do not use." 거대 인터페이스는 구현 클래스에 사용하지 않는 메서드 구현 부담 을 강요하고,
UnsupportedOperationException같은 LSP 위반을 유발한다. 클라이언트가 실제로 사용하는 단위로 인터페이스를 분리 하면 이 부담이 사라진다.2️⃣ ISP는 SRP의 인터페이스 버전이자 LSP의 보장 장치다.
SRP가 클래스 레벨이라면, ISP는 인터페이스 레벨의 책임 분리. ISP를 따르면 자식 클래스가 부모의 약속을 자연스럽게 지킬 수 있어 LSP가 자동 보장 된다. 자바 표준 라이브러리의
Collection계층 (List, Set, Queue),Comparable/Comparator,Iterable/Iterator가 ISP의 모범 사례.3️⃣ ISP는 클라이언트 관점에서 분리하되 균형이 필요하다.
적용 단계: ① 클라이언트 식별, ② 사용 패턴 분석, ③ 인터페이스 분리, ④ 구현체 설계, ⑤ 클라이언트는 필요한 것만 의존. 단, 너무 잘게 분리 (인터페이스 폭발) 하거나, 인터페이스만 분리하고 구현은 God Class 인 경우는 안티패턴. 명확히 다른 클라이언트가 다른 메서드 그룹을 사용 할 때만 분리.
박승제님의 ILIC 코드를 점검:
ISP 위반 신호 ⚠️:
UnsupportedOperationException 사용3개 이상 해당 = ISP 적용 가치 큼.