5 min to read
SOLID 설계원칙
SOLID 설계원칙
SOLID 원칙
SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법으로, ‘클래스’란 단어를 사용했다고 해서 객체지향 소프트웨어에만 적용되는 것은 아니다
단순히 함수와 데이터를 결합한 집합
의 의미로 클래스를 개념을 접근하여 SOLID 원칙에 접목한다.
SOLID 원칙의 목적
- 변경에 유연하다
- 이해하기 쉽다
- 많은 SW 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
SOLID 원칙의 목적은 코드 수준보다 상위 개념인 모듈 수준에서 작업할 때 적용될 수 있다.
중간 수준
의미란 하나의 큰 아키텍처 안에서 SOLID의 개념이 건축 개념으로 빗대어 볼 때 큰 건물을 짓는 재료 중의 하나인 벽돌 수준의 개념으로 이해해볼 수 있을 것 같다.
SRP(단일 책임 원칙)
각 SW 모듈은 변경의 이유가 단 하나여야만 한다.
보통 단 하나의 일만 해야 한다는 의미로 잘못 알고 있는데 이는 함수는 반드시 한 가지의 일만 해야 한다는 다른 원칙이 따로 있다. 이는 SOLID 원칙보다 더 저수준 개념에 해당하는 내용이다.
SRP란 다시말해, 오직 하나의 액터(actor)에 대해서만 책임져야한다. 하나의 모듈(소스파일)은 하나의 사용자, 또는 하나의 이해관계자에 대해서만 책임져야한다.
SRP 위반 사례
우발적 중복
Employee 클래스 내에 세 가지 역할을 하는 메서드가 존재
- calculatePay : 재무팀
- reportHours : 인사팀
- save : DBA
세 가지의 액터(재무팀/인사팀/DBA)가 하나의 클래스에 결합된 상태.
이로 인해 일반적으로 하나의 클래스 내에 메소드들 중 중복되는 코드가 발생할 경우 private 메서드를 구현함으로써 중복 코드를 줄이려는 노력을 시도할 수 가 있는데,
만약 재무팀에서 calculatePay 기능을 수정하면서 중복 코드가 들어있는 private 메서드도 함께 수정하게 된다면 인사팀과 DBA에서 사용되는 기능들은 예상치 못한 코드 수정으로 에러를 야기할 수 있게 된다.
따라서, 서로 다른 액터가 의존하는 코드는 서로 분리하는 것이 좋다.
병합
하나의 Employee 클래스 내에 코드 중 재무팀과 인사팀에서 관리하고 있는 calculatePay 메서드와 reportHours 메서드를 동시에 수정하게 될 경우, 병합하는 과정에서 동일한 파일에 대한 수정을 진행했기 떄문에 충돌이 발생할 수 있다.
즉, 동일한 파일을 서로 다른 목적으로 소스 파일 수정을 시도하는 과정에서 코드 충돌이 발생할 수 있다.
해결책
- 클래스 분리
- 세 가지 역할을 하는 클래스를 각각 생성, 각 클래스는 EmployeeData라는 하나의 클래스를 공유.
- 각 클래스는 서로의 존재를 모름으로써 우연한, 우발적인 중복을 피할 수 있게된다.
BUT, 구현 시 일일히 세 클래스를 인스턴스화 하고 추적해야 하는 단점이 있다.
- Facade 패턴
- EmployeeFacade에서는 별도의 코드 없이 세 개의 클래스를 생성하고 메서드를 위임하는 일만을 책임진다.
BUT, 다만 일부 개발자들은 중요한 핵심 기능은 Employee 클래스가 직접 들고 있어야 한다고 생각할 수 있기 때문에 위 방법을 선호하지 않을 수 있다.
- 덜 중요한 부분만 Facade 패턴 적용
OCP(개방-폐쇄 원칙)
기존 코드를 수정하기보단, 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 SW 시스템을 쉽게 변경할 수 있다.
OCP를 적용하기 위해서는 두 가지 원칙이 선행된 구조에서 제대로 그 목적을 발휘할 수 있다.
- SRP 적용
- 서로 다른 목적으로 변경될 수 있는 요소를 적절하게 분리
- DIP 적용
- 분리된 요소 사이의 의존성을 조직화함으로써 새로 조직화된 구조에서 하나의 요소가 그 행위가 확장했을 떄 다른 요소에는 영향이 발생하지 않음을 보장해야 한다.
즉, OCP 목적을 적용하려면 SRP와 DIP가 선행된 구조여야 한다.
- 화살표가 A 클래스에서 B 클래스로 향한다면, A 클래스에서는 B 클래스를 호출하지만 B 클래스에서는 A 클래스를 전혀 호출하지 않음.
- 모든 컴포넌트 관계는 단 방향으로 이루어진다.
- 이들 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
- A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다.
DIP
- FinancialDataGateway 인터페이스는 FinancialReportGenerator와 FinancialDataMapper 사이에 위치.
- FinancialDataGateway 인터페이스가 없었다면 의존성이 Interactor 컴포넌트에서 Database 컴포넌트로 바로 향하게 된다.
정보은닉
- FinancialReportRequester 인터페이스는 FinancialReportController가 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해 존재함으로써 FinancialEntities에 대해 추이 종속성(transtive dependency)을 가지게 된다.
- 추이 종속성을 가지게 되면, 소프트웨어 엔티티는 ‘자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다’는 소프트웨어 원칙을 위반하게 된다.
- Controller에서 발생한 변경으로부터 Interactor를 보호하는 일의 우선순위가 가장 높지만, 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다. 이를 위해 Interactor 내부를 은닉한다.
LSP(리스코프 치환 원칙)
상속 관계를 맺는 상호 관계는 서로 치환, 대체 가능한 관계여야 한다.
LSP의 올바른 예시
- Billing 애플리케이션의 행위가 License 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않음.
- 이들 하위 타입은 모두 License 타입을 치환할 수 있다.
LSP 위반 예시
- Rectangle(직사각형)의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면, Square(정사각형)의 높이와 너비는 반드시 함께 변경되기 때문.
ISP(인터페이스 분리 원칙)
사용되지 않는 것에는 의존관계를 맺지 않아야 한다.
ISP의 잘못된 예시
ISP의 적절한 예시
ISP와 아키텍처
일반적으로 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 소스코드와 아키텍처 두 수준 모두에게 큰 비용을 치루게 한다. 불필요한 재컴파일과 재배포를 강제하기 때문이다.
DIP(의존성 역전 원칙)
고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 의존해서는 안된다.
의존성 역전 원칙에서 말하는 ‘유연성이 극대화된 시스템’이란 소스 코드 의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는 시스템이다.
안정된 추상화
추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라서 수정은 필연적이다.
반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현한 인터페이스는 변경될 필요가 없다.
인터페이스는 구현체보다 변동성이 낮다
즉, 안정된 SW 아키텍처란 변동성이 큰 구현체에 의존을 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처를 의미한다.
구체적인 코딩 실천법
- 변동성이 큰 구체 클래스를 참조하지 말라
- 변동성이 큰 구체 클래스로부터 파생하지 말라
- 구체 함수를 오버라이드하지 말라
- 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라
팩토리
- 의존성을 관리하기 위해 추상 팩토리 패턴을 사용한다.
- 제어 흐름은 코드의 의존성과는 정반대로 향한다.
즉, 소스 코드의 의존성은 제어 흐름과는 반대 방향으로 역전되게 되는데 이러한 이유로 의존성 역전
이라고 부른다.
DIP는 향후 아키텍처 다이어그램에서 가장 눈에 드러나는 원칙이 될 것이다.
Comments