🎯1주차 Unit 3.4 — ISP (인터페이스 분리 원칙)

Psj·2026년 5월 7일

F-lab

목록 보기
33/240

🎯 Unit 3.4 — ISP (인터페이스 분리 원칙)

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를 안전하게 만드는 도구.


🌍 1. 세상 속 비유

ISP = "맥가이버 칼 vs 전용 도구"

맥가이버 칼 (Swiss Army Knife):

  • 한 도구에 칼, 가위, 드라이버, 병따개, 손톱깎이... 50개 기능
  • 무엇이든 할 수 있다
  • 그러나 각 기능은 평범
  • 특정 작업만 필요해도 모든 도구를 들고 다님

전용 도구:

  • 칼 → 잘 드는 칼
  • 가위 → 잘 드는 가위
  • 드라이버 → 정밀한 드라이버
  • 각자 한 가지를 잘함
  • 필요한 것만 선택해서 사용

언제 무엇이?:

  • 캠핑 (어떤 일이 생길지 모름) → 맥가이버 칼
  • 전문 작업 (특정 일에 집중) → 전용 도구

자바에서:

  • 맥가이버 칼 = 거대 인터페이스 (모든 메서드 다 있음)
  • 전용 도구 = 작은 인터페이스 (한 책임만)

→ 대부분의 경우 전용 도구가 더 좋음 (ISP의 정신).


더 직관적인 비유 — "회사 직원 채용"

거대 인터페이스 = "만능 직원" 채용 공고:

"코딩, 디자인, 영업, 마케팅, 회계, 청소를 모두 할 수 있는 사람"

이런 사람을 찾을 수 있을까?

  • 매우 어려움
  • 찾아도 각 분야 전문가가 아님
  • 한 영역만 필요해도 모든 능력 요구

작은 인터페이스 = "전문가" 채용 공고:

"코딩 전문가" / "디자인 전문가" / "영업 전문가"

각자 한 가지를 잘함:

  • 채용 쉬움
  • 전문성 ↑
  • 필요한 능력만 갖춤

이게 ISP. 한 인터페이스에 너무 많은 책임을 넣지 말 것.


핵심 한 문장

"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."

ISP의 핵심:

  • 큰 인터페이스 1개 → 작은 인터페이스 여러 개로 분리
  • 각 클라이언트는 자기가 필요한 인터페이스만 의존
  • 불필요한 메서드 구현/의존 강제 X

비유 정리:

비유 요소ISP 적용
맥가이버 칼거대 인터페이스 (안티패턴)
전용 도구작은 인터페이스 (좋은 설계)
만능 직원 채용거대 인터페이스 강제
전문가 채용인터페이스 분리

🔥 2. 탄생 배경

Robert C. Martin (Uncle Bob) 의 정의

ISP (Interface Segregation Principle) — Uncle Bob이 1990년대 정립.

원래 정의:

"Clients should not be forced to depend on methods they do not use."
("클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다.")

핵심 개념: Fat Interface (뚱뚱한 인터페이스) 의 안티패턴.


Uncle Bob의 실제 경험 — Xerox 사례

ISP의 등장 배경에는 실제 사례가 있습니다.

Xerox 의 새 프린터 시스템 (1990년대):

  • 한 거대 클래스 Job 이 모든 작업을 표현
  • 프린트, 복사, 팩스, 스캔, 바인딩 등 모든 메서드 를 가짐

문제 발생:

  • Job 에 변경 → 다른 작업 코드 모두 재컴파일 필요
  • 거대 인터페이스 → 모든 작업이 모든 메서드 구현 강제
  • → 시스템 유지보수 불가능

Uncle Bob의 해결:

  • Job 인터페이스를 작업별 작은 인터페이스로 분리
  • PrintJob, CopyJob, FaxJob, ScanJob
  • 각 클래스는 자기가 하는 작업만 구현

→ 이 경험을 바탕으로 ISP가 정립.


