본문 바로가기
설계/객체지향설계

[객체지향설계] SOLID 설계 원칙 (4) - ISP (인터페이스 분리 원칙)

by J4J 2024. 3. 12.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 solid 설계 원칙 네 번째인 isp (인터페이스 분리 원칙)에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 글

 

[객체지향설계] SOLID 설계 원칙 (1) - SRP (단일 책임 원칙)

[객체지향설계] SOLID 설계 원칙 (2) - OCP (개방 폐쇄 원칙)

[객체지향설계] SOLID 설계 원칙 (3) - LSP (리스코프 치환 원칙)

 

 

반응형

 

 

ISP (인터페이스 분리 원칙) 란?

 

solid 설계 원칙에서 isp가 의미하는 것은 클라이언트는 자신의 목적에 맞는 메서드에만 의존해야 된다는 것을 말합니다.

 

즉, 자바 관점에서 얘기를 해보면 인터페이스를 활용하여 클래스 내부 메서드를 정의할 때 실제로 클래스 내부에서 사용되는 메서드만 담긴 인터페이스를 활용되어야 한다는 것입니다.

 

 

 

 

isp가 활용되는 경우에 대해 이어폰을 이용하여 간단히 예시를 들어보겠습니다.

 

최근에는 이어폰을 구매한다고 하면 보통 블루투스 이어폰을 구매하실 겁니다.

 

하지만 블루투스 이어폰이 등장하기 이전에는 유선 이어폰을 활용했습니다.

 

유선 이어폰과 블루투스 이어폰의 기능을 모두 구현해야 된다고 가정했을 때 공통 기능에 대해서만 도출해 보면 다음과 같이 작성해 볼 수 있습니다.

 

  유선 이어폰 블루투스 이어폰
연결 방법 선을 이용하여 연결 블루투스를 이용하여 연결
소리 듣는 방법 이어폰 몸체를 귀에 고정 이어폰 몸체를 귀에 고정

 

 

 

위의 상황을 기준으로 인터페이스를 활용하여 코드를 작성한다고 할 때 1개의 인터페이스에 각 기능들에 대한 메서드를 정의해볼 수 있습니다.

 

그리고 인터페이스를 유선 이어폰과 블루투스 이어폰 구현체 클래스에 상속하여 각 클래스 별로 메서드를 작성하게 됩니다.

 

 

 

 

하지만 일반적으로 유선 이어폰보다는 블루투스 이어폰에 더 많은 기능들이 담겨있습니다.

 

위의 예제에서 블루투스 이어폰의 기능들을 더 추가한다면 다음과 같이 작성해볼 수 있습니다.

 

  유선 이어폰 블루투스 이어폰
연결 방법 선을 이용하여 연결 블루투스를 이용하여 연결
소리 듣는 방법 이어폰 몸체를 귀에 고정 이어폰 몸체를 귀에 고정
충전 방법 - 충전 케이스에 연결
볼륨 높이는 방법 - 오른쪽 이어폰 몸체 터치
볼륨 낮추는 방법 - 왼쪽 이어폰 몸체 터치

 

 

 

해당 경우도 인터페이스를 활용하여 코드를 작성한다고 하면 쉽게 실수할 수 있는 부분은 이어폰 기능을 1개의 인터페이스에 모두 담는 것입니다.

 

한 곳에서 모든 기능들을 확인해볼 수 있지만 유선 이어폰 입장에서는 제공해주지 않는 기능들에 대해 정의를 할 수 없습니다.

 

결국 유선 이어폰 객체를 활용하는 곳에서 제공해주지 않는 메서드를 호출할 수 있는 상황이 발생합니다.

 

 

 

 

이런 상황에서 필요한 것이 isp를 이용한 설계 방식입니다.

 

isp를 이용한 설계를 한다면 이어폰의 공통 기능에 대한 인터페이스는 이어폰이 모두 가지고 있는 메서드만 정의를 합니다.

 

그리고 블루투스 이어폰만이 가지고 있는 기능에 대해서는 공통 기능을 정의하는 인터페이스에서 분리하여 블루투스 기능만을 담아두는 인터페이스를 작성하게 됩니다.

 

그러면 유선 이어폰에는 블루투스 이어폰만이 가지고 있는 기능에 대해서 정의해줄 필요가 없어지게 되므로 각 객체의 목적에 맞는 메서드만이 작성되는 결과를 만들게 도와줍니다.

 

 

 

 

ISP (인터페이스 분리 원칙) 특징

 

