인터페이스 분리 원칙에 대해

다 알고 있는 사실

어떤 코드도 자신이 사용하지 않는 메소드에 의존하면 안된다
자신이 사용하는 메서드에만 의존해야 한다

매번 나오는 설명입니다.
보통 이야기를 들어도 잘 이해가 안가기 때문에 보통 예제를 봅니다.

interface Person {
	public void work();
	public void eat();
	public void play();
}

class SomePerson implements Person{
	public void work() {
        ...
	}
	public void eat() {
        ...
	}
	public void play() {
		...
	}
}

class VeganPerson implements Person{
	public void work() {
        ...
	}

	public void eat() {
        ...
	}
	public void play() {
		...
	}
}

하지만 eat 메소드가 필요 없는 Worker가 등장할 경우 예외 처리를 하거나 의미없는 반환값 등으로 구현을 해야합니다.

따라서

interface Workable {
	public void work();
}

interface Feedable{
	public void eat();
}

interface Playable{
	public void eat();
}

class Person implements Workable, Feedable, Playable{
	public void work() {
		...
	}

	public void eat() {
		...
	}

	public void play() {
		...
	}
}

이렇게 짜라고 알려져 있습니다.

근데 왜?

ISP를 준수하면 얻는것이 무엇인가?

  • 변경에 대해서 유연한 것 ?
  • 불필요한 책임 제거 ?
  • 확장성 향상?

이것들은 SRP나 다형성에 대한 것들이지, ISP와는 거리가 있다고 생각합니다.

의존하는 모듈을 생각하라

앞서 언급된 설명은 클래스에 구현에 초점이 맞춰져 있습니다.
하지만 ISP의 의의는 의존관계에 있다고 생각합니다.
클래스의 의존관계의 측면에서 ISP를 준수하지 않는 경우를 살펴보도록 합시다.

alt text

하나의 클래스에 모듈 A,B,C에 필요한 메소드들이 모두 선언되어있습니다
여기서 문제는 모듈 A가 자신이 사용하지 않는 메소드 (2,3,5,6)을 호출하는것을 막지 못한다는것입니다.
모듈 B와 C도 마찬가지입니다.

다시 이전 코드 예시로 돌아가겠습니다.
ISP 를 준수하지 않은 Worker 인터페이스를 의존하는 모듈을 가정해봅시다.

class WorkPlace {
	private List<Person> workers;

	public void addWorker(Person worker) {
		workers.add(worker);
	}

	public void allWork() {
		for(Person worker : workers) {
			worker.work();
		}

		//troll
		worker.play();
	}
}

WorkPlace 클래스 내부에서 Worker을 직접적으로 의존하게되면 다른 메소드를 호출하는것이 가능해져버립니다.

이 문제를 해결하는 방법은 두가지가 존재합니다.

소유 모델

alt text

class Person {
	private Workable work;
	private Playable play;
	private Eatable eat;
}

이후 각 모듈들은 Person이 소유하는 내부 객체만을 바라보면서 소통할 수 있습니다.

인터페이스 모델

alt text

interface Person extends Workable, Playable, Eatable {

}

마찬가지로 외부 모듈들은 Workable, Playable, Eatable을 골라서 협력하는것이 가능합니다.

결과

위 두 방법을 사용하여 인터페이스에 의존한다면 컴파일 타임에 막을 수 있습니다.

class WorkPlace {
	private List<IWork> workers;

	public void addWorker(Workable worker) {
		workers.add(worker);
	}

	public void allWork() {
		for(Workable worker : workers) {
			worker.work();
		}

		//doesn't compile
		worker.play();
	}
}

즉 ISP를 만족시킨다면 인터페이스에 대한 접근권한을 컴파일 시간에 타입으로 막을 수 있는 것입니다.

감사합니다.

Credits

모든 내용은 유튜브 코드스피츠 채널 을 참고하였습니다

댓글남기기