SRP 와 ISP 의 관계 ⭐

ISP는 인터페이스 레벨의 SRP 라고 볼 수 있습니다:

SRPISP
적용 대상클래스인터페이스
핵심한 책임한 책임 (인터페이스로)
관점변경 이유클라이언트 사용 패턴
위반 안티패턴God ClassFat Interface

두 원칙이 한 사고의 다른 표현. SRP는 클래스에, ISP는 인터페이스에.


LSP 와 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의 사고를 인터페이스 레벨로 확장한 원칙.


💣 3. 없으면 생기는 문제

ISP를 위반했을 때의 구체적 문제를 ILIC 시나리오로 보겠습니다.

시나리오 1: ILIC 운임 서비스 — Fat Interface

처음에는 단순하게 시작:

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개 메서드 ❌
}

Fat Interface의 6가지 심각한 문제

1. 구현 부담 폭증

public class FareServiceImpl implements FareService {
    // 50개 메서드 모두 구현해야 함 ❌
    
    public Fare create(...) { ... }
    public Fare findById(...) { ... }
    // ...
    public void syncWithExternalSystem(...) { ... }
    // → 50번의 구현 ❌
}

새 구현체 만들 때마다 50개 메서드 강제.


2. UnsupportedOperationException 폭탄 (LSP 위반)

// 보고서만 처리하는 특수 구현
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 직접 위반. 부모(인터페이스) 자리에 자식 넣으면 폭탄.


3. 변경 영향 폭발

Fare 클래스에 새 필드 추가 → FareService 의 여러 메서드 시그니처 변경 → 모든 구현체 수정.

재컴파일/재배포 비용 ↑.


4. 의존성 폭발

// 이 컨트롤러는 보고서 기능만 필요한데
@RestController
public class ReportController {
    private final FareService fareService;  // 50개 메서드 다 의존
    
    public void generateReport(Long id) {
        byte[] report = fareService.generatePdfReport(id);
        // ... 50개 메서드 중 1개만 사용
    }
}

불필요한 의존. 보고서 변경 시 무관한 부분도 영향.


5. 테스트 어려움

// 보고서 컨트롤러 테스트
@Test
void 보고서_생성_테스트() {
    FareService mock = mock(FareService.class);
    
    // 50개 메서드 모두 mock 가능 — 너무 큰 mock
    when(mock.generatePdfReport(anyLong())).thenReturn(new byte[10]);
    
    // 사용하지 않는 메서드까지 mock 신경 써야
}

mock 설정 복잡, 테스트 의도 흐림.


6. 신규 개발자의 학습 부담

신입: "FareService 가 뭐 하나요?"
선임: "음... 50개 메서드를 봐야 알 수 있어요"
신입: "어디서부터 봐야 하나요?"
선임: "관련 부분만 봐도 되긴 한데, 전체 맥락도 알아야..."

이해 비용 ↑.


해결 — 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 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);
}

효과:

  • 각 인터페이스가 한 책임
  • 구현체는 자기가 제공하는 인터페이스만 구현
  • 클라이언트는 자기가 필요한 인터페이스만 의존

6가지 문제가 어떻게 해결됐나?

문제해결
구현 부담 폭증필요한 인터페이스만 구현
UnsupportedOperationException불필요한 메서드 자체가 없음
변경 영향 폭발변경된 인터페이스만 영향
의존성 폭발클라이언트는 필요한 것만 의존
테스트 어려움작은 mock
학습 부담작은 단위로 이해

이게 ISP의 진짜 가치.


✅ 4. 해결책 — ISP를 적용하는 방법

핵심 원칙 — "클라이언트 관점"

ISP의 사고방식:

"이 인터페이스를 누가 사용하는가? 각 사용자는 무엇을 필요로 하는가?"

클라이언트의 사용 패턴 으로 인터페이스 분리.


적용 단계 ⭐

단계 1: 클라이언트 식별

