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

드디어 어려운 원칙이 지나갔다.

이제는 행복해질 시간이다. 이 원칙은 상대적으로 쉽고, 단일 책임 원칙(SRP)을 알고 있다면, 더 이해하기 쉬운 원칙이다.

 

이 원칙의 사전적 정의는 '클라이언트는 자신이 사용하지 않는 인터페이스 멤버에 대하여 의존적이면 안된다'이다.

 

클라이언트라고 하니 딱딱하다. 흔히 이 정의는 이렇게로도 많이 알려져 있다.

'하나 인터페이스가 하나의 동작만을 하도록 인터페이스가 분리되어야 한다.'

 

이유

우리가 좋아하는 예를 들어보자.

이 예시에서 사용하지 않는 인터페이스 멤버에 대해 의존성을 가진다는 게 어떤 건지, 이로 인한 문제가 뭔지 살펴볼 것이다.

public interface IDataManager
{
    void Load();
    
    void Prepare();
    
    void Save();
}

public class DataLoader : IDataManager
{
    public void Load()
    {
    }

    public void Prepare()
    {
    }
    
    public void Save()
    {
    }
}

DataLoader클래스는 인터페이스로 IDataManager를 상속하고 있다.

그런데 DataLoader는 Load 함수만 필요한 클래스인데, 원치않게 Prepare와 Save까지 구현을 해야 되는 것을 볼 수 있다.

 

만약 설상가상으로, Save의 시그니처(파라미터, 리턴, 함수명에 대한 정의)가 변경되어, 리턴 값으로 bool을 갖게 된다면?

DataLoader도 똑같이 수정을 해야 할 것이다.

 

이게 바로 불필요한 인터페이스 멤버에 대한 의존성이다.

이런 문제를 해결하기 위해서 우리는 인터페이스를 잘게 쪼갤 필요가 있다.

 

그리고 이 원칙을 따라야 하는 또 다른 이유가 하나가 더 있는데,

인터페이스를 잘게 쪼게면. 인터페이스를 사용하는 개발자가 잘못된 방법으로 인터페이스를 사용하도록 예방하고,

필요한 동작만을 수행할 수 있게 유도할 수 있다는 것이다.

 

위의 코드를 예를 들어, IDataManager 파라미터를 이용한 함수를 구현하는데,

IDataManagerDataLoader가 전달되는 상황이라고 가정해보자.

public class AnotherClass
{
    public void LoadData(IDataManager dataLoader)
    {
        dataLoader.Prepare();
        dataLoader.Load();
    }
}

 

구현된 함수를 보면, 이 함수를 개발한 개발자가 IDataManager의 인터페이스만 보고, Prepare를 사전에 호출해야 하는 함수로 착각해,

호출한 것을 알 수 있다. 인터페이스를 잘게 쪼갠다면, 이런 문제를 사전에 방지할 수 있다.

 

인터페이스 분리하기

원칙을 적용하는 법은 간단하다. 하나의 인터페이스를 여러 개의 인터페이스로 분리하면 된다.

그런데 이 원칙은 단일 책임 원칙 마냥 적절히 쪼개는 것에 그치지 않고, 아예, 인터페이스 하나 당, 하나의 멤버만을 갖도록 정의하고 있다.

public interface IDataLoader
{
    void Load();
}

public interface IDataPreparer
{
    void Prepare();
}

public interface IDataSaver
{
    void Save();
}

public class DataLoader : IDataLoader
{
    public void Load()
    {
    }
}

public class DataPreparer : IDataPreparer
{
    public void Prepare()
    {
    }
}

public class DataSaver : IDataSaver
{
    public void Save()
    {
    }
}

그래서 이렇게, 각자 쪼갠 인터페이스 별로 구현을 하거나,

public interface IDataLoader
{
    void Load();
}

public interface IDataPreparer
{
    void Prepare();
}

public interface IDataSaver
{
    void Save();
}

public class DataManager : IDataLoader, IDataPreparer, IDataSaver
{
    public void Load()
    {
    }
    
