[객체지향 SW 설계의 원칙] ③ 인터페이스 분리의 원칙

일반입력 :2008/09/11 09:12

최상훈 (핸디소프트) 송치형 (서울대)

“사람은 다른 사람과 말을 할 때 듣는 사람의 경험에 맞추어 말해야만 한다. 예를 들어 목수와 이야기할 때는 목수가 사용하는 언어를 사용해야 한다.” - 플라톤의 파에톤(Phaethon)

“아무도 듣는 사람이 없는 숲 속에서 나무가 쓰러질 때 소리가 나는가?”라는 불가에서 유래한 질문이 있다. 그간 많은 현자들이 이 질문에 대해 ‘아니다’라는 일관된 대답을 해왔다. 소리는 지각되어야만 소리가 되기 때문에 나무가 쓰러질 때 음파는 발생하겠지만 듣는 사람이 없다면 소리는 없다는 것이다. 사람 사이의 커뮤니케이션도 마찬가지이다. 전달하려는 사람이 무언가를 외친다고 해도 듣는 사람이 없다면 커뮤니케이션은 없는 것이다.

만약 듣는 사람은 있지만 발신자가 수신자의 경험, 지식, 기대를 고려하지 않고 떠들어 대는 것은 어떨까? 이 역시 올바른 커뮤니케이션이라 할 수 없을 것이다. 수신자가 커뮤니케이션 메시지를 제대로 이해 또는 지각하지 못하거나, 기대와 어긋나 의식적으로 혹은 무의식적으로 메시지를 흘려버릴 가능성이 크기 때문이다. 그러므로 플라톤의 말대로 수신자의 경험, 역량 그리고 기대를 고려해 메시지를 전달해야만 제대로 된 커뮤니케이션이 될 수 있다.

맥킨지 일본 지사의 커뮤니케이션 스페셜리스트로 근무하고 있는 테루야 하나코는 좀 더 구체적으로 다음과 같이 이야기한다. “타인에게 무언가를 전하려고 할 때는 자신이 이야기하고 싶은 것을 어떻게 정리할까, 어떻게 말할까, 어떻게 쓸까를 생각하기 전에 반드시 과제(테마)와 상대방이 기대하는 반응을 확인하자.”

이 글을 읽고 있는 누구나 자신이 말하고 싶은 메시지를 더욱 잘 전달하기 위해 보고서를 몇 번씩 고치고 파워포인트의 디자인과 컬러에 공을 들여 본 경험이 있을 것이다. 그런데 혹시 이러한 노력이 과제 혹은 테마에 대해 자신이 말하고 싶은 것, 자신이 중요하다고 생각하는 것을 전달하기 위한 것은 아니었는지. ‘여러분’이 말하고 싶은 것, ‘여러분’이 중요하다 생각하는 것보다 더욱 중요한 것이 있다. 상대방이 전달받기를 기대하고 있는 ‘메시지’가 무엇이냐 하는 것이다.

여러분이 아무리 많은 준비를 했고, 많은 데이터를 축적했고, 하고 싶은 말이 많더라도 이러한 내용은 상대방의 기대라는 필터를 통해 여과되어 전달되어야 한다. 그렇지 않고 준비한 자료가 아까워서 이것저것 모두 전달하는 것은 수신자에게는 일종의 고역이다. 물론 준비한 노력과 자료 중 일부를 버려야 하겠지만 일부를 버리고 효과적으로 메시지를 전달하는 것이 모든 자료를 끌어 앉은 채 메시지를 허공 속으로 흘려보내는 것보다 훨씬 현명한 선택일 것이다.

객체지향 시스템은 메시지를 통해 커뮤니케이션하는 수많은 객체들로 구성된다. 그리고 이들 객체 간의 통신에도 앞에서 언급한 커뮤니케이션의 논리가 그대로 적용된다. 즉 상대방이 기대하고 있는 메시지를 ‘군더더기 없이’ 전달해야 하듯 서비스를 제공하는 객체는 자신을 이용하는 객체에게 해당 객체가 기대하는 서비스만을 제공해야 한다는 것이다. 이번 호에서 살펴볼 인터페이스 분리의 원칙(Interface Segregation Principle, 이하 ISP)이 바로 이러한 원칙을 설명해 준다.

