인터페이스 분리 원칙 - ISP (Interface Segregation Principle)
ISP 원칙이란 범용적인 인터페이스 보다는 클라이언트(사용자)가 실제로 사용하는 Interface를 만들어야 한다는 의미로, 인터페이스를 사용에 맞게 끔 각기 분리해야한다는 설계 원칙이라고 보면 된다.
만약 인터페이스의 추상 메서드들을 범용적으로 이것저것 구현한다면, 그 인터페이스를 상속받은 클래스는 자신이 사용하지 않는 인터페이스마저 억지로 구현 해야 하는 상황이 올 수도 있다.
또한 사용하지도 않는 인터페이스의 추상 메소드가 변경된다면 클래스에서도 수정이 필요하게 된다.
즉, 인터페이스 분리 원칙이란 인터페이스를 잘게 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이다.
따라서 아래 그림과 같이 Pet 인터페이스를 좀더 잘게 나눔으로써 각 애완동물 클래스의 역할과 맞게 상속 시켜줌으로서, 클래스의 기능을 쉽게 파악할 수 있다는 이점을 얻을 수 있으며 유연하게 객체의 기능을 확장하거나 수정할 수 있게 된다.
인터페이스 분리 원칙은 마치 단일 책임 원칙과 비슷하게 보이는데, SRP 원칙이 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조한다고 말할 수 있다.
다만 유의할 점은 인터페이스는 클래스와 다르게 추상화이기 때문에 여러개의 역할을 가지는데 있어 제약이 없긴 하다.
즉, SRP 원칙의 목표는 클래스 분리를 통하여 이루어진다면, ISP 원칙은 인터페이스 분리를 통하여 이루어 진다고 볼 수 있다.
또한 SRP 원칙의 클래스 책임의 범위에 대해 분리 기준이 다르듯이, 인터페이스를 분리하는 기준은 상황에 따라 다르다.
핵심은 관련 있는 기능끼리 하나의 인터페이스에 모으되 지나치게 커지지 않도록 크기를 제한하라는 점이다.
ISP 원칙 위반 예제와 수정하기
우리는 스마트폰 종류의 클래스를 구현하기 앞서 인터페이스로 스마트폰을 추상화 하였다.
스마트폰 인터페이스에는 스마트폰이라면 가지고 있을 통화나 메세지 기능 이외에도 무선 충전, AR 뷰어, 생체인식 등의 다채로운 기능을 포함하고 있다.
interface ISmartPhone {
void call(String number); // 통화 기능
void message(String number, String text); // 문제 메세지 전송 기능
void wirelessCharge(); // 무선 충전 기능
void AR(); // 증강 현실(AR) 기능
void biometrics(); // 생체 인식 기능
}
만일 갤럭시 S20이나 S21 클래스를 구현한다면, 최신 스마트폰 기종인 만큼 객체의 동작 모두가 필요하므로 ISP 원칙을 만족하게 된다.
class S20 implements ISmartPhone {
public void call(String number) {
}
public void message(String number, String text) {
}
public void wirelessCharge() {
}
public void AR() {
}
public void biometrics() {
}
}
class S21 implements ISmartPhone {
public void call(String number) {
}
public void message(String number, String text) {
}
public void wirelessCharge() {
}
public void AR() {
}
public void biometrics() {
}
}
그러나 최신 기종 스마트폰 뿐만 아니라 구형 기종 스마트폰 클래스도 다뤄야 할 경우 문제가 생긴다.
갤럭시 S3 클래스를 구현해야 한다면 무선 충전, 생체인식과 같은 기능은 포함되어 있지 않기 때문이다.
이렇게 된다면 추상 메소드 구현 규칙상 오버라이딩은 하되, 메서드 내부는 빈공간으로 두거나 혹은 예외(Exception)을 발생토록 구성해야 한다.
결국 필요하지도 않은 기능을 어쩔수없이 구현해야하는 낭비가 발생된 것이다.
class S3 implements ISmartPhone {
public void call(String number) {
}
public void message(String number, String text) {
}
public void wirelessCharge() {
System.out.println("지원 하지 않는 기능 입니다.");
}
public void AR() {
System.out.println("지원 하지 않는 기능 입니다.");
}
public void biometrics() {
System.out.println("지원 하지 않는 기능 입니다.");
}
}
따라서 각각의 기능에 맞게 인터페이스를 잘게 분리하도록 구성한다.
그리고 잘게 분리된 인터페이스를 클래스가 지원되는 기능만을 선별하여 implements 하면 ISP 원칙이 지켜지게 된다.
interface IPhone {
void call(String number); // 통화 기능
void message(String number, String text); // 문제 메세지 전송 기능
}
interface WirelessChargable {
void wirelessCharge(); // 무선 충전 기능
}
interface ARable {
void AR(); // 증강 현실(AR) 기능
}
interface Biometricsable {
void biometrics(); // 생체 인식 기능
}
class S21 implements IPhone, WirelessChargable, ARable, Biometricsable {
public void call(String number) {
}
public void message(String number, String text) {
}
public void wirelessCharge() {
}
public void AR() {
}
public void biometrics() {
}
}
class S3 implements IPhone {
public void call(String number) {
}
public void message(String number, String text) {
}
}
ISP 원칙 적용 주의점
SRP 와 ISP 원칙 사이의 관계
위에서 SRP가 클래스의 단일 책임 원칙이라면, ISP는 인터페이스의 단일 책임 원칙이라고 했다.
즉, 인터페이스에 기능에 대한 책임에 맞게 추상 메소드를 구성하면 된다는 말이다.
하지만 책임을 준수하더라도 실무에서는 ISP가 만족되지 않을 수 있는 케이스가 존재한다.
예를들어 위와 같이 게시판 인터페이스엔 글쓰기, 읽기, 삭제 추상 메서드가 정의되어 있다. 이들은 모두 게시판에 필요한 기능들이며 게시판만을 이용하는 단일 책임에 위배되지 않는다.
하지만 이를 구현하는 일반 사용자 입장에선 게시글 강제 삭제 기능은 사용할 수 없기 때문에 결국 ISP 위반으로 이어진다.
따라서 책임을 잘 구성해 놓은 것 같지만 실제 적용되는 객체에겐 부합되지 않을 수 있기 때문에 책임을 더 분리해야 한다.
정리하자면, ISP는 SRP를 만족하면 성립되는가 라고 질문한다면 반드시 그렇다고는 볼 수 없다고 답변하는게 맞을지도 모른다.
인터페이스 분리는 한번만
ISP 원칙의 주의해야 할점은 한번 인터페이스를 분리하여 구성해놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스들을 분리하는 행위를 가하지 말라는 점이다.
이미 구현되어 있는 프로젝트에 또 인터페이스들을 분리한다면, 이미 해당 인터페이스를 구현하고 있는 온갖 클래스들과 이를 사용하고 있는 클라이언트(사용자)에서 문제가 일어날 수 있기 때문이다.
본래 인터페이스라는 건 한번 구성하였으면 왠만해선 변하면 안되는 정책같은 개념이다.
따라서 처음 설계부터 기능의 변화를 생각해두고 인터페이스를 설계해야 하는데, 이는 현실적으로 참 힘든 부분이며 역시 개발자의 역량에 달렸다.
참고