그러면 isp를 이용하여 코드를 설계할 때 얻을 수 있는 이점은 무엇인지에 대해 궁금하게 됩니다.

 

isp의 이점은 다음과 같이 얘기해볼 수 있습니다.

 

  • 객체 간의 결합도를 낮춤
  • 유연한 개발이 가능하도록 도와줌
  • 관심사의 명확한 분리

 

 

 

 

위의 예제에 대해 다시 한번 얘기해보겠습니다.

 

만약 유선 이어폰 / 블루투스 이어폰처럼 2개의 경우에 대해서만 판단해 보면 isp를 적용한 설계 방식의 이점에 대해 많이 느껴지지 않을 수 있습니다.

 

그래서 1개의 인터페이스만을 활용하여 기능들을 정의한 뒤 구현체를 작성했다고 가정해 보겠습니다.

 

 

 

하지만 나중에 유선 이어폰 / 블루투스 이어폰 외에 새로운 기술들을 활용한 다양한 방식의 이어폰이 더 많이 등장할 수 있습니다.

 

그러면 유선 이어폰 / 블루투스 이어폰 모두에서 제공해주지 않던 기능들도 새롭게 등장할 수 있고 이 또한 1개의 인터페이스만을 활용하여 관리를 한다면 사용되지 않는 메서드를 정의해야 되는 상황이 더 많이 발생하게 됩니다.

 

 

 

이런 상황에서 isp를 이용한 설계가 제대로 이루어졌다면 새로운 이어폰이 등장할 때 새로운 이어폰의 기능만을 명확히 담은 인터페이스만 정의하면 됩니다.

 

다른 말로는 기존에 사용되고 있던 유선 이어폰 / 블루투스 이어폰 등과 관련된 기능 변경은 발생될 필요가 없기 때문에 위에서 말한 isp의 이점을 명확히 얻을 수 있습니다.

 

 

 

 

ISP (인터페이스 분리 원칙) 적용

 

isp의 적용은 결국 oop의 특징 중 하나인 상속 개념을 활용하게 됩니다.

 

또한 상속을 사용하면서 각 기능들을 클라이언트의 역할에 맞게 인터페이스를 모두 분리해주시기만 하면 됩니다.

 

 

 

 

하지만 isp를 활용할 때 무분별한 인터페이스 분리에 주의해야 합니다.

 

인터페이스를 분리하게 되면 관심사를 명확히 확인할 수 있기 때문에 관리 측면에서 이로울 수 있습니다.

 

그러나 인터페이스를 과도하게 분리한다면 오히려 구조를 더 복잡하게 만드는 원인이 됩니다.

 

또한 인터페이스에 변경 사항이 발생될 때 영향받는 객체들이 많아지기 때문에 단순 변경에 대해 확인해야 하는 객체들이 증가하게 됩니다.

 

그래서 설계를 할 때 각 기능들을 과하지 않고 필요에 맞는 부분에 대해서만 명확히 분리를 해준다면 좋은 개발자 경험을 느끼실 수 있습니다.

 

 

 

 

예제 코드 (1) - Bad Case

 

위에서 말한 예시를 코드화시켜보겠습니다.

 

먼저 bad case에 대해서 작성하면 다음과 같이 구성해 볼 수 있습니다.

 

// main
package com.jforj.solidisp.badcase;

public class Main {
    public static void main(String[] args) {
        // 유선 이어폰
        WiredEarPhone wiredEarPhone = new WiredEarPhone();
        wiredEarPhone.connection();
        wiredEarPhone.listenSound();
        // wiredEarPhone.charge();
        // wiredEarPhone.turnUpSound();
        // wiredEarPhone.turnDownSound();

        // 블루투스 이어폰
        BluetoothEarPhone bluetoothEarPhone = new BluetoothEarPhone();
        bluetoothEarPhone.connection();
        bluetoothEarPhone.listenSound();
        bluetoothEarPhone.charge();
        bluetoothEarPhone.turnUpSound();
        bluetoothEarPhone.turnDownSound();
    }
}


// earphone interface
package com.jforj.solidisp.badcase;

public interface EarPhone {

    void connection();

    void listenSound();

    void charge();

    void turnUpSound();

    void turnDownSound();
}


// wired earphone
package com.jforj.solidisp.badcase;

public class WiredEarPhone implements EarPhone {

    @Override
    public void connection() {
        System.out.println("선을 이용하여 연결");
    }

    @Override
    public void listenSound() {
        System.out.println("이어폰 몸체를 귀에 고정");
    }

