2025-08-09 00:34

Tags:

인터페이스 분리 원칙(ISP) 핸드북

1. 만들어진 이유: “뚱뚱한 인터페이스”의 문제점

소프트웨어 설계 초창기에는 하나의 인터페이스에 관련된 모든 기능을 넣으려는 경향이 있었음. 예를 들어, ‘기계’라는 인터페이스가 있다면 ‘켜기’, ‘끄기’, ‘작동하기’, ‘수리하기’, ‘충전하기’ 등 모든 관련 행위를 포함시키는 식.

이러한 인터페이스를 “뚱뚱한 인터페이스(Fat Interface)” 또는 **“오염된 인터페이스(Polluted Interface)“**라고 부름.

문제는 이 ‘기계’ 인터페이스를 구현하는 모든 클래스가 자신이 사용하지 않는 기능까지 억지로 구현해야 한다는 점. 예를 들어, 한 번 쓰고 버리는 ‘일회용 기계’ 클래스는 ‘수리하기’나 ‘충전하기’ 메서드가 필요 없음에도 불구하고, 인터페이스 때문에 해당 메서드를 비워둔 채로라도 구현해야만 함.

이는 마치 스캔 기능만 필요한 사람에게 복사, 팩스, 인쇄 기능까지 모두 포함된 거대한 복합기를 강매하는 것과 같음. 불필요한 기능 때문에 기계는 비싸지고, 사용법은 복잡해지며, 팩스 기능이 고장 나면 스캔 기능과 상관없음에도 전체를 수리해야 할 수 있음.

이러한 문제, 즉 클라이언트가 자신이 사용하지 않는 메서드에 의존하게 되는 문제를 해결하기 위해 로버트 C. 마틴(Robert C. Martin)은 인터페이스 분리 원칙을 제안. 핵심은 간단함. “클라이언트는 자신이 사용하지 않는 인터페이스에 의존하도록 강요받아서는 안 된다.”

2. 구조: 인터페이스를 잘게 나누기

ISP의 구조는 ‘역할’에 따라 인터페이스를 잘게 분리하는 것. 하나의 거대한 인터페이스 대신, 특정 역할에 맞는 여러 개의 작은 인터페이스를 만드는 것.

나쁜 구조: 단일 인터페이스

하나의 인터페이스(IWorker)에 모든 직원의 행동(work, eat)을 정의.

// 나쁜 예: 모든 직원의 행동을 포함하는 단일 인터페이스
interface IWorker {
    void work();
    void eat();
}

class Developer implements IWorker {
    public void work() {
        System.out.println("개발자가 코딩을 합니다.");
    }
    public void eat() {
        System.out.println("개발자가 점심을 먹습니다.");
    }
}

// 로봇은 일을 하지만, 먹지는 않음.
class Robot implements IWorker {
    public void work() {
        System.out.println("로봇이 부품을 조립합니다.");
    }
    // 불필요한 메서드. 비워두거나 예외를 던져야 함.
    public void eat() {
        // 아무것도 안 함
    }
}

위 예시에서 Robot 클래스는 eat() 메서드가 필요 없지만, IWorker 인터페이스 때문에 억지로 구현해야 함. 이는 불필요한 코드를 만들고, 의도를 파악하기 어렵게 만듦.

좋은 구조: 분리된 인터페이스

역할에 따라 인터페이스를 IWorkable(일하는)과 IFeedable(먹는)으로 분리.

// 좋은 예: 역할을 기준으로 인터페이스 분리
interface IWorkable {
    void work();
}

interface IFeedable {
    void eat();
}

// 개발자는 일도 하고, 밥도 먹음
class Developer implements IWorkable, IFeedable {
    public void work() {
        System.out.println("개발자가 코딩을 합니다.");
    }
    public void eat() {
        System.out.println("개발자가 점심을 먹습니다.");
    }
}

// 로봇은 일만 함
class Robot implements IWorkable {
    public void work() {
        System.out.println("로봇이 부품을 조립합니다.");
    }
}

이제 Developer는 필요한 두 인터페이스를 모두 구현하고, Robot은 자신에게 필요한 IWorkable 인터페이스만 구현함. 더 이상 불필요한 메서드를 강제로 구현할 필요가 없어졌으며, 각 클래스의 역할이 명확해짐.

3. 사용법: ISP 적용 단계

  1. 클라이언트의 요구사항 식별: 인터페이스를 사용하는 클라이언트(클래스)가 실제로 어떤 기능들을 필요로 하는지 분석.

  2. 거대 인터페이스 분리: 식별된 요구사항에 따라, 기존의 거대 인터페이스를 더 작고 응집도 높은(cohesive) 여러 개의 인터페이스로 분리. 이때 인터페이스의 이름은 ‘행위’나 ‘역할’을 나타내도록 짓는 것이 좋음 (예: Printable, Scannable).

  3. 필요한 인터페이스만 구현: 각 클라이언트 클래스는 자신이 필요로 하는 인터페이스들만 선택하여 구현.

이 과정을 통해 시스템의 유연성이 높아지고, 한 인터페이스의 변경이 다른 클라이언트에 미치는 영향을 최소화할 수 있음.

4. 심화 내용: 단일 책임 원칙(SRP)과의 관계

인터페이스 분리 원칙(ISP)은 종종 단일 책임 원칙(SRP)과 혼동됨. 둘은 비슷해 보이지만 초점이 다름.

  • 단일 책임 원칙 (SRP): 클래스가 변경되어야 하는 이유는 단 하나여야 한다는 원칙. 즉, 클래스는 하나의 책임(기능)만 가져야 함. 초점은 클래스의 구현에 있음.

  • 인터페이스 분리 원칙 (ISP): 클라이언트가 사용하지 않는 메서드에 의존해서는 안 된다는 원칙. 즉, 인터페이스는 클라이언트의 역할에 맞게 분리되어야 함. 초점은 클래스 외부의 인터페이스에 있음.

쉽게 비유하자면, SRP는 “요리사는 요리만 해야 한다(운전이나 수리까지 해서는 안 된다)“이고, ISP는 “손님에게 메뉴판을 줄 때, 주방에서 쓰는 식자재 목록(손님은 관심 없는 정보)까지 함께 주어서는 안 된다”와 같음.

하지만 ISP를 과도하게 적용하면 너무 많은 인터페이스가 생겨나 시스템이 오히려 복잡해질 수 있으므로, 항상 설계의 맥락과 실용성을 고려하여 균형을 맞추는 것이 중요함.

ISP를 잘 지키면 자연스럽게 SRP도 만족하게 될 가능성이 높으며, 이는 결국 더 유연하고, 재사용하기 쉽고, 유지보수가 용이한 소프트웨어로 이어짐.

이 핸드북이 인터페이스 분리 원칙을 이해하는 데 도움이 되셨나요? 혹시 특정 상황에서 ISP를 어떻게 적용할지 더 구체적인 예시가 필요하신가요?

References

인터페이스 분리 원칙