Backend/꾸준히 TIL

[클린아키텍쳐] 설계원칙 SOLID 3

개발하는 후딘 2023. 11. 7. 23:59
728x90
반응형

학습차원으로 "클린아키텍쳐" 도서를 읽고 정리한 글입니다.


ISP: 인터페이스 분리법칙

(문제사항)

다수의 사용자가 OPS클래스의 오퍼레이션을 사용한다.

User1은 오직 op1을, User2는 op2만을, User3는 op3만을 사용한다고 가정한다.

 

OPS 가 정적타입언어로 작성된 클래스라고 해보자

이경우 User1에서는 op2와 op3를 전혀 사용하지 않음에도 User1의 소스코드는 이 두 메소드에 의존하게된다.

 

이러한 의존성에 의해 OPS클래스에서 op2의 소스코드가 변경되면

User1도 다시 컴파일한 후 새로 배포해야한다. 사실 User1과 관련된 코드는 전혀 변경되지 않았음에도 말이다.

이러한 문제는 오퍼레이션을 인터페이스 단위로 분리하여 해결할 수 있다.

 

 

분리된 오퍼레이션

User1의 소스코드는 U1Ops와 op1에는 의존하지만, OPS에는 의존하지 않게된다.

따라서 OPS에서 발생한 변경이 User1과는 전혀 관계없는 변경이라면, User1은 다시 컴파일하고 새로 배포하는 상황은 초래되지 않는다.

 

 

ISP와 언어

  • 정적타입언어
    • 정적 타입 언어는 사용자가 import, use, include 와 같은 타입 선언문을 사용하도록 강제한다.
    • 이처럼 소스코드에 포함된 선언문으로 인해 소스코드 의존성이 발생하고, 이로인해 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.

 

 

  • 동적타입언어
    • 동적타입언어에서는 소스코드에 선언문이 존재하지 않는 대신에, 런타임 추론이 발생한다.
    • 소스코드의 의존성이 아예 없으며, 결국 재컴파일과 재배포가 필요없다.
    • 동적타입 언어를 사용하면 정적타입언어를 사용할 때보다 유연하며, 결합도가 낮은 시스템을 만들 수 있는 이유는 이 때문이다.
    • ISP를 아키텍쳐가 아니라, 언어와 관련된 문제라고 결론 내릴 여지가 있다.

 


 

ISP와 아키텍쳐

  • 필요이상으로 많은걸 포함하는 모듈에 의존하는 것은 해로운 일이다.
  • 소스코드 의존성의 경우 이는 분명한 사실인데, 불필요한 재컴파일과 재배포를 강제하기 때문이다.
  • 하지만 더 고수준인 아키텍쳐 수준에서도 마찬가지 상황이 발생한다.

 

시스템 S → 프레임워크 F → 데이터베이스 D (S는 F에 의존, F는 D에 의존)

프레임워크 F에서 불필요한 기능, 따라서 시스템S와는 전혀관계 없는 기능이 D에 포함된다고 가정하자.

그 기능때문에 D내부가 변경되면, F를 재배포해야 할 수 도 있고, 따라서 S까지 재배포해야 할지 모른다.

더 심각한 문제는 D내부의 기능중 F와 S에서 불필요한 그 기능에 문제가 발생해도 F와 S에 영향을 준다는 사실이다.

불피요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실이다.


DIP: 의존성 역전 법칙

  • 유연성이 극대화된 시스템이란 소스코드 의존성이 추상(abstraction)에 의존하며, 구체(concretion)에는 의존하지 않는 시스템이다.
  • 자바와 같은 정적타입언어에서 이말은 use, import, include 구문은 오직 인터페이스나 추상클래스와 같은 추상적인 선언만을 참조해야 한다는 뜻이다. 구체적인 대상에는 절대로 의존해서는 안된다.
  • 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존하기 때문이다.

 

안정된 추상화

  • 추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라서 수정해야한다.

 

  • 반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 항상, 좀 더 정확히 말하면 대다수의 경우 변경될 필요가 없다. 따라서 인터페이스는 구현체보다 변동성이 낮다.

 

  • 안정된 소프트웨어 아키텍쳐변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍쳐라는 뜻이다.

 

 

