[클린아키텍쳐] 설계원칙 SOLID 3
학습차원으로 "클린아키텍쳐" 도서를 읽고 정리한 글입니다.
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와 아키텍쳐
- 필요이상으로 많은걸 포함하는 모듈에 의존하는 것은 해로운 일이다.
- 소스코드 의존성의 경우 이는 분명한 사실인데, 불필요한 재컴파일과 재배포를 강제하기 때문이다.
- 하지만 더 고수준인 아키텍쳐 수준에서도 마찬가지 상황이 발생한다.
프레임워크 F에서 불필요한 기능, 따라서 시스템S와는 전혀관계 없는 기능이 D에 포함된다고 가정하자.
그 기능때문에 D내부가 변경되면, F를 재배포해야 할 수 도 있고, 따라서 S까지 재배포해야 할지 모른다.
더 심각한 문제는 D내부의 기능중 F와 S에서 불필요한 그 기능에 문제가 발생해도 F와 S에 영향을 준다는 사실이다.
불피요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실이다.
DIP: 의존성 역전 법칙
- 유연성이 극대화된 시스템이란 소스코드 의존성이 추상(abstraction)에 의존하며, 구체(concretion)에는 의존하지 않는 시스템이다.
- 자바와 같은 정적타입언어에서 이말은 use, import, include 구문은 오직 인터페이스나 추상클래스와 같은 추상적인 선언만을 참조해야 한다는 뜻이다. 구체적인 대상에는 절대로 의존해서는 안된다.
- 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존하기 때문이다.
안정된 추상화
- 추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라서 수정해야한다.
- 반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 항상, 좀 더 정확히 말하면 대다수의 경우 변경될 필요가 없다. 따라서 인터페이스는 구현체보다 변동성이 낮다.
- 안정된 소프트웨어 아키텍쳐란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍쳐라는 뜻이다.
[규칙1] 변동성이 큰 구체 클래스를 참조하지 말라, 대신 추상인터페이스를 참조하라.
- 언어가 정적타입이든 동적타입이든 관계없이 모두 적용된다.
- 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상팩토리를 사용하도록 강제한다.
[규칙2] 변동성이 큰 구체 클래스로부터 파생하지 말라.
- 의존성 관계를 갖는 상속관계는 강력한 관계이고 변경하기 어렵다. 상속을 신중하게 사용해야한다.
- 정적타입언어 에서 상속은 소스코드에 존재하는 모든 관계중에서 가장 강력한 동시에 변경하기 어려우므로, 상속을 아주 신중하게 사용해야 한다.
- 동적 타입 언어라면 문제가 덜 되지만 의존성을 가진다는 사실에는 변함이 없다.
[규칙3] 구체함수를 오버라이드 하지 말라.
- 구체함수는 소스코드 의존성을 필요로 한다. 따라서 구체함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게되며, 실제로는 그 의존성을 상속하게 된다.
- 이러한 의존성을 제거하려면, 차라리 추상함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야한다.
[규칙4] 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 마라.
팩토리
- 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야한다. 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스코드 의존성이 발생하기 때문이다.
- 객체지향언어에서 바람직하지 못한 의존성을 처리할 때 추상팩토리를 사용하곤 한다.
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의 인스턴스에 접근할 것이다.