누가 FareService를 사용하나?
- FareController (CRUD)
- ReportController (보고서)
- AdminController (관리, 통계)
- BatchService (일괄 처리)
- ExternalSyncService (외부 연동)

단계 2: 사용 패턴 분석

FareController:
  - create(), findById(), findAll(), update(), delete()
  
ReportController:
  - generatePdfReport(), emailReport()

AdminController:
  - getTotalRevenueByMonth(), getStatusDistribution()
  
BatchService:
  - bulkImport(), bulkExport()

각 클라이언트가 사용하는 메서드 그룹 을 식별.


단계 3: 인터페이스 분리

각 그룹을 별도 인터페이스 로:

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 { ... }

단계 4: 구현체 설계

선택지 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 의 영역.


단계 5: 클라이언트는 필요한 인터페이스만 의존

@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;  // 상태만 의존
}

각 클라이언트가 자기가 필요한 것만 의존.


Java 표준 라이브러리의 ISP 사례 ⭐

1. List, Set, Queue (Collection 분리)

자바 컬렉션도 처음엔 거대 인터페이스로 시작했다면 어땠을까?

// 가상의 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만의 특성
}

공통은 부모 인터페이스, 특수는 자식 인터페이스.


2. Comparable vs Comparator

// 자기 자신과 비교
public interface Comparable<T> {
    int compareTo(T other);
}

// 외부에서 두 객체 비교
public interface Comparator<T> {
    int compare(T a, T b);
}

두 가지 다른 책임 을 별도 인터페이스로 분리.


3. Iterable vs Iterator

public interface Iterable<T> {
    Iterator<T> iterator();  // 반복 가능한 것
}

public interface Iterator<T> {
    boolean hasNext();
    T next();
    // 실제 반복 동작
}

"반복 가능""반복 동작" 을 분리.


Spring 의 ISP 활용

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 활용 시 주의 ⚠️

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;  // ✅ 필요한 것만
}

🏗️ 5. 내부 동작 원리

ISP는 설계 원칙 이라 직접적 JVM 동작은 없습니다. 대신 컴파일 단위와 의존성 관점에서 봅니다.

컴파일 단위와 ISP

Fat Interface 사용 시:

[FareService.java] (50개 메서드)
       ↑ 의존
[Controller A] [Controller B] [Service C] ... [10개 클라이언트]

FareService 변경 → 모든 클라이언트 재컴파일.


ISP 적용 시:

[FareCrudService] [FareReportService] [FareStatisticsService] ...
       ↑                    ↑                    ↑
[Controller A]       [Report Controller]    [Admin Controller]

각 인터페이스 변경 → 해당 클라이언트만 재컴파일.

재컴파일 영향 최소화.


Java 인터페이스의 다중 구현

// 한 클래스가 여러 인터페이스 구현 가능
public class FareServiceImpl implements 
    FareCrudService, 
    FareSearchService, 
    FareStatusService { ... }

JVM 관점:

  • 클래스의 메서드 테이블에 모든 인터페이스의 메서드 등록
  • 어떤 인터페이스 타입으로 호출하든 동일한 구현 실행
FareCrudService crud = new FareServiceImpl();
FareSearchService search = new FareServiceImpl();
// 같은 인스턴스, 다른 인터페이스 타입

다중 인터페이스 구현 = ISP의 자연스러운 결과.


Spring의 의존성 주입과 ISP

Spring은 타입 기반 으로 빈을 주입:

@Service
public class FareServiceImpl implements 
    FareCrudService, FareReportService { ... }

@RestController
public class ReportController {
    private final FareReportService reportService;  // ← 이 타입만 봄
    
    // Spring: "FareReportService 구현체는? FareServiceImpl"
    // → 같은 인스턴스를 다른 타입으로 주입
}

인터페이스 분리해도 인스턴스는 하나.


default 메서드와 ISP (Java 8+)

Java 8 default 메서드로 인터페이스에 기본 구현 가능:

public interface Walker {
    void walk();
    
    default void rest() {  // 기본 구현
        System.out.println("쉬는 중");
    }
}

ISP 적용 시 활용:

  • 인터페이스가 분리되어도 공통 동작은 default로
  • 구현 부담 감소
public interface FareReportService {
    byte[] generatePdfReport(Long id);
    
    default String getDefaultReportFormat() {
        return "PDF";  // 모든 구현체에 공통
    }
}

ISP + default 메서드 의 조합.


컴파일 시점의 강제

자바 컴파일러가 ISP를 강제하지는 않지만, 다음을 통해 간접 강제:

1. 인터페이스 메서드는 모두 구현 필요

public interface Big {
    void m1(); void m2(); void m3(); /* ... */ void m50();
}

public class Impl implements Big {
    // 50개 메서드 모두 구현 안 하면 컴파일 에러
}

→ Fat Interface는 자동으로 부담 발생.

2. 의존성은 컴파일 의존성을 만듦

@Service
public class Controller {
    private final BigInterface big;
    // BigInterface 변경 → Controller 재컴파일
}

→ Fat Interface 의존 = 재컴파일 영향.


💻 6. 실전 코드 예시

예시 1: ILIC 운임 시스템 — 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;
}

예시 2: 결제 인터페이스 분리 (LSP 와 협력)

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를 자연스럽게 보장.


예시 3: 사용자 권한 인터페이스 분리

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 {
    // 모든 권한
}

권한이 타입으로 표현됨. 컴파일 시점에 권한 검증.


예시 4: ILIC 알림 채널 분리

// 기본 알림
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
}

예시 5: 안티패턴 — ISP 위반 검증 인터페이스

// ❌ 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;  // 기업 고객용 (사업자번호 포함)
}

검증기를 작은 단위로 분리 + 조합으로 다양한 시나리오.


⚠️ 7. 주의사항 & 흔한 실수

실수 1: 너무 잘게 분리

// ❌ 과도한 분리
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개 인터페이스로?

원칙: 클라이언트 관점에서 분리. 같은 클라이언트가 같이 사용하는 메서드는 같이.


실수 2: 인터페이스 분리만 하고 구현은 그대로

// 인터페이스는 분리
public interface FareCrudService { ... }
public interface FareReportService { ... }

// 그러나 구현은 한 클래스에
@Service
public class FareServiceImpl implements 
    FareCrudService, FareReportService, /* 모든 인터페이스 */ {
    // 1000줄 ❌ — God Class
}

ISP 적용했지만 SRP 위반. 두 원칙은 함께 가야 함.


실수 3: 변경 가능성 무시한 분리

// ❌ 미래 변경 가능성을 고려 안 함
public interface CustomerCreate { Customer create(...); }
public interface CustomerUpdate { void update(...); }
public interface CustomerDelete { void delete(...); }

// 만약 미래에 모든 변경 작업에 검증이 추가된다면?
// 3개 인터페이스 모두 수정 ❌

해결: 지금 명백히 다른 클라이언트 일 때만 분리.


실수 4: 거대 인터페이스의 default 메서드 남발

public interface FareService {
    void method1();
    
    default void method2() { ... }  // default
    default void method3() { ... }  // default
    // 50개 default 메서드 ❌
}

문제:

  • 인터페이스가 사실상 추상 클래스
  • 자바의 단일 상속 제약 회피 시도
  • ISP 정신 위반 — 여전히 Fat

해결: 진짜 필요한 default만, 나머지는 인터페이스 분리.


실수 5: instanceof로 ISP 깨기

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);
}

실수 6: 도메인 인터페이스 vs 기술 인터페이스 혼재

// ❌ 도메인 + 기술 혼재
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) 의 핵심.


실수 7: 지나친 일반화

// ❌ 너무 일반화
public interface Doable<T, R> {
    R doIt(T input);
}

→ 의미 없는 인터페이스. 어떤 클라이언트도 의도 파악 불가.