[규칙1] 변동성이 큰 구체 클래스를 참조하지 말라, 대신 추상인터페이스를 참조하라.

  • 언어가 정적타입이든 동적타입이든 관계없이 모두 적용된다.
  • 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상팩토리를 사용하도록 강제한다.

 

 

[규칙2] 변동성이 큰 구체 클래스로부터 파생하지 말라.

  • 의존성 관계를 갖는 상속관계는 강력한 관계이고 변경하기 어렵다. 상속을 신중하게 사용해야한다.
    • 정적타입언어 에서 상속은 소스코드에 존재하는 모든 관계중에서 가장 강력한 동시에 변경하기 어려우므로, 상속을 아주 신중하게 사용해야 한다.
    • 동적 타입 언어라면 문제가 덜 되지만 의존성을 가진다는 사실에는 변함이 없다.

 

 

[규칙3] 구체함수를 오버라이드 하지 말라.

  • 구체함수는 소스코드 의존성을 필요로 한다. 따라서 구체함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게되며, 실제로는 그 의존성을 상속하게 된다.
  • 이러한 의존성을 제거하려면, 차라리 추상함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야한다.

 

 

[규칙4] 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 마라.

 

 

팩토리

  • 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야한다. 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스코드 의존성이 발생하기 때문이다.
  • 객체지향언어에서 바람직하지 못한 의존성을 처리할 때 추상팩토리를 사용하곤 한다.

 

의존성을 관리하기 위해 추상팩토리(Abstract Factory) 패턴을 사용한다.

 

Application은 Service인터페이스를 통해 ConcreteImpl을 사용하지만 Application에서는 어떤식으로든 ConcreteImpl의 인스턴스를 생성해야한다.

 

ConcreteImpl에 대해 소스코드 의존성을 만들지 않으면서 이 목적을 이루기 위해 Application은 ServiceFactory 인터페이스의 makeSvc 메소드를 호출한다.

 

makeSvc 이 메서드는 ServiceFactory로부터 파생된 ServiceFactoryImpl에서 구현된다.

그리고 ServiceFactoryImpl 구현체가 ConcreteImpl의 인스턴스를 생성후 Service 타입으로 반환한다.

 

 

  • 곡선은 아키텍쳐 경계를 뜻한다. 이 곡선은 구체적인 것들로부터 추상적인 것들을 분리한다.
  • 소스코드 의존성은 해당 곡선과 교차할 때 모두 한 방향, 즉 추상적인 쪽을 향한다.

 

  • 곡선은 시스템을 두가지 컴포넌트로 분리한다. 하나는 추상컴포넌트이며, 다른 하나는 구체 컴포넌트이다.
    • 추상컴포넌트는 애플리케이션의 모든 고수준 업무규칙을 포함한다.
    • 구체컴포넌트는 업무규칙을 다루기 위해 필요한 모든 세부사항을 포함한다.

 

  • 제어흐름은 소스코드 의존성과는 정반대 방향으로 곡선을 가로지른다는 점에 주목하자. 다시말해 소스코드 의존성은 제어흐름과는 반대방향으로 역전된다.

 

 


구체 컴포넌트

  • ServiceFactoryImpl 구체클래스가 ConcreteImpl 구체클래스에 의존하여 구체컴포넌트에는 구체적인 의존성이 하나 있고, 따라서 DIP에 위배된다.
  • DIP 위배를 모두 없앨 수는 없다. 하지만 DIP를 위배하는 클래스들은 적은 수의 구체컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과는 분리할 수 있다.
  • 대다수의 시스템은 이러한 구체 컴포넌트를 최소한 하나는 포함할 것이다. 흔히 이 컴포넌트를 Main(메인)이라 부르는데, main 함수를 포함하기 때문이다.

main함수는 ServiceFactoryImpl의 인스턴스를 생성한 후, 이 인스턴스를 ServiceFactory 타입으로 전역변수에 저장할 것이다.

그런 다음 Application은 이 전역변수를 이용해서 ServiceFactoryImpl의 인스턴스에 접근할 것이다.

728x90
반응형