인터페이스 분리의 원칙 개요

요즘 복제폰으로 인한 불법 결제가 사회적인 문제가 되고 있다. 특정 사용자의 폰 정보를 복제해 온라인상에서 불법 결제를 하는 것이다. 불법 결제자는 실물을 구매하거나 현금으로 환금 가능한 온라인 화폐 등을 결제하는데, 요금은 실제 폰 사용자에게 과금된다. 그래서 휴대폰 결제 서비스를 제공하는 모 업체에서는 이런 복제폰 사용자들의 패턴을 찾아 차단하고자 한다. 불법 결제 차단 시스템은 자사의 결제 시스템을 이용하는 각 컨텐츠 프로바이더(CP, Contents Provider) 별로 룰을 설정해 불법 결제 패턴에 해당하는 사용자의 결제를 차단하고, 이에 대해 회사 내부 담당자와 컨텐츠 프로바이더 담당자에게 특정 시간에 메일로 리포팅을 하게 된다. 이를 위해 이 회사의 개발자 A군은 다음과 같은 구조의 프로그램을 만들었다.

그런데 만약 이메일 리포팅 외에 SMS를 통한 리포팅 기능도 구현해 달라는 요청이 들어왔다면 어떻게 해야 할까? SMS와 관련된 메쏘드를 추가하거나, 현재의 이메일 관련 메쏘드를 SMS 관련 메쏘드와 통합해 getReportingRule과 같은 메쏘드를 만들어야 할 것이다.

그런데 이러한 메쏘드 추가는 이와는 전혀 상관 없는 BlockTransaction 클래스에게까지 영향을 미치게 되고, BlockTransaction 클래스의 재컴파일, 재배포 등과 똑같은 문제를 야기한다.

왜 이런 문제가 발생했을까? 원인은 BlockTransaction과 EmailReporting과 같은 클래스가 CPRule이 제공하는 서비스 중 일부만을 사용하는데 있다. CPRule 입장에서는 BlockRule과 EmailRule 모두를 갖는 것이 응집력 있는 구성이지만 BlockTransaction의 입장에서 보면 getEmailRule()은 필요없는 서비스이다. 역시 EmailReporting의 입장에서는 getBlockRule()이 필요없는 서비스라 할 수 있다. 즉 CPRule이 자신을 이용하는 클라이언트의 입장을 고려하지 않고 클라이언트에게 자기가 하고 싶은 말을 다 하고 있는 것이다. 현재는 CPRule을 이용하려면 모든 서비스를 이용하는 방법 밖에는 없다.

ISP를 통해 이런 문제를 해결할 수 있다. ISP를 간단히 정의하면 “클라이언트는 자신이 사용하지 않는 메쏘드에 의존 관계를 맺으면 안된다”는 것이다. 그런데 어떻게 자신이 사용하지 않는 메쏘드에 의존 관계를 맺지 않게 할 수 있을까? 답은 인터페이스를 사용하는 것이다. 이에 따라 앞의 프로그램 구조를 바꾸어 보면 다음과 같다.

 

CPRule은 CPBlockRule과 CPEmailRule이라는 두 개의 인터페이스를 구현한다. 그리고 BlockTransaction과 EmailReporting은 CPRule을 직접 이용하지 않고 각각 자신의 기대에 맞는 인터페이스를 통해 CPRule이 제공하는 서비스를 이용할 수 있게 된다.

이제 SMS 관련 기능을 기존 이메일 기능과 통합해 제공하든, 별도로 제공하든 결제 차단 클래스는 전혀 변화의 영향을 받지 않게 된다. 결제 차단 규칙이라는 인터페이스가 이러한 변화의 방벽 역할을 해주기 때문이다. 그리고 SMS 기능을 이메일과 별도로 추가한다면 기존의 이메일 리포팅 역시 변화의 영향을 받지 않는다(이메일 기능과 SMS 기능을 통합할지 말지는 둘 사이에 얼마나 공통성과 유사성이 있느냐에 따라 달라지게 될 것이다).