해결: 명확한 도메인 의미 가진 인터페이스:

public interface FareCalculator {
    int calculate(Fare fare);
}

🔗 8. 연관 개념 맵

Phase 3 (SOLID) 내 흐름

[SRP] — 클래스 책임 분리
   ↓ 
[OCP] — 확장 가능
   ↓
[LSP] — 안전한 다형성
   ↓
[ISP] ★ ← 지금 여기 — 인터페이스 책임 분리
   ↓
[DIP] — 추상화 의존

ISP는 SRP의 인터페이스 버전. LSP를 자연스럽게 보장.


SRP, ISP, LSP 의 삼각관계

[SRP] — "한 클래스, 한 책임"
   ↓ 인터페이스로 확장
[ISP] — "한 인터페이스, 한 책임"
   ↓ 자식이 약속 지킴
[LSP] — "안전한 대체"

세 원칙이 서로를 강화:

  • SRP 따르면 → 클래스가 작아짐
  • ISP 따르면 → 인터페이스가 작아짐 → LSP 만족 쉬움
  • LSP 만족하면 → 다형성/OCP 안전

Phase 2와의 연결

Phase 2 학습ISP 적용
Unit 2.4 (다형성)인터페이스가 다형성의 도구
Unit 2.5 (instanceof)인터페이스로 타입 분리 → instanceof 감소
Unit 2.6 (Anonymous)작은 인터페이스 = 람다 친화적

Java 8 함수형 인터페이스 = ISP의 극단적 형태 (메서드 1개).


미래 주차와의 연결

3주차 (제네릭/람다):

  • 함수형 인터페이스 (Function, Consumer, Supplier) = ISP의 극단
  • 메서드 1개씩 분리

5주차 (Spring DI):

  • @Component + 인터페이스 분리 = 자연스러운 ISP
  • 필요한 빈만 주입

5주차 (디자인 패턴):

  • Strategy 패턴 = ISP 응용
  • Adapter 패턴 = ISP로 호환성

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 등

📝 9. 핵심 요약 — 3줄 정리

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 인 경우는 안티패턴. 명확히 다른 클라이언트가 다른 메서드 그룹을 사용 할 때만 분리.


🎓 학습 자기 점검

기본 이해

  • ISP의 정의를 한 문장으로 설명할 수 있다
  • Fat Interface의 6가지 문제를 나열할 수 있다
  • SRP와 ISP의 관계를 안다
  • ISP가 LSP를 보장하는 이유를 안다

실전 적용

  • ILIC 코드의 Fat Interface를 식별할 수 있다
  • 클라이언트 관점에서 인터페이스를 분리할 수 있다
  • 한 클래스가 여러 인터페이스를 구현하는 패턴을 활용할 수 있다
  • 과도한 분리를 피하는 균형감각이 있다

면접 대비 (3-5분 답변)

  • "ISP가 뭔가요?" 답변 가능
  • "Fat Interface의 안티패턴?" 답변 가능
  • "ISP와 LSP의 관계?" 답변 가능
  • "ILIC에서 ISP를 어떻게 적용?" 답변 가능

자기 점검 — ILIC 적용

박승제님의 ILIC 코드를 점검:

ISP 위반 신호 ⚠️:

  • Service 인터페이스에 메서드가 20개 이상
  • 한 컨트롤러가 Service의 1-2개 메서드만 사용
  • UnsupportedOperationException 사용
  • 같은 Service가 여러 도메인에서 사용됨
  • 인터페이스 변경 시 무관한 클래스도 재컴파일

3개 이상 해당 = ISP 적용 가치 큼.


다음 Unit으로

  • DIP (의존 역전 원칙) 을 학습할 준비 완료 — SOLID의 마지막!
  • "구체에 의존하지 말고 추상화에 의존하라" 가 궁금하다
  • Spring DI의 진짜 의미를 만날 준비 완료
profile
Software Developer

0개의 댓글