SOLID의 네 번째 원칙인 인터페이스 분리 원칙Interface Segregation Principle에 대해 알아보자.

인터페이스 분리 원칙이란?

“CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.”

Robert Martin. ISP: The Interface Segregation Principle

사용하지도 않는 인터페이스에 클라이언트가 의존해서는 안된다. 클라이언트 간의 의도치 않은 결합을 피하기 위해서이다. A라는 인터페이스가 있고 이를 사용하는 클라이언트 B, C가 있다고 해보자. A의 특정 메서드를 B가 사용하지만 C는 사용하지 않을 경우, 이 메서드가 변경될 때 마다 C는 자신과 상관 없는 이유로 영향을 받게 된다.

뚱뚱한 인터페이스

ISP를 간단한 예제와 함께 좀 더 살펴보려고 한다. 문을 열고 닫으며, 열림 여부를 확인할 수 있는 Door 인터페이스가 있다. 만약, 일정 시간이 지났을 때 문이 자동으로 닫히게 하려면 어떻게 해야 할까? 한 가지 방법은 TimerClient라는 인터페이스를 추가하고 Door가 이를 구현하는 것이다. 아래 그림을 보자.

그림 1. 뚱뚱한 인터페이스

Timer는 일정 시간이 지났음을 통지하기 위해 TimerClient 인터페이스의 timeout을 호출한다. 호출 대상은 register 메서드로 등록하며, 코드는 다음과 같다.

public interface Timer {
   void register(int timeout, TimerClient timerClient);
}

public interface TimerClient {  
   void timeout();
}

public interface Door extends TimerClient {
   void lock();
   void unlock();
   boolean isDoorOpen();
}

public class TimedDoor implements Door {

   private boolean locked;

   @Override
   public void lock() {
       locked = true;
   }

   @Override
   public void unlock() {
       locked = false;
   }

   @Override
   public boolean isDoorOpen() {
       return !locked;
   }

   @Override
   public void timeout() {
       lock();
   }
}

요구 사항은 만족시켰지만 몇 가지 문제점들이 보인다. 우선, TimerClient의 상속은 오로지 타이머 관련 구현체 만을 위한 노력이다. 타이머 기능이 필요 없는 구현체에서는 timeout 구현이 무의미 하며 오히려 부담이 될 뿐이다. 이 부담을 덜기 위해 상위 클래스를 만들고 아무 것도 하지 않는 구현을 기본적으로 제공한다 해도 LSP(리스코프 치환 원칙)를 위반하게 된다. 무엇보다도, 이런 식의 상속이 계속된다면 Door는 머지 않아 뚱뚱한 인터페이스fat interface가 될 것이다.

위임을 통한 분리Separation through Delegation

위 문제를 해결하기 위한 한 가지 방법은 위임이다.

그림 2. 위임을 통한 분리

DoorTimerAdapterTimerClient의 구현체로써 Timer로부터 타임아웃을 통지 받는다. 이 때 통지 받은 메시지는 TimedDoor에게 바로 위임된다. 위임을 통해 기존 인터페이스의 수정 없이 새로운 인터페이스를 지원 한다는 점에서 객체 형식의 어댑터 패턴이다.

코드는 다음과 같다.

public class DoorTimerAdapter implements TimerClient {

   private Door door;

   public DoorTimerAdapter(Door door) {
       this.door = door;
   }

   @Override
   public void timeout() {
       door.lock();
   }
}

이제 Door의 클라이언트와 Timer 간의 결합은 사라졌다. 게다가 기존 TimedDoor를 변경할 필요가 없으며, Timer의 어떠한 변경에도 Door의 사용자는 영향 받지 않는다.

다중 인터페이스를 통한 분리separation through Multiple Interface

또 다른 해결책으로 다중 인터페이스 방식이 있다. 이는 TimedDoorDoorTimerClient 모두를 구현하는 것이다. 이렇게 하면 서로 다른 클라이언트가 TimedDoor에 의존하지 않으면서도 TimedDoor를 사용할 수 있다. 인터페이스가 분리 되어 있으므로 인터페이스의 변경은 이를 사용하는 클라이언트에만 영향을 미친다. 또한 기존 Door의 구현체들이 TimedDoor에만 필요한 timeout을 일일이 구현하지 않아도 된다.

위임을 통한 분리와는 어떤 차이가 있을까? 기존 코드의 변경 없이 새로운 방식의 커뮤니케이션을 지원하므로 어댑터 패턴인 것은 동일하지만, 클래스에 새로운 인터페이스를 추가했다는 점에서 클래스 형식의 어댑터 패턴이다. 따라서 객체 형식의 어댑터와 차이점이 무엇인지 생각해 보면 된다. 클래스 형식의 객체 형식 대비 장단점은 다음과 같다.

  • 타임아웃 등록을 위한 별도의 객체 생성이 필요 없다.
  • 단지 위임만을 수행하는 코드 작성이 필요 없다.
  • 다른 Door 구현체가 TimerClient가 되어야 하는 경우 timeout을 일일이 구현해야 한다.
  • TimedDoor의 인스턴스화 시 Door의 구현부와 TimerClient 구현부가 항상 생명 주기를 함께 한다.

참고 문헌

[1] Robert Martin. ISP: The Interface Segregation Principle
[2] wikipedia. Adapter Pattern