ISP를 ‘하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다’라고도 정의할 수 있다. 만약 어떤 클래스를 이용하는 클라이언트가 여러 개 있고, 이들이 해당 클래스의 특정 부분집합만을 이용한다면 이들을 따로 인터페이스로 빼내어 클라이언트가 기대하는 메시지만을 전달할 수 있도록 하는 것이다.

첫 연재에서 보았던 OCP에서는 ‘확장’이 핵심이었고 잘 정의된 공통의 인터페이스를 통해 수정에는 ‘닫고’ 확장에는 ‘열려 있는’ 구조를 만들었다. SRP에서는 ‘변화’가 핵심이었고 각 클래스가 하나의 책임만을 갖도록 하여 변화가 다른 클래스로 미치지 않도록 하는 구조를 가능케 하였다. 그러면 ISP의 핵심은 무엇일까? SRP와 마찬가지로 ‘변화’가 관전의 포인트가 된다. ISP는 뒤에서 설명하는 ‘ISP vs. SRP’에서 설명하듯이 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 가질 수 있다는 것을 인정한다. 이러한 경우 ISP가 사용되는데 SRP가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면, ISP에서는 인터페이스 분리를 통해 같은 목표에 도달하게 된다. 이제 전형적인 ISP 구조와 케이스 스터디로 넘어가기로 하자.

 

전형적인 ISP 구조

<그림 3>을 보면 Service 인터페이스는 3개 군의 메쏘드 집합을 제공하며 각 클라이언트는 이들 중 일부만 사용하고 있다. 그림과 같은 경우에는 Service 인터페이스의 어느 하나가 바뀌면 세 클라이언트 모두를 재컴파일, 재배포해줘야 한다.

 

이제 <그림 3>에서 Service가 제공하는 메쏘드를 클라이언트가 사용하는 기준별로 그룹핑한다. 그리고 각 클라이언트는 자신의 기대에 부응하는 인터페이스만을 이용하게 된다. 이와 같이 인터페이스를 분리하면 한 클라이언트에서의 변화가 다른 쪽으로 확산되지 않는다.

자바 스윙이 제공하는 JTable 클래스는 이와 같은 ISP의 좋은 예제가 된다. JTable 클래스에는 굉장히 많은 메쏘드들이 있다. 컬럼을 추가하고 셀 에디터 리스너를 부착하는 등 여러 역할이 하나의 클래스 안에 혼재되어 있지만 JTable의 입장에서 본다면 모두 제공해야 하는 역할이다. JTable은 ISP가 제안하는 방식으로 모든 인터페이스 분리를 통해 특정 역할만을 이용할 수 있도록 해준다.

즉 Accessible, CellEditorListener, ListSelectionListener, Scrollable, TableColumnModelListener, TableMoldelListener 등 여러 인터페이스 구현을 통해 서비스를 제공하는 것이다. JTable은 자신을 이용하여 테이블을 만드는 객체, 즉 모든 서비스를 필요로 하는 객체에게는 기능의 전부를 노출시키지만, 이벤트 처리와 관련해서는 여러 리스너 인터페이스를 통해 해당 기능만을 노출시키고 있다.

수준에 따른 EAI 인터페이스

소프트웨어의 일반적인 목적은 업무를 자동화하고 인력을 시스템으로 대체화시키는 것이다. 이를 통해 새로운 비즈니스가 만들어지기도 하고 인간의 업무영역이 줄어들기도 한다. 지난 IT의 역사는 기업에서 사람이 행하던 업무를 전산화하여 업무의 효율을 극대화시키는 것이었다. 여러 가지 기술의 도움으로 점차 기업 시스템의 전산화가 차지하는 비율은 확대되고 있다. 하지만 실제 기업 업무가 그렇듯, 대부분의 비즈니스는 하나의 부서, 하나의 기업에서 완결되는 것이 아니라 서로 유기적인 소통을 통해서 이루어진다.

