#3 LSP, ISP

cho·2022년 8월 26일
0

clean-architecture

목록 보기
2/2
post-thumbnail

LSP: 리스코프 치환 원칙

바바라 리스코프가 정의한 하위 타입

  • S 타입의 객체 o1에 대응하는 T타입 객체 o2가 있다. → o1: S, o2: T
    -
    T 타입을 이용해 정의한 모든 프로그램 P에서 o2 → o1 치환하더라도 P의 행위가 변하지 않는다면,
    S는 T의 하위 타입이다.

✅ 예시를 통해 알아보는 리스코프 치환 원칙


1) 상속을 사용하도록 가이드하기 - LSP 준수 설계

  • Billing 애플리케이션의 행위가 License (인터페이스) 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않음
  • 하위 타입은 모두 License 타입을 치환 할 수 있음

LSP 준수 설계

2) 정사각형/직사각형 문제 - LSP 위반!

  • Square는 Rectangle의 하위 타입으로 적합하지 않음
    • Rectangle의 높이와 너비는 독립적으로 변경될 수 있지만,
      Square의 높이와 너비는 반드시 함께 변경되기 때문임
  • 따라서 User는 사용과정에서 혼동이 생길 수 있음
  • 해결 방법: 조건문 등을 이용해 Rectangle이 실제로는 Square인지 검사
    • 하지만 이렇게 되면 결국 User의 행위가 사용하는 타입에 의존하게 되므로,
      서로 타입을 치환할 수 없게 됨

LSP 위반 설계

public class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

		public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}
Rectangle square = new Square();
square.setWidth(5);
square.setHeight(2);
assert(square.getArea() == 10);
  • 자식 클래스가 부모 클래스를 대신하였을 때 의도에 맞게 동작하지 않음 → 리스코프 치환 원칙 위반

직사각형, 정사각형 상속 관계를 끊고 상속관계 재정의

getArea 메서드를 자식 클래스로 옮기는 방법 등이 필요

✅ LSP와 아키텍처


  • LSP는 인터페이스와 구현체에도 적용됨
  • 아키텍처 관점에서 LSP를 이해하는 최선의 방법은,
    이 원칙을 어겼을 때 시스템 아키텍처에서 무슨 일이 일어나는지 관찰하는 것

✅ LSP 위배 사례 - 아키텍처 관점의 LSP


상황

  • 택시 파견 서비스
    • 기사 선택 → 해당 기사의 URI 정보를 얻은 후 → 그 URI를 이용해 해당 기사를 고객 위치로 파견
    • URI에 파견에 필요한 정보를 덧붙인 후, PUT 방식으로 호출
      purplecab.com/driver/Bob
      					/pickupAddress/24 Maple St.
      					/pickupTime/153
      					/destination/ORD

이 예제에서 말하고자 하는 것

  • 다양한 택시업체에서 파견 서비스를 만들 때, 동일한 REST 인터페이스를 반드시 준수해야함
  • 일부 업체에서 다르게 처리한다면, if문 등의 방법으로 해결해야함. 이런 예외 상황은 끊임없이 발생
  • 아키텍트는 이러한 버그로부터 시스템을 보호 해야함
    • 파견 URI를 키로 사용해 파견 요청 포맷을 관리
    • 각 REST 서비스들의 인터페이스가 서로 치환 가능하지 않다는 사실을 처리하는 복잡한 메커니즘을 추가해야 함
URIDispatch Format
Acme.com/pickupAddress/%s/pickupTime/%s/dest/%s
./pickupAddress/%s/pickupTime/%s/destination/%s

✅ 결론


  • 치환 가능성을 조금이라도 위배할 경우 시스템 아키텍처가 오염되어, 상당량의 별도 메커니즘을 추가해야할 수도 있음
  • 따라서 LSP는 아키텍처 수준까지 확장할 수 있고, 위와 같은 문제를 막으려면 반드시 확장해야 함


ISP: 인터페이스 분리 원칙

  • OPS: 정적 타입 언어로 작성된 클래스
  • User1: op1, User2: op2, User3: op3
  • User1은 op2, op3를 전혀 사용하지 않음에도 User1은 op2, op3에 의존하게 됨 → op2, op3의 코드가 변경되면 User1도 재컴파일 후 배포 ⭕️

⇒ 인터페이스 분리 원칙 위반

✅ 해결 방법 - 인터페이스 단위로 분리


  • OPS를 상속받는 U1Ops, U2Ops, U3Ops를 인터페이스 단위로 분리
  • User1은 U1Ops, op1에는 의존하지만 OPS에는 의존하지 않게 됨 → OPS의 변경이 User1과 관계없다면, 재컴파일/배포 ❌

인터페이스 분리 원칙 준수

✅ 인터페이스 분리 원칙 위반 예제


public interface MultifunctionService {
  void copy();
  void fax(Address from, Address to);
  void print();
}
public class CopyMachine implements MultifunctionService {
  @Override
  public void copy() {
    System.out.println("### 복사 ###");
  }

  @Override
  public void fax(Address from, Address to) {
    // 사용하지 않는 인터페이스가 변경되어도 함께 수정이 일어난다.
  }

  @Override
  public print() {
    // 사용하지 않는 인터페이스가 변경되어도 함께 수정이 일어난다.
  }
}
  • 만약 multinfunction 인터페이스에서 fax()나 print()에 대해서 리턴 타입이 변경된다면, 이와 전혀 상관없는 CopyMachine 클래스도 같이 수정해줘야 하는 문제가 발생한다.

✅ 해결방법

public interface Print{
  void print();
}

public interface Copy {
  void copy();
}

public interface Fax {
  void fax(Address from, Address to);
}
public class copyMachine implements Copy {
  @Override
  void copy() {
    System.out.println("### 복사 ###");
  }
}
  • Print, Copy, Fax 인터페이스로 분리

    copyMachine 과 관계없다면 print(), fax()가 변경되어도 재컴파일/배포하지 않아도 됨

    인터페이스 분리 원칙 준수

    ✅ ISP와 언어


  • ISP는 아키텍처가 아니라, 언어와 관련된 문제라고 결론내릴 여지가 있음
    • 정적 타입 언어import, include 선언문으로 인해 코드 의존성 발생,
      재컴파일/재배포가 강제되는 상황 초래
    • 동적 타입 언어는 코드 의존성 ❌ , 재컴파일/재배포가 필요하지 않음
      a. 참고할 사항
      • 자바는 정적 타입 언어이지만 재컴파일/재배포 하지 않는 경우가 있음. 자바는 비-final, 비-private, 인스턴스 변수에 대해서는 동적 바인딩을 수행하기 때문

호출할 정확한 메서드를 런타임에 결정하는 것
(정적 바인딩 → 컴파일링시에 결정)

⇒ 따라서 ISP는 언어 종류에 따라 영향받는 정도가 다름

✅ ISP와 아키텍처


  • 시스템 S → F 프레임워크 사용 / F 프레임워크 → 데이터베이스 D를 사용
  • S → F → D 의존 관계
  • S와 전혀 관계없는 기능이 D에 포함된 경우
    • D 변경 시 → S, F 재배포해야할 수도 있는 문제
    • D 내부 기능 중 문제가 발생했을 때 → S, F에도 영향을 미치는 문제

✅ 결론


  • 클라이언트는 사용하지 않는 메소드에 의존하지 않아야함 → 불필요한 의존성 제거

0개의 댓글