목차
다 쓰지도 않는 메서드를 억지로 구현한다
복합기를 떠올려 인터페이스를 하나 만들었다고 하자. 인쇄·스캔·팩스를 다 담았다.
public interface Machine {
void print(Document doc);
void scan(Document doc);
void fax(Document doc);
}그런데 우리가 실제로 놓을 건 인쇄만 되는 단순 프린터다. Machine을 구현하면 이렇게 된다.
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)이 이걸 겨눈다.
클라이언트가 자기가 쓰지 않는 메서드에 의존하도록 강요하지 마라.
SimplePrinter는 scan·fax를 쓰지 않는다. 그런데 Machine이 그 둘을 의존하도록 강요했다. 하나의 뚱뚱한 인터페이스가 여러 능력을 다 묶고 있어서, 그중 하나만 필요한 쪽도 전부를 떠안는다.
뚱뚱한 인터페이스가 왜 문제인가
강요가 양쪽을 다 힘들게 한다.
- 구현하는 쪽 - 못 하는 기능까지 메서드를 채워야 한다. 예외를 던지거나 빈 몸으로 두는데, 이건 “이 타입은 사실 그 약속을 못 지킨다”는 뜻이라 언제 터질지 모른다.
- 부르는 쪽 -
Machine을 받아 인쇄만 하는 코드도, 안 쓰는scan·fax에 묶인다. 나중에fax의 시그니처가 바뀌면 팩스를 쓰지도 않던 코드까지 영향권에 든다.
쓰지 않는 것에 묶이는 건 공짜가 아니다.
역할별로 쪼갠다
고침은 인터페이스를 역할별로 잘게 나누는 것이다.
public interface Printer { void print(Document doc); }
public interface Scanner { void scan(Document doc); }
public interface Fax { void fax(Document doc); }하나였던 Machine이 세 개의 작은 역할로 갈라졌다. 이제 각자 필요한 역할만 골라 구현한다.
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) { }
}단순 프린터는 Printer만 구현한다. 이제 억지로 채운 예외 메서드가 없다. 복합기는 세 역할을 다 구현한다 - 진짜로 다 할 수 있으니 정직하다.
부르는 쪽이 보는 것도 좁아진다
인터페이스를 나누면 부르는 쪽도 딱 필요한 것만 받는다.
public void printReport(Printer printer) { // Machine이 아니라 Printer만 받는다
printer.print(report);
}이 코드는 Printer만 안다. scan·fax가 있는지도 모르고, 알 필요도 없다. 그래서 팩스 기능이 어떻게 바뀌든 이 코드는 흔들리지 않는다. 덜 아는 만큼 덜 묶인다.
무엇을 기준으로 쪼개나 - 클라이언트다
그럼 인터페이스를 어디서 갈라야 하나. 기준은 인터페이스를 쓰는 쪽이다.
인쇄만 하는 코드, 스캔만 하는 코드가 따로 있다면, 그 쓰임을 따라 인터페이스를 가른다. 하나의 인터페이스가 서로 다른 쓰임의 메서드를 다 담고 있으면 그게 뚱뚱한 것이다. 쪼개는 선은 **“누가 무엇을 쓰느냐”**를 따라 긋는다.
그래서 무작정 잘게 쪼개는 게 아니다. 늘 같이 쓰이는 메서드는 같이 둔다. 쓰임이 갈리는 곳에서만 인터페이스도 갈린다.
실무: 뚱뚱한 인터페이스·빈 구현이 신호
실무에서 ISP 위반은 이렇게 잡힌다.
- 구현 클래스에 빈 몸이거나 예외를 던지는 메서드가 있다 (그 기능을 못/안 하는데 인터페이스가 요구해서).
- 인터페이스 하나에 메서드가 잔뜩 있고, 구현체마다 쓰는 메서드가 제각각이다.
이럴 때 “이 인터페이스를 쓰는 쪽이 하나인가, 여럿인가”를 물어본다. 여러 종류의 클라이언트가 서로 다른 부분만 쓴다면, 그 선을 따라 역할 인터페이스로 가른다. 큰 인터페이스 하나보다, 딱 맞는 작은 인터페이스 여럿이 낫다.
정리
| ISP란 | 클라이언트가 안 쓰는 메서드에 의존하도록 강요하지 마라 |
| 문제 | 뚱뚱한 인터페이스는 구현자에게 억지 메서드를, 사용자에게 불필요한 결합을 준다 |
| 고침 | 역할별로 작은 인터페이스로 쪼갠다 (Printer·Scanner·Fax) |
| 구현 | 각자 필요한 역할만 구현·조합. 억지 예외 메서드가 사라진다 |
| 사용 | 부르는 쪽도 필요한 인터페이스만 받아 덜 묶인다 |
| 기준 | 쓰는 쪽(클라이언트)을 따라 가른다. 무작정 쪼개기 아님 |
인터페이스를 받는 쪽이 자기가 쓸 것만 알게 만드는 것. 쓰지도 않는 것에 아무도 묶이지 않게 하는 것 - 그게 인터페이스 분리가 지키는 것이다.