본문으로 바로가기

디자인 패턴과 SOLID 디자인 원칙 - ④ 인터페이스 분리 원칙

 

디자인 패턴은 소프트웨어 개발과정에서 자주 마주치게 되는 문제들에 적용할 수 있는 몇 가지 패턴들을 의미합니다. 디자인 패턴을 활용하면 읽기 쉽고, 이해하기 쉽고, 수정하기 쉬우며 재사용성이 높은 코드를 작성할 수 있게 됩니다. 그리고 이런 효율적인 코드를 작성하기 위해서 제안된 중요한 다섯 개의 원칙이 있습니다. 이 원칙들의 앞글자를 따서 SOLID 디자인 원칙이라 부르고 그 다섯 개의 원칙은 다음과 같습니다.

  • Single Responsibility Principle(SRP, 단일 책임 원칙)
  • Open-Closed Principle(OCP, 열림-닫힘 원칙)
  • Liskov Substitution Principle(LSP, 리스코프 치환 원칙)
  • Interface Segregation Principle(ISP, 인터페이스 분리 원칙)
  • Dependency Inversion Principle(DIP, 의존성 역전 원칙)

이 다섯개의 원칙들은 디자인 패턴이 존재하는 이유에 모두 녹아들어 있기 때문에 디자인 패턴을 본격적으로 다루기 전에 이 5개의 원칙들을 먼저 살펴볼 필요가 있습니다. 이번 글에서는 네 번째 인터페이스 분리 원칙을 살펴보겠습니다. 

Interface Segregation Principle(ISP, 인터페이스 분리 원칙)

인터페이스 분리 원칙이란? "사용하지 않는 함수에 의존성이 강요되지 않도록, 큰 인터페이스를 작은 인터페이스 단위로 분리하는 것"을 의미합니다. 이번에도 예시를 통해서 인터페이스 분리 원칙을 설명해보겠습니다.

ISP를 위배하는 설계

프린트, 팩스, 스캔이 모두 가능한 복합기 PrinterA를 제작하기 위해서 인터페이스 IMachine을 다음과 같이 만들었습니다. 그리고 IMachine을 상속받아 PrinterA를 다음과 같이 구현하였습니다.

class IMachine  {
public:
    virtual void print() = 0;
    virtual void fax() = 0;
    virtual void scan() = 0;
};

class PrinterA : IMachine {
public:
    void print() override { /* Print docs */ }
    void fax() override { /* Send docs */ }
    void scan() override { /* Scan docs */ }
};

그런데 이번에는 경량형으로 프린트만 가능한 PrinterB를 만들려고 합니다. 하지만 이미 IMachine에서 fax 기능과 scan 기능까지 인터페이스로 제공하고 있습니다. 그래서 PrinterB를 만들기 위해서는 어쩔 수 없이 IMachine을 상속받은 후 다음과 같이 빈 함수 구현해야 합니다. (혹은 예외가 발생하도록 해야합니다.)

class PrinterB : IMachine {
public:
    void print() override { /* Print docs */ }
    void fax() override {}
    void scan() override {}
};

만약 fax기능만 가능한 FaxA를 만들려면 마찬가지로 다음과 같이 또다시 빈 함수를 사용해야 합니다.

class FaxA : IMachine {
public:
    void print() override {}
    void fax() override { /* Send docs */}
    void scan() override {}
};

ISP의 정의는 "사용하지 않는 함수에 의존성이 강요되지 않도록, 큰 인터페이스를 작은 인터페이스 단위로 분리하는 것"이라고 했습니다. 위 예시처럼 PrintB와 FaxA는 사용하지도 않는 함수에 의존성이 강요되었기 때문에 ISP가 위배되었다고 할 수 있습니다. ISP를 만족시키기 위해서는 큰 인터페이스 IMachine을 작은 인터페이스로 분리할 필요가 있습니다.

ISP를 만족하는 설계

앞서 나온 예시의 문제는 IMachine 인터페이스가 너무 크다는 점입니다. 아래는 IMachine의 인터페이스를 print, fax, scane 기능 별로 분리한 모습입니다.

class IPrint  {
public:
    virtual void print() = 0;
};
class IFax {
public:
    virtual void fax() = 0;
};
class IScan {
public:
    virtual void scan() = 0;
};

class PrinterA : IPrint, IFax, IScan {
public:
    void print() override { /* Print docs */ }
    void fax() override { /* Send docs */ }
    void scan() override { /* Scan docs */ }
};

class PrinterB : IPrint { // need only print
public:
    void print() override { /* Print docs */ }
};
class FaxA : IFax { // need only fax
public:
    void fax() override { /* Send docs */}
};

print, fax, scan 기능이 모두 필요한 PrintA는 IPrint, IFax, IScan을 모두 상속받아 구현할 수 있고, print 기능만 필요한 PrinterB는 IPrint만 상속받아서 구현할 수 있습니다. 그리고 fax기능만 필요한 FaxA는 IFax만 상속받아서 구현할 수 있습니다. 이렇게 인터페이스를 분리하여 사용하지 않는 함수에 의존성이 강요되지 않는(즉, ISP를 만족하는) 설계를 살펴보았습니다.


<출처>

유튜브 채널 "코드없는 프로그래밍": https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg

길벗 출판사: 모던 C++ 디자인 패턴(Dmitri Nesteruk 저)