    public void Prepare()
    {
    }
    
    public void Save()
    {
    }
}

혹은 이렇게 다중 상속을 이용해서 하나의 클래스에 구현할 수 있다.

 

public class AnotherClass
{
    public void LoadData(IDataLoader dataLoader)
    {
        dataLoader.Load();
    }
}

그래서 인터페이스를 잘 분리하면, 다른 클래스에서 인터페이스를 사용할 때, 다른 개발자가 쓸데없는 멤버를 사용하는 것을 막을 수 있다.

 

다만, 이 원칙에서 주의할 점은 분리한 인터페이스를 다시 하나의 인터페이스로 병합하는 경우인데,

public interface IDataLoader
{
    void Load();
}

public interface IDataPreparer
{
    void Prepare();
}

public interface IDataSaver
{
    void Save();
}

public interface IDataManager : IDataLoader, IDataPreparer, IDataSaver
{
}

이렇게 기껏 분리한 인터페이스를 다시 병합하지 않게 조심해야 한다.

(이런 경우를 '인터페이스 수프(Soup)'라고 부른다.)

 

단일 책임 원칙과의 관계

몇몇 구글에 퍼진 얘기로 인터페이스 분리 원칙과 단일 책임 원칙은 같은 문제에 대한 두 가지의 다른 해결책이다라는 말이 있는데,

내가 볼 때 이건 잘못된 얘기다. 누군가 잘못 이해하고 생산한 걸 퍼 나르고 퍼 나르다 보니 사실인 것처럼 퍼져 있는 거 같은데,

 

단일 책임 원칙은 클래스와 메서드가 하나의 책임만을 갖도록 하는 원칙이고,

인터페이스 분리 원칙은 인터페이스가 하나의 멤버만을 갖도록 분리하는 원칙이다.

 

걍 쉽게 예를 들어, 위에 있는 예시 코드를 보면 답이 나온다.

public interface IDataLoader
{
    void Load();
}

public interface IDataPreparer
{
    void Prepare();
}

public interface IDataSaver
{
    void Save();
}

public class DataManager : IDataLoader, IDataPreparer, IDataSaver
{
    public void Load()
    {
    }
    
    public void Prepare()
    {
    }
    
    public void Save()
    {
    }
}

이 예시는 인터페이스들이 하나의 목적만을 정의하고, DataManager는 Data 관리에 대한 단일 책임을 가지고 있으므로,

단일 책임 원칙과, 인터페이스 분리 원칙 둘 다 적용된 사례이다.

 

그리고 상식적으로 생각해봐도 '객체지향 설계의 5대 원칙'인데,

우리가 흔히 #대 원칙을 말할 때, 'A 원칙과 B 원칙은 양립할 수 없다.'라고 얘기하진 않는다.

 

생각해볼 점

이 원칙에 가장 걸리는 부분은 아무래도,

인터페이스 하나 당, 하나의 멤버라는 점일 것이다.

 

상식적으로 인터페이스에 하나의 멤버만 넣는 것은 불가능이다.

당장 자기가 만든 프로젝트만 생각해도, 그렇게 적용했다간 프로젝트 내에 인터페이스 파일이 수백 개가 될 것이고,

인터페이스 관리에 헬게이트가 열릴 것이 분명하다.

 

다른 사람들도 이 부분에 같은 생각인지, 잘게 쪼개는 건 동의하지만, 적정선까지만 쪼개는 게 더 나을 것이라고 얘기하고 있다.

 

이 글을 읽는 사람도 이 부분에 대해선 어떻게 실전에 적용하는 게 좋을지 생각해볼 필요가 있다.

'Refactoring & Clean Code' 카테고리의 다른 글

SOLID : 의존 역전 원칙  (0) 2020.04.14
SOLID : 리스코프 치환 원칙  (0) 2020.04.12
SOLID : 개방 폐쇄 원칙  (0) 2020.04.10
SOLID : 단일 책임 원칙  (0) 2020.04.10
리팩토링에 대한 개인적인 생각  (0) 2020.04.10

+ Recent posts