    @Override
    public void charge() {
        throw new RuntimeException("충전 기능을 제공하지 않습니다.");
    }

    @Override
    public void turnUpSound() {
        throw new RuntimeException("볼륨 높이는 기능을 제공하지 않습니다.");
    }

    @Override
    public void turnDownSound() {
        throw new RuntimeException("볼륨 낮추는 기능을 제공하지 않습니다.");
    }
}


// bluetooth earphone
package com.jforj.solidisp.badcase;

public class BluetoothEarPhone implements EarPhone {

    @Override
    public void connection() {
        System.out.println("블루투스를 이용하여 연결");
    }

    @Override
    public void listenSound() {
        System.out.println("이어폰 몸체를 귀에 고정");
    }

    @Override
    public void charge() {
        System.out.println("충전 케이스에 연결");
    }

    @Override
    public void turnUpSound() {
        System.out.println("오른쪽 이어폰 몸체 터치");
    }

    @Override
    public void turnDownSound() {
        System.out.println("왼쪽 이어폰 몸체 터치");
    }
}

 

bad case 결과

 

 

 

 

작성된 코드를 보면 유선 이어폰에서는 제공해주지 않는 기능들에 대해서도 정의를 해줘야 하는 상황이 발생하는 것을 볼 수 있습니다.

 

이런 경우 구현체를 활용하는 곳에서 제공하지 않는 메서드를 호출하는 상황을 만들 수 있습니다.

 

그리고 애초에 필요 없는 기능들에 대해서 호출되는 것은 올바르지 않은 상황으로도 생각할 수 있습니다.

 

또한 객체에서 사용되는 기능들만 정의한다는 isp의 설계 기준에도 부합하지 않게 됩니다.

 

 

 

 

예제 코드 (2) - Best Case

 

이번에는 위의 bad case에 isp를 이용한 설계를 진행했을 때 어떻게 코드가 변경되는지 확인해 보겠습니다.

 

// main
package com.jforj.solidisp.bestcase;

public class Main {
    public static void main(String[] args) {
        // 유선 이어폰
        WiredEarPhone wiredEarPhone = new WiredEarPhone();
        wiredEarPhone.connection();
        wiredEarPhone.listenSound();

        // 블루투스 이어폰
        BluetoothEarPhone bluetoothEarPhone = new BluetoothEarPhone();
        bluetoothEarPhone.connection();
        bluetoothEarPhone.listenSound();
        bluetoothEarPhone.charge();
        bluetoothEarPhone.turnUpSound();
        bluetoothEarPhone.turnDownSound();
    }
}


// earphone interface
package com.jforj.solidisp.bestcase;

public interface EarPhone {

    void connection();

    void listenSound();
}


// enable bluetooth interface
package com.jforj.solidisp.bestcase;

public interface EnableBluetooth {

    void charge();

    void turnUpSound();

    void turnDownSound();
}


// wired earphone
package com.jforj.solidisp.bestcase;

public class WiredEarPhone implements EarPhone {

    @Override
    public void connection() {
        System.out.println("선을 이용하여 연결");
    }

    @Override
    public void listenSound() {
        System.out.println("이어폰 몸체를 귀에 고정");
    }
}


// bluetooth earphone
package com.jforj.solidisp.bestcase;

public class BluetoothEarPhone implements EarPhone, EnableBluetooth {

    @Override
    public void connection() {
        System.out.println("블루투스를 이용하여 연결");
    }

    @Override
    public void listenSound() {
        System.out.println("이어폰 몸체를 귀에 고정");
    }

    @Override
    public void charge() {
        System.out.println("충전 케이스에 연결");
    }

    @Override
    public void turnUpSound() {
        System.out.println("오른쪽 이어폰 몸체 터치");
    }

    @Override
    public void turnDownSound() {
        System.out.println("왼쪽 이어폰 몸체 터치");
    }
}

 

best case 결과

 

 

 

 

코드를 살펴보면 인터페이스를 각 역할에 맞게 명확히 분리를 했기 때문에 유선 이어폰에서 필요 없는 메서드에 대한 정의를 하는 과정이 없어진 것을 볼 수 있습니다.

 

그에 대한 결과로 유선 이어폰에서는 더 이상 사용하지 않는 기능에 대한 호출을 할 수 없는 것도 볼 수 있습니다.

 

즉, 클라이언트가 가지고 있어야 하는 메서드에 대해서만 의존하는 관계를 만들었고 isp가 지켜지고 있는 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

 

 

이상으로 solid 설계 원칙 네 번째인 isp (인터페이스 분리 원칙)에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글