← Posts

SOLID · ISP · OOP

Interface Segregation Principle

쓰지도 않는 메서드에 의존하게 만들지 마라. 뚱뚱한 인터페이스를 역할별로 쪼개는 네 번째 SOLID 원칙.

목차
  1. 다 쓰지도 않는 메서드를 억지로 구현한다
  2. ISP = 안 쓰는 메서드에 의존하게 하지 마라
  3. 뚱뚱한 인터페이스가 왜 문제인가
  4. 역할별로 쪼갠다
  5. 부르는 쪽이 보는 것도 좁아진다
  6. 무엇을 기준으로 쪼개나 - 클라이언트다
  7. 실무: 뚱뚱한 인터페이스·빈 구현이 신호
  8. 정리

다 쓰지도 않는 메서드를 억지로 구현한다

복합기를 떠올려 인터페이스를 하나 만들었다고 하자. 인쇄·스캔·팩스를 다 담았다.

java
public interface Machine {
    void print(Document doc);
    void scan(Document doc);
    void fax(Document doc);
}

그런데 우리가 실제로 놓을 건 인쇄만 되는 단순 프린터다. Machine을 구현하면 이렇게 된다.

java
public class SimplePrinter implements Machine {
    public void print(Document doc) { /* 인쇄 */ }

    public void scan(Document doc) { throw new UnsupportedOperationException(); } // 못 한다
    public void fax(Document doc)  { throw new UnsupportedOperationException(); } // 못 한다
}

인쇄만 하면 되는데, 스캔·팩스까지 억지로 구현했다. 할 수 없으니 예외를 던지거나 빈 몸으로 둔다. 인터페이스가 너무 많은 걸 요구해서 벌어진 일이다.

ISP = 안 쓰는 메서드에 의존하게 하지 마라

인터페이스 분리 원칙(ISP)이 이걸 겨눈다.

클라이언트가 자기가 쓰지 않는 메서드에 의존하도록 강요하지 마라.

SimplePrinterscan·fax를 쓰지 않는다. 그런데 Machine이 그 둘을 의존하도록 강요했다. 하나의 뚱뚱한 인터페이스가 여러 능력을 다 묶고 있어서, 그중 하나만 필요한 쪽도 전부를 떠안는다.

뚱뚱한 인터페이스가 왜 문제인가

강요가 양쪽을 다 힘들게 한다.

  • 구현하는 쪽 - 못 하는 기능까지 메서드를 채워야 한다. 예외를 던지거나 빈 몸으로 두는데, 이건 “이 타입은 사실 그 약속을 못 지킨다”는 뜻이라 언제 터질지 모른다.
  • 부르는 쪽 - Machine을 받아 인쇄만 하는 코드도, 안 쓰는 scan·fax에 묶인다. 나중에 fax의 시그니처가 바뀌면 팩스를 쓰지도 않던 코드까지 영향권에 든다.

쓰지 않는 것에 묶이는 건 공짜가 아니다.

역할별로 쪼갠다

고침은 인터페이스를 역할별로 잘게 나누는 것이다.

java
public interface Printer { void print(Document doc); }
public interface Scanner { void scan(Document doc); }
public interface Fax     { void fax(Document doc); }
diagramdiagram

하나였던 Machine이 세 개의 작은 역할로 갈라졌다. 이제 각자 필요한 역할만 골라 구현한다.

java
public class SimplePrinter implements Printer {
    public void print(Document doc) { /* 인쇄만 */ }
}

public class MultiFunction implements Printer, Scanner, Fax {
    public void print(Document doc) { }
    public void scan(Document doc)  { }
    public void fax(Document doc)   { }
}
diagramdiagram

단순 프린터는 Printer만 구현한다. 이제 억지로 채운 예외 메서드가 없다. 복합기는 세 역할을 다 구현한다 - 진짜로 다 할 수 있으니 정직하다.

부르는 쪽이 보는 것도 좁아진다

인터페이스를 나누면 부르는 쪽도 딱 필요한 것만 받는다.

java
public void printReport(Printer printer) {   // Machine이 아니라 Printer만 받는다
    printer.print(report);
}

이 코드는 Printer만 안다. scan·fax가 있는지도 모르고, 알 필요도 없다. 그래서 팩스 기능이 어떻게 바뀌든 이 코드는 흔들리지 않는다. 덜 아는 만큼 덜 묶인다.

무엇을 기준으로 쪼개나 - 클라이언트다

그럼 인터페이스를 어디서 갈라야 하나. 기준은 인터페이스를 쓰는 쪽이다.

인쇄만 하는 코드, 스캔만 하는 코드가 따로 있다면, 그 쓰임을 따라 인터페이스를 가른다. 하나의 인터페이스가 서로 다른 쓰임의 메서드를 다 담고 있으면 그게 뚱뚱한 것이다. 쪼개는 선은 **“누가 무엇을 쓰느냐”**를 따라 긋는다.

그래서 무작정 잘게 쪼개는 게 아니다. 늘 같이 쓰이는 메서드는 같이 둔다. 쓰임이 갈리는 곳에서만 인터페이스도 갈린다.

실무: 뚱뚱한 인터페이스·빈 구현이 신호

실무에서 ISP 위반은 이렇게 잡힌다.

  • 구현 클래스에 빈 몸이거나 예외를 던지는 메서드가 있다 (그 기능을 못/안 하는데 인터페이스가 요구해서).
  • 인터페이스 하나에 메서드가 잔뜩 있고, 구현체마다 쓰는 메서드가 제각각이다.

이럴 때 “이 인터페이스를 쓰는 쪽이 하나인가, 여럿인가”를 물어본다. 여러 종류의 클라이언트가 서로 다른 부분만 쓴다면, 그 선을 따라 역할 인터페이스로 가른다. 큰 인터페이스 하나보다, 딱 맞는 작은 인터페이스 여럿이 낫다.

정리

ISP란클라이언트가 안 쓰는 메서드에 의존하도록 강요하지 마라
문제뚱뚱한 인터페이스는 구현자에게 억지 메서드를, 사용자에게 불필요한 결합을 준다
고침역할별로 작은 인터페이스로 쪼갠다 (Printer·Scanner·Fax)
구현각자 필요한 역할만 구현·조합. 억지 예외 메서드가 사라진다
사용부르는 쪽도 필요한 인터페이스만 받아 덜 묶인다
기준쓰는 쪽(클라이언트)을 따라 가른다. 무작정 쪼개기 아님

인터페이스를 받는 쪽이 자기가 쓸 것만 알게 만드는 것. 쓰지도 않는 것에 아무도 묶이지 않게 하는 것 - 그게 인터페이스 분리가 지키는 것이다.

관련 글