반응형
SOLID 원칙의 목적은 중간 수준
의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.
- 변경에 유연하다.
- 이해하기 쉽다.
- 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
중간 수준?
- 모듈 수준에서 작업할 때 적용할 수 있음. 즉, 코드 수준보다는 조금 상위에서 적용되며 모듈과 컴포넌트 내부에서 사용되는 소프트웨어 구조를 정의하는데 도움을 줌
단일 책임 원칙 (SRP - Single Responsibility Principle)
하나의 모듈(단일 모듈)은 변경의 이유가 오직 하나뿐이어야 한다.
- 하나의 모듈은 하나의 액터에 대해서만 책임져야 한다.
- 액터? 해당 변경을 요청하는 집단을 뜻함
- 모듈이 단 하나의 일만 해야 한다라는 뜻이 아니다. 단 하나의 일만 해야하는 원칙은 함수다.
- 함수는 반드시 하나의 일만 해야 한다. (조금 더 저수준에서 사용된다.)
- 모듈? 함수와 데이터 구조로 응집된 집합
- 여기서는 클래스로 보아도 무방할 듯 하다. 하지만 보통 모듈은 더 큰 단위를 뜻하지 않는가? todo
SRP 위반 사례1 - 우발적 중복
예) 급여 애플리케이션의 Employee클래스
세 가지 메서드가 서로 다른 세명의 액터를 책임지기 때문에 이 클래스는 SRP를 위반한다.
- calculatePay() 회계팀에서 CFO 보고를 위해 사용
- reportHours() 인사팀에서 COO 보고를 위해 사용
- save() 데이터베이스 관리자가 CTO 보고를 위해 사용
이 세 메서드가 Employeee 단일 클래스에 배치되어 세 액터들이 결합되었다. 이렇게 되면 CFO팀에서 결정한 조치가 COO팀에 의존하는 무언가에 영향을 줄 수 있게 된다.
- calculatePay()와 reportHours()에서 초과 근무를 제외한 업무 시간을 계산하는 메서드 regularHours()를 사용한다고 했을 때, 한 팀에서 업무시간 계산 방식(regularHours)을 변경해버리면 다른팀에서 문제가 생길 수 있다. (흔히 일어날 수 있는 case)
서로 다른 액터가 의존하는 코드를 서로 분리 하자
해결책
가장 확실한 방법은 데이터와 메서드를 분리
하는 방식이다. 메서드를 각기 다른 클래스로 이동시킨다. 그 후 facade 패턴 적용
- 파라미터로 EmployeeData 사용
- 퍼사드(Facade)패턴 이용하여, 요청된 메서드를 가지는 객체로 위임한다.
- facade가 없을시, 세 가지 클래스를 인스턴스화하고 의존해야하는 단점이 생긴다.
결론
- SRP는 메서드와 클래스 수준의 원칙이다.
- 상위 모듈 혹은 컴포넌트 수준에서는 공통 폐쇄 원칙(Common Closure Principle)이 등장한다.
개방-폐쇄 원칙 (OCP - Open-Closed Principle)
소프트웨어 개체(artifcat)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
- 개체의 행위는 확장할 수 있어야 하지만, 개체를 변경해서는 안된다.
- 요구사항을 살짝 확장하는데 엄청나게 수정해야 한다면, 실패한 시스템 설계다. (확장의 기준 - 요구사항)
- OCP는 클래스와 모듈을 설계할 때 도움되는 원칙으로 알고 있지만, 아키텍처 컴포넌트 수준에서 고려할 때 중요한 의미를 가진다.
사고 실험
- 소프트웨어 아키텍처가 훌륭하다면 변경되는 코드의 양이 가능한 한 최소화 될 것. 이상적인 변경량은 0
- 서로 다른 목적으로 변경되는 요소를 적절하게 분리(SRP)하고, 요소 사이의 의존성을 체계화(DIP)함으로써 변경량을 최소화할 수 있다.
- 재무 제표를 웹 페이지로 보여주는 시스템에서 프린터 출력 기능이 추가되는 경우 SRP를 적용하면 아래와 같음
보고서 생성
이 두 개의 책임으로 분리된다는 사실에 관심을 가져야 한다. (Web Reporter, Print Reporter)- 이처럼 책임을 분리하면, 두 책임 중 하나에서 변경이 발생하더라도 다른 하나는 변경되지 않도록 소스 코드 의존성을 조직화하고 변경이 발생하지 않음을 보장해야 한다.
- 이러한 목적을 달성하기 위해선 처리 과정을 클래스 단위로 분할하고, 이중선으로 표시한 컴포넌트 단위로 구분해야 한다.
- 위의 모든 의존성은 소스코드 의존성이며, 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
- A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다. A -> B
- Presenter에서 발생한 변경으로부터 Controller를 보호하고자 한다.
- Interactor는 다른 모든 것에서 발생한 변경으로부터 보호받는 위치이다. 즉, OCP를 가장 잘 준수할 수 있는 곳이다.
- Datbase, Controller, Presenter, View에서 발생한 어떤 변경도 Interactor에 영향을 주지 않는다.
- Interactor는 비즈니스 로직이 포함되어야하는 고수준 컴포넌트다. 고수준 컴포넌트는 저수준 컴포넌트의 변경으로부터 보호할 수 있다.
방향성 제어
- FinancialGateGateway 인터페이스는
의존성 역전
을 위한 인터페이스이다.- FinancialReportGenerator, FinancialDataMapper 사이에 위치
- FinancialGateway 인터페이스가 없었다면, 의존성이 Interactor 컴포넌트에서 Database 컴포넌트로 바로 향하게 된다.
정보 은닉
- FinancialReportRequester 인터페이스는 DIP와는 다른 목적을 가진다.
- 이 인터페이스는 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해 존재
- 만약 이 인터페이스가 없었다면 Controller는 Financial Entites에 대해 추이 종속석(transitive dependency)를 가진다.
추이 종속성
: A 클래스가 B에 의존하고, B 클래스가 C에 의존한다면, 클래스 A는 클래스 C에 의존하게 된다.
- 추이 종속성을 가지게 되면, 소프트웨어 엔티티는
자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안된다.
는 소프트웨어 원칙을 위반하게 된다. (feat. 인터페이스 분리 원칙 ISP, 공통 재사용 원칙 CRP) - Controller에서 발생한 변경으로 Interactor를 보호하는 일의 우선순위가 가장 높지만, 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다. 이를 위해 Interactor 내부를 은닉한다.
결론
- OCP는 시스템 아키텍처를 떠받치는 원동력 중 하나
- OCP의 목표는
시스템을 확장하기 쉬운 동시에 변경으로 인해 많은 영향을 받지 않도록
하는 데 있다.- 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 의존성 계층 구조가 만들어져야함
리스코프 치환 원칙 (LSP - Liskov Substitution Principle)
- 바바라 리스코프가 하위 타입(sbutype)을 아래와 같이 정의
- 필요한 것은 다음과 같은 치환(sbustitution)원칙. S 타입의 객체 o1 각각에 대응하는 T 타입의 객체 o2 가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위타입이다.
상속을 사용하도록 가이드하기
- 요금 계산(calcFee())을 하는 License 인터페이스 존재
- License는 2가지 하위 타입이 존재하며, 서로 다른 알고리즘을 이용하여 라이센스 비용을 계산
- 이 설계는 LSP를 준수
- Billing 애플리케이션의 행위가 License 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않기 때문. 이들 하위 타입은 모두 License 타입으로 치환할 수 있다.
정사각형/직사각형 문제
- LSP를 위반하는 전형적인 문제로 정사각형/직사각형문제가 있다
- Sqaure는 Rectangle의 하위 타입으로 적합하지 않음
- Rectangle은 높이/너비가 독립적으로 변경될 수 있지만, Sqaure는 높이와 너비가 반드시 함께 변동되기 때문
- 이런 LSP 위반을 막기 위한 유일한 방법은 if 문등을 추가하여 Rectangle이 실제로는 Sqaure인지 검사하는 메커니즘을 User에 추가하는 것이다.
- but, User의 행위가 사용하는 타입에 의존하게 되므로, 결국 타입을 서로 치환할 수 없다.
val r: Rectangle = Sqaure()
r.setW(5)
r.set(2)
assert(r.area() == 10) // Square인 경우 error
LSP와 아키텍처
- 객체 지향 초창기에는 LSP는 상속을 사용하도록 가이드하는 방법으로 간주되었다.
- 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해 왔다.
LSP 위배 사례
택시 파견(tax dispatch)서비스를 토압하는 애플리케이션을 만들고 있음
- purplecard.com/driver/Bob 에 파견에 필요한 정볼르 덧붙인 후 PUT으로 호출
- purplecard.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/destination/ORD
- 한 업체에서 사양서를 신중하게 읽지 않고 destination 필드를 dest로 축약해서 사용. 하지만 이 업체는 무척 큰 업체이기 때문에 요구사항을 들어줘야하는 상황 (acme.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/dest/ORD)
- REST 서비스들이 서로 호환되지 않는 상황.
- 이 경우 if문으로 파견 명령어를 구성하는게 아니라
버그로부터 시스템을 격리
해야 한다. - 아키텍트는 REST 서비스들의 인터페이스가 서로 치환 가능하지 않다는 사실을 처리하는 중요하고 복잡한 매커니즘을 추가해야 한다.
// 이런식은 곤란하다.
if (driver.getDispatchUri().startsWith("acme.com")) ....
파견 URI를 Key로 사용하는 설정용 데이터베이스를 이용하는 파견 명령 생성 모듈을 생각해봐야할 수도 있다.
URI | DispatchFormat |
Acme.com | /pickupAddress/%s/pickupTime/%s/dest/%s |
*.* | /pickupAddress/%s/pickupTime/%s/destination/%s |
결론
- LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야 한다.
- 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야할 수 있기 때문이다.
인터페이스 분리 원칙 (ISP - Interface Segregation Principle)
- 다수의 사용자가 OPS 클래스의 오퍼레이션을 사용한다.
- User1은 오직 op1만 사용하지만, op2와 op3 메소드에 의존하게 된다.
- 정적 타입언어라면 OPS 클래스 op2 소스코드가 변경되면 User1도 다시 컴파일 배포가 필요하다.
- 오퍼레이션을 인터페이스 단위로 분리하면 이러한 문제가 해결된다.
- User1은 U1Ops에 의존하지만 OPS에는 의존하지 않게 된다.
- 따라서 OPS에서 발생한 변경이 User1과는 전혀 관련이 없다면 새로 컴파일 배포가 필요 없다.
ISP와 아키텍처
- 일반적으로 필요 이상으로 많은걸 포함하는 모듈에 의존하는 것은 해롭다.
- 소스 코드 의존성의 경우 불필요한 재컴파일과 재배포를 갖에하기 때문이다. 하지만 더 고수준인 아키텍처 수준에서도 마찬가지 상황이 발생한다.
- 시스템(S)을 구축하는 아키텍트는 프레임워크(F)를 도입하길 원하고, 프레임워크(F)를 개발자는 특정한 데이터베이스(D)를 반드시 사용하도록 만들었다고 가정하자.
- S는 F에 의존하며, F는 다시 D에 의존한다.
- F에서는 불필요한 기능, 따라서 S와는 관에 없는 기능이 D에 포함되어있다고 가정
- 그 기능 때문에 D 내부가 변경되면, F를 재배포해야 할 수도 있고, S까지 재배포해야 할지 모른다.
- D내부의 기능 중 F와 S에서 불필요한 기능 때문에 문제가 발생하여 F, S와 영향을 준다는게 문제이다.
결론
- 불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실을 기억하자.
- 13장 컴포넌트 응집도에서 더 세세히 다룬다.
모듈을 작게 쪼개서 관리하는 것이 가장 좋은 방법 아닐까?
하나의 공통모듈을 관리하게되면 공통 모듈 수정시 예상치 못한 문제가 발생할 수도 있으니 하지만 모듈이 많아지게 되면 관리가 가능할지?
의존성 역전 원칙 (DIP - Dependency Inversion Principle)
유연성이 극대화된 시스템이란 소스코드 의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는 시스템이다.
- 자바와 같은 정적 타입 언어에서 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만 참조해야한다는 뜻
- 이 아이디어를 규칙으로 보기는 확실히 비현실 적이다.
- 자바에서 String은 구체 클래스이며, 이를 굳이 추상 클래스로 만들려는 시도는 현실성이 없다.
- 반면 String 클래스는 매우 안정적이다. String 클래스가 변경되는 일은 거의 없으며, 있더라도 엄격하게 통제된다.
- 이러한 이유로 DIP를 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경은 무시하는 편이다.
- 우리가 의존하지 않도록 피하고자 하는 것은
변동성이 큰(volatile)
구체적인 요소다.
안정된 추상화
- 추상 인터페이스에 변경이 생기면 구현체들도 따라 수정해야 하지만, 구체적인 구현체에 변경이 생기더라도 인터페이스는 대다수 변경될 필요가 없다.
- 즉, 인터페이스는 구현체보다 변동성이 낮다.
- 인터페이스의 변동성을 낮춰야하며, 인터페이스를 변경하지 않고도 구현체의 기능을 추가할 수 있는 방법이 소프트웨어 설계의 기본이다.
- 안정된 소프트웨어 아키텍처란
변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처
구체적인 코딩 실천법
- 변동성이 큰 구체 클래스를 참조하지 말라
- 추상 인터페이스를 참조하라
- 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리(abstract Factory)를 사용하도록 강제한다.
- 변동성이 큰 구체 클래스로부터 파생하지 말라
- 정적 타입 언어에서 상속은 소스 코드에 존재하는 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다. 따라서 신중하게 사용해야 한다.
- 구체 함수를 오버라이드 하지 말라
- 대체로 구체 함수는 소스 코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드 하면 의존성 제거가 불가능 하며 의존성을 상속하게 된다.
- 이러한 의존성을 제거하려면, 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
팩토리
- 위 규칙들을 준수하려면 변동성이 큰 구체 클래스는 주의해서 생성해야 한다.
- 사실상 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문이다.
- 자바 등 대다수의 객체 지향언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다.
- 위의 그림 설명
- Application은 Service 인터페이스를 통해 ConretImpl을 사용하지만, Application에서는 어떤 식으로든 ConcreteImpl 인스턴스를 생성해야한다.
- ConcreteImpl에 대해 소스 코드 의존성을 만들지 않으려면 Application은 ServiceFactory 인터페이스의 makeSvc 메서드를 호출한다.
- 이 메서드는 ServiceFactory로부터 파생된 ServiceFactoryImpl에서 구현된다. 그리고 ServiceFactoryImpl 구현체가 ConcreteImpl의 인스턴스를 생성한 후 Service 타입으로 반환한다.
- 곡선은 아키텍처 경계를 뜻한다.
- 구체적인 것들로부터 추상적인 것들을 분리한다. 소스 코드 의존성은 해당 곡선과 교차할 때 추상적인 쪽으로 향한다.
- 곡선은 시스템을 두 가지 컴포넌트로 분리한다.
- 추상 컴포넌트 - 애플리케이션의 고수준 업무 규칙 포함
- 구체 컴포넌트 - 업무 규칙을 다루기 위해 필요한 모든 세부사항 포함
- 제어흐름은 소스 코드 의존성과 정 반대 방향으로 곡선을 가로지른다.
- 소스 코드 의존성은 제어 흐름과는 반대 방향으로 역전된다. 이러한 이유로 이 원칙을 의존성 역전(Dependency Inversion)이라 부른다.
구체 컴포넌트
ServiceFactoryImpl -> ConcreteImpl
로 구체적인 의존성이 하나 있다. (DIP 위배)- 일반적이며 DIP 위배를 모두 없앨 수 없음
- DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과 분리가 가능하다.
결론
- 의존성은 이 곡선을 경계로, 더 추상적인 엔티티가 있는 쪽으로만 향한다.
- 추후 이 규칙은 의존성 규칙(Dependency Rule)이라 부를 것이다.
반응형
'프로그래밍 노트 > 아키텍처' 카테고리의 다른 글
[클린 아키텍처] 5장. 아키텍처 (0) | 2023.11.11 |
---|---|
[클린 아키텍처] 4장. 컴포넌트 원칙 (4) | 2023.11.09 |
[클린 아키텍처] 객체 지향 프로그래밍 (0) | 2023.09.04 |
[클린 아키텍처] 행위, 아키텍처 두가지 가치에 대하여 (0) | 2023.08.28 |
[클린 아키텍처] 설계? 아키텍처? (0) | 2023.08.28 |