또한 기존에 구현된 시스템과 새로 구축되는 시스템간의 의사소통도 만만치 않은 작업이다. Aberdeen Group(http://www.aberdeen.com/) 보고서에 의하면 이런 시스템간의 통합 요구는 시간이 지날수록 많아지고 있다. 이런 요구사항에서 등장된 개념이 EAI(Enterprise Application Integration) 개념이다. 즉 EAI는 기업 내, 기업 간의 서로 다른 애플리케이션 인터페이스를 통합하기 위해 제안된 기술이다.

우리는 흔히 ‘인터페이스’라고 하면 객체나 컴포넌트의 인터페이스를 떠올리곤 한다. 하지만 인터페이스는 여러 타입과 여러 방식이 사용될 수 있으며 수준에 따른 EAI 인터페이스를 소개하면서 인터페이스의 개념을 확장하려고 한다. 다음은 EAI에서 분류하는 애플리케이션 통합을 위한 (인터페이스의) 종류이다. 더불어 결합도와 확장성의 관점을 더해서 생각해 보는 것도 좋을 듯하다.

데이터 수준의 통합을 위한 인터페이스

두 개 이상의 애플리케이션이 서로 통합하기 위해서 가장 간단한 방법은 한곳에 데이터베이스를 두고 모든 애플리케이션이 데이터베이스를 통하여 정보를 주고받는 것이다. 한 애플리케이션이 데이터베이스에 정보를 저장하면 그 정보에 관심있어 하는 다른 애플리케이션들이 그 정보를 발견하여 그 정보에 대한 처리를 한다. 즉, 이 때 데이터베이스는 하나의 애플리케이션들이 정보를 교환하는 일종의 인터페이스가 된다. 이런 아키텍처 스타일을 공유 리파지토리 패턴이라고 한다.

하지만 (잘 일어나지 않지만) 데이터베이스의 스키마가 변경되거나 새로 테이블이 추가되어 데이터베이스가 복잡해진다면? 즉 인터페이스가 바뀌거나 확장되게 된다면? 모든 애플리케이션이 이 구조에 따라서 애플리케이션이 변경되어야 한다. 즉, 잘 분리, 디자인되지 못하고 확장이 용이하지 못한 인터페이스는 거대한 변경에 대한 비용이 발생하게 된다.

또한 기존의 테이블 구조가 사용하려는 애플리케이션이 원하는 구조를 만족하지 못했을 때 데이터베이스에서 원하는 특정 정보를 다른 데이터베이스로 이동(migration)시키거나 여러 개의 데이터베이스에 있는 정보들을 취합하게 된다. 즉, 불필요한 인터페이스의 복제, 재정립하는 비용이 발생하게 된다. 이를테면 학사행정 시스템에서 ‘학생’ 데이터베이스와 ‘임직원’이란 데이터베이스가 있을 때 이 두 데이터베이스의 정보를 취합하여 ‘예비군’이란 데이터베이스가 만들어져야 하는 경우가 이에 해당한다.

이 방법은 매우 단순하고 빠르게 시스템 통합을 구축할 수 있다. 하지만 결과적으로 여러 다른 애플리케이션이 통합되기 위해서 중앙에 데이터 수준의 통합을 위한 인터페이스를 두고 모든 애플리케이션이 이 인터페이스에 맞춰야 한다. 이로써 확인되는 한계는 하나의 인터페이스가 여러 목적의 애플리케이션과 일대 다의 관계를 가질 때 여러 애플리케이션간의 관계가 하나의 인터페이스로 강결합되어 확장이나 변경의 자유도가 매우 떨어지게 된다. 이것은 ISP의 핵심인 “클라이언트가 자신이 사용하지 않는 메쏘드에 의존하도록 강제되어서는 안된다”는 원리에 위배된다. 이 구조는 인터넷 기반에 여러 서비스를 제공해야 하는, 따라서 인터페이스가 유연해야 하는 현대 시스템 상황에 적절하지 않은 방법이다.

또한 이 구조에서 중요한 축이 되는 데이터베이스가 변경이 될 경우 모든 애플리케이션이 같이 변경해야 하는 거대한 리스크를 안고 있다. 그리고 데이터베이스는 여러 애플리케이션이 원하는 인터페이스를 구축해야 하기 때문에 테이블 (인터페이스) 설계가 복잡하고 난해하게 이루어져 결국 이해를 위해 높은 진입장벽을 만나게 된다.

애플리케이션 수준의 통합을 위한 인터페이스

데이터 수준의 통합을 위한 인터페이스의 단점이 여러모로 확인된바 데이터가 아닌 API 수준의 통합이 제안된다. 기존에 우리가 사용하던 라이브러리나 패키지를 사용자(다른 애플리케이션)에게 제공하여 인터페이스를 통해 연동을 구축한다. 단순하고 독립적이지 않은 서비스의 경우 라이브러리나 패키지 형태로 배포되어 컴파일 타임에 바인딩하여 사용할 수 있다.

하지만 독립적인 애플리케이션간의 연동은 두 개 이상의 프로세스가 서로 통신(Inter-Process Communication)을 해야 하는데 IPC 방식에서는 연동을 위한 프로토콜이 정의되어야 한다. 즉, 프로토콜은 애플리케이션 간에 연동을 위한 인터페이스 역할을 한다. 나아가서 요즘 애용되고 있는 XML 전송에서 XML 스키마가 프로토콜과 같은 맥락을 갖는다. 좋은 IPC 구조를 위해서 잘 정의된 프로토콜이 전제되어야 한다. 프로토콜이 복잡하고 장황할수록 연동을 위한 비용이 많이 든다. 즉, 인터페이스가 지저분하지 않을수록 효과적이고 연동비용이 적게 든다. 실제로 잘 정의된 프로토콜(인터페이스)은 그 프로토콜 안에 여러 인터페이스를 식별하기 쉬운 구조를 제공한다(프로토콜 내의 ISP). 가령 HTTP의 경우 GET, POST, HEAD… 등의 메시지 식별이 단순하도록 구성되어 있다.

세련된 인터페이스를 제공하는 기술이 RPC(Remote Procedure Call) 기술이다. RPC는 애플리케이션이 실제 함수 호출하듯 원격지에 있는 함수의 시그니처를 모방한 프록시를 두어, 그 프록시를 통해 원격 함수 호출을 제공한다. IPC 방식에서 RPC 방식으로 전환은 진화라 할 만큼 획기적이다. 프로토콜을 이용한 연동방식에서 원격 함수 호출을 이용한 호출방식으로 전환됐기 때문이다. 이것은 의미적으로 인터페이스를 더욱 단순하고 분리하기 쉽게 사용하도록 유도하고 있다.

비즈니스 로직 수준의 통합을 위한 인터페이스

애플리케이션 수준의 인터페이스와 비즈니스 로직 수준의 통합을 위한 인터페이스의 근본적인 차이점은 인터페이스의 형태가 일반 함수 수준에서 비즈니스 수준의 인터페이스를 제공한다는 것이다. 가령 전자의 경우 ‘복사’, ‘저장’, ‘~접근자’ 수준의 인터페이스를 제공하는데 반해 비즈니스의 경우 ‘입고’, ‘이체’, ‘결제’ 수준으로 인터페이스가 비즈니스화 되었다.

즉, 비즈니스 인터페이스는 분리된 객체 인터페이스의 묶음으로 해석할 수도 있다. 따라서 인터페이스가 큰 분류로 정의됨에 따라 복잡하던 인터페이스간의 관계가 단순하게 이루어진다. 객체 관계에 비교해 볼 때 컴포넌트 관계가 상대적으로 매우 단순하게 이뤄지는 이유가 여기에 있다. 정리하자면 비즈니스 인터페이스는 비즈니스 목적에 의해서 복잡한 객체 인터페이스를 구분하여 분류한 Facade (GoF의 Facade 패턴 참조) 역할을 한다.

이때부터 연동을 위해 본격적으로 CORBA, COM, EJB와 같은 기술들이 사용된다. 비즈니스 인터페이스를 통해 얻을 수 있는 이점은 비즈니스의 공유하여 재사용하기 위한 목적이다. 여기서 읽을 수 있는 현상은 잘 분리된 인터페이스는 재사용도가 높아진다는데 있다. 각 언어에서 제공하는 API는 근본적으로 재사용을 전제하기 때문에 인터페이스를 관찰해 보면 엄격하게 ISP 원칙을 지키고 있음을 볼 수 있다.

유저 인터페이스 수준의 통합을 위한 인터페이스

EAI에서의 유토피아는 모든 애플리케이션의 인터페이스를 접근하는데 있어서 마치 사람이 접근하듯이 접근하는 방법이다. 가령 엑셀에 연동하기 위해서 마치 사람이 엑셀을 다루는 것 같이 애플리케이션이 엑셀에 이벤트를 발생하여 연동하는 방식이다. 또 다른 예는 웹 서비스에 마치 사람이 웹 브라우저를 조작하여 문서를 요청하는 것 같이 애플리케이션이 HTTP 리퀘스트를 던져 결과를 얻어오는 것이다.

유저 인터페이스 수준의 통합을 위한 인터페이스는 이렇게 사람이 스크린을 통해 애플리케이션을 사용하듯 애플리케이션간의 연동이 이루어지는 방식을 말한다. 이런 요청을 스크린 카탈로그라 하며 이런 요청 방식을 스크린 스크래핑(screen scrapping)이라고 한다. <그림 9>는 애플리케이션이 여러 장의 스크린 카탈로그를 만들어 타겟 애플리케이션에 연동하는 장면이다.

EAI 인터페이스 타입에서 본 바와 같이 컴포넌트의 확장성을 높이기 위해 결합도를 낮춰야 한다. 그렇기 위해 컴포넌트간의 관계에서 잘 분리된 인터페이스를 구성하여 관계를 단순화 하는 노력이 필요하다. 이 단순화된 관계는 컴포넌트간의 응집성을 향상시킨다.

인터페이스 분리 방법

SRP(지난 호 참조)에서 부분적으로 언급한 바와 같이 서로 목적이 다른 클라이언트가 하나의 인터페이스를 접근하고 있다면 그 인터페이스는 분리되어야 마땅하다. 왜냐하면 다른 클라이언트에 의해 나와 무관한 인터페이스가 변경됐을 때 그 변경으로 인해 내가 사용하는 인터페이스가 변경될 수 있기 때문이다. 따라서 서로 다른 종류의 클라이언트들이 하나의 인터페이스에 접근한다면 그 클라이언트의 종류만큼 인터페이스는 분리되어야 한다.

다시 처음에 예시한 <그림 1> 초기의 불법 결제 차단 시스템의 예제를 보자. CPRule 클래스의 getBlockRules(), getEmailRule()는 서로 성격이 다른 클라이언트에 의해 접근되고 있다. getBlockRules()는 Block Transactoin 클래스에 의해서 getEmailRule()은 Email Reporting 클래스에 의해서 사용된다. 인터페이스를 사용하는 클라이언트가 명백히 분리된다는 것은 인터페이스가 그 클라이언트의 개수만큼 다른 서비스를 제공한다는 의미이다. 하지만 애초에 이 여러 개의 인터페이스가 같이 있다는 것은 나름대로의 ‘공유, 연관되는 무엇인가’가 있다는 것이다.

그렇다면 공유, 연관되는 부분은 그대로 두되 효과적으로 인터페이스를 분리하는 방법이 필요하다. 여기서 몇 가지 규칙이 필요하다. ① 기 구현된 클라이언트의 변경을 주지 말아야 할 것 ② 두 개 이상의 인터페이스가 공유하는 부분의 재사용을 극대화할 것 ③ 서로 다른 성격의 인터페이스를 명백히 분리할 것 등이다. 분리 방법은 클래스 인터페이스를 통한 분리와 객체 인터페이스를 통한 분리를 이용할 수 있다.

 

클래스 인터페이스를 통한 분리

다중 인터페이스를 분리하는 방법으로 클래스의 다중 상속을 이용하여 인터페이스를 나눌 수 있다. <그림 10>에서와 같이 ‘결제 차단’에 관련한 인터페이스를 제공하는 getBlockRules()를 포함하는 BlockRule 인터페이스와 이메일 처리를 전담하며 getEmailRule()을 구현하는 EmailRule 인터페이스로 분리하여 인터페이스를 정의할 수 있다. 하지만 분리된 두 인터페이스는 상술하여 제시한 첫 번째 원칙인 ‘기 구현된 클라이언트의 변경을 주지 말아야’ 하는 조건을 만족해야 한다. 즉, 클라이언트 클래스는 기존의 인터페이스를 그대로 유지하여 변경없이 접근할 수 있어야 한다. 이런 조건을 만족하기 위해서 EmailRule과 BlockRule 클래스를 다중상속하는 CPRule이 정의될 수 있다.

이와 같은 구조는 Block Transaction, Email Reporting 클라이언트 모두에게 변화의 영향을 주지 않을 뿐 아니라 인터페이스를 분리하는 효과를 갖는다. 하지만 거의 모든 객체지향 언어에서는 상속을 이용한 확장은 상속받는 클래스의 성격을 디자인 시점에서 규정해 버리는 특징이 있다. 따라서 CPRule 클래스는 Block Transaction 인터페이스와 Email Reporting 인터페이스를 상속받는 순간 이 두 인터페이스에 예속되어 제공하는 서비스의 성격이 제한된다.

객체 인터페이스를 통한 분리

다른 방법으로는 위임(delegation)을 이용한 방법이 있다. 이 방법은 CPRule의 필드로 Rule이란 객체를 갖는다. Rule 객체는 인터페이스인데 BlockRule과 EmailRule이 각각의 서비스에 맞는 rule() 메쏘드를 구현한다. 이런 상황에서 Block Transaction 클라이언트는 getBlockRule() 메쏘드를 통해 BlockRule 클래스의 rule() 메쏘드를 호출한다. Email Reporting의 경우도 마찬가지로 동작한다. 즉 위임을 이용해서 getBlockRule(), getEmailRule() 메쏘드는 멤버변수 rule 객체의 rule() 메쏘드를 호출하게 된다.

위임을 이용한 객체의 인터페이스 분리는 Rule이란 객체를 확장한 어떤 구현도 대체될 수 있다. 만약 암호화된 EmailRule이 필요하다면 Rule 인터페이스를 구현하는 암호화된 SecuredEmailRule이라는 클래스를 정의하여 CPRule의 rule 필드에 꼽아주면 확장이 용이하다.

위임을 이용한 확장과 상속을 이용한 확장은 똑같이 인터페이스를 분리하는 기능을 하지만 서비스 결정권의 차이가 있다. 대부분의 객체지향 언어에서 상속의 경우 컴파일 시점에서 부모의 구현을 차용하게 되고 부자간의 관계는 변경이 불가능하다(자식 클래스는 부모를 바꿀 수 없다). 반면, 위임을 이용한 관계는 런타임에 변경이 가능하다. 가령 일반 이메일 모드에서 암호화 이메일 모드로 역할이 변경될 경우 CPRule.rule 변수를 EmaileRule에서 SecuredEmailRule 객체로 바꿔주기만 하면 서비스의 변경이 동적으로 이뤄진다. 따라서 위임을 이용한 확장은 상속을 위한 확장보다 관계 설정의 변화가 상대적으로 유연하다.

다른 방식으로는 C++에서 템플릿을 이용한 확장 방법을 사용할 수 있다. 자바에서는 Generic 프로그래밍을 통해서 구현할 수 있는데 컴파일 시점(정확히 pre-processing 시점)에서 적용되는 클래스의 타입을 설정해줌으로써 확장성을 지원할 수 있다.

사실은 앞의 두 가지 분리 방법은 GoF의 Adapter 패턴에서 구현 방법 중 Class Adapter와 Object Adapter 구조를 그대로 차용한 방법이다. 하지만 정 반대의 목적을 갖는다. Adapter 패턴의 경우 기존의 구현된 인터페이스를 통해 클라이언트가 원하는 인터페이스로 개조하는 목적을 갖고 있지만 우리의 목적은 서로 다른 서비스를 하는 인터페이스를 분리하려는 목적이다. 하지만 목적과 해결 대상이 비꼈을 뿐이지 그 구조는 동일하다.

ISP vs. SRP

일반적으로 인터페이스와 역할은 1:1 관계를 갖는다. 하지만 인터페이스가 여러 역할을 갖는 경우도 있다. 이것은 컴포넌트의 크기에 따라 결정되게 되는데, 가령 ‘이체’라는 인터페이스는 ① 트랜잭션을 시작한다. ② 상대의 계좌 존재 여부를 확인한다 ③ 고객의 계좌에 원하는 금액을 출금한다 ④ 상대의 계좌에 입금한다 ⑤ 트랜잭션을 마친다의 절차를 거친다. 여기에 참여하는 역할들은 서로 응집성이 결여된다.

하지만 특정 비즈니스 목적에 의해 하나의 처리 절차로 묶이면서 ‘이체’라는 인터페이스를 만족시킨다. 다시 말하자면, ‘이체’라는 인터페이스는 여러 역할의 묶음으로 구현된다(1:n 관계의 인터페이스와 역할). 이 때 ‘이체’ 인터페이스는 이런 역할의 묶음의 Facade가 된다. 앞에서 살펴본 JTable의 경우에도 테이블의 데이터 관리, 컬럼 관리, 렌더링 관리, 이벤트 관리라는 여러 역할이 JTable이라는 하나의 클래스 안에 혼재되어 있다.

반면 하나의 역할은 여러 인터페이스로 분해되기도 하는데 자바의 java.io.OutputStream 클래스는 데이터를 쓰는 write() 인터페이스를 갖고 있다. 똑같은 역할을 하지만 인터페이스가 다른 java.io.PrintWriter 클래스는 printXX() 류의 인터페이스로 그 역할을 세련되게 제공한다(1:n 관계의 역할과 인터페이스). 이처럼 인터페이스와 역할과의 관계는 상호의존성을 갖지만 꼭 같은 의미로 사용되지도 않는다. 따라서 ISP와 RSP를 구분할 때 역할과 인터페이스의 맥락은 명백히 다르게 규정하여 접근할 필요가 있다. 즉 SRP를 접근할 때와 ISP를 접근할 때의 접근법은 차이를 두어야 한다.

ISP를 지키지 않았을 때 발생할 수 있는 악취

앞에서 살펴본 바와 같이 규모가 큰 클래스에서만 ISP가 문제되는 것은 아니지만, ISP와 관련한 문제는 대부분 비대한 클래스로부터 발생하게 된다(JTable 클래스를 한번 보라). 비대한 클래스라는 악취는 Extract Class, Extract Subclass, Extract Interface와 같은 리팩토링 기법을 통해 해결하게 되는데 이중 Extract Class와 Extract Subclass는 SRP와 연관이 있고 Extract Interface가 ISP를 따라는 설계를 가능하게 해준다.

 

* Extract Interface : 클라이언트가 클래스의 어느 특정 기능만을 이용한다면 이러한 기능의 부분 집합을 별도의 인터페이스를 통해 추출하라.

우리는 친구들에겐 친구가 되고, 부모님께는 자식이 되고, 사랑하는 사람에겐 연인이 된다. 같은 존재이건만 상대에 따라 우리의 역할은 다르다. 하지만 친구가 없다면 우린 친구가 될 수 없으며, 부모님이 계시지 않는다면 자식이 될 수 없고, 애인이 없다면 누군가의 연인이 될 수 없는 노릇이다. 우리의 상대에 대해 좀 더 관심과 애정을 기울이고 배려의 노력을 해야 하는 것은 아닐지.@

* 이 기사는 ZDNet Korea의 제휴매체인 마이크로소프트웨어에 게재된 내용입니다.