- Today
- Total
개성있는 개발자 되기
3부 설계 원칙 본문
좋은 소프트웨어 시스템은 깔끔한 코드 (clean code)로부터 시작한다. 그래서 좋은 아키텍처를 정의하는 원칙이 필요한데, 그게 바로 SOLID이다.
SOLID 원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.
- 중간 수준 : 프로그래머가 이들 원칙을 모듈 수준에서 작업할 때 적용할 수 있는 수준
1) 변경에 유연하다.
2) 이해하기 쉽다.
3) 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
SRP : 단일 책임 원칙 (Single Responsibility Principle)
- 각 소프트웨어 모듈은 변경의 이유가 단 하나여야만 한다.
OCP : 개방 폐쇄 원칙 (Open-Closed Principle)
- 기존 코드를 수정하기보다는, 새로운 코드를 추가하는 방식으로 설계해야만 소프트웨어 시스템을 쉽게 변경할 수 있다.
LSP : 리스코프 치환 원칙 (Liskov Substitution Principle)
- 서브타입에 관한 유명한 원칙이다. 유연한 소프웨어는 구성요소가 반드시 서로 치환가능해야 한다는 계약을 반드시 지켜야 한다.
ISP : 인터페이스 분리 원칙 (Interface Segregation Principle)
- 소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.
DIP : 의존성 역전 원칙 (Dependency Inversion Principle)
- 고수준 정책(추상 로직)을 구현하는 코드는 저수준 세부사항을 구현하는 코드(비즈니스 로직)에 절대 의존해서는 안된다.
이들 원칙이 아키텍처 관점에서 지닌 의미를 알아보자.
1. SRP : 단일 책임 원칙
이름만 들어서는, 하나의 모듈이 단 하나의 일만 해야 한다는 의미로 들리지만, 사실 이와 같은 의미는 함수에 적용된다.
함수는 반드시 단 하나의 일만 해야 한다
→ 이 원칙은 커다란 함수를 작은 함수들로 리팩토링하는 더 저수준에서 사용된다.
그렇다면 SRP의 의미는?
하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다.
→ 하나의 모듈이 있다고 가정했을 때, 이 모듈의 내용이 변경되어야 하는 이유는 A기획자 또는 A업무 때문이어야 하고, 다른 B기획자 또는 B업무 때문에 변경되지 않아야 한다는 것이다.
ex) 상품 저장하는 method에서 ecms의 로직이 심어져있다면, 이 method가 책임지는 액터가 상품 & ECMS가 되버린다.
징후1 : 우발적 중복
위 클래스는 SRP를 위반하는데, 이들 세가지 메서드는 서로 매우 다른 세명의 액터를 책임지기 때문이다.
만약, 어떠한 메소드 A를 세 액터가 공유하고 있는 상황에서 CTO 부서에서 메소드A를 수정했을 경우, 다른 액터들에게 영향을 줄 수 있다.
→ SRP는 서로 다른 액터가 의존하는 코드를 서로 분리하라고 말한다.
✔ Note
하나의 메소드를 서로 다른 액터가 공유하지 말라고 하는 것이 이 원칙의 핵심인 것 같다.
사실, 이 책에서 말하는 저수준?의 코드에서는 자주 사용되는 메서드는 하나의 공유 메소드로 만들어 놓고, 다른 여러 영역에서 호출해서 쓰는 것이 맞다. SRP는 이와같은 코드 수준의 영역에서의 원칙이 아닌, 조금 더 고수준인, 액터, 사용자 입장에서 원칙을 설명하는 것 같다.
예를들어, A부서, B부서에서 하나의 메소드를 서로 공유하지 말라 라는 것처럼 말이다.
징후2 : 병합
병합은 친숙하게 말해서 머지 (merge)로 이해하면 된다. 병함의 징후는, 많은 사람들이 서로 다른 목적으로 동일한 소스파일을 변경하는 경우에 해당한다.
해결책
이 문제를 해결할 수 있는 가장 확실한 방법은 메서드를 각기 다른 클래스로 분리 하는 것이다.
위와 같이 한다면, 각 클래스는 자신의 메서드에 필요한 소스코드만 포함하기 때문에, 서로의 존재를 모른다.
→ 우연한 중복을 피할 수 있다.
→ 세 액터가 사용하는 메소드를 클래스로 분리한 것이다.
반면, 이 해결책은 개발자가 세 가지 클래스를 인스턴스화하고 추적해야 한다는게 단점이다. 이러한 단점을 보완하기 위해 흔히 쓰는 기법으로 퍼사드 (Facade) 패턴이 있다.
Employee Facade 클래스는 메소드 요청이 온다면, 세 클래스의 객체를 생성하고, 요청된 메서드를 가지고 있는 객체로 위임하는 일을 책임진다.
△ 위 예제를 보면, Facade는 메소드의 집합으로 하나의 패키지처럼 있고, 요청이 들어왔을 때, 각 클래스의 메소드를 호출하는 방식으로 보인다. 핵심은 서로 다른 액터들이, 해당 클래스를 공유하지 않는다 라는 것인 것 같다.
✔ Note
위와 같이 Facade로 만들고, 해당 메소드가 호출 된다면 요청된 메서드를 가지고 있는 객체로 위임하는 일을 책임하도록 하는 방법이 될 수 있겠다.
2. OCP : 개방 - 폐쇄 원칙
개방-폐쇄 원칙이란, 소프트웨어 개체는 확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다.
쉽게 말해서, 뭔가 새로운 서비스를 도입하고자 하거나, 뭔가를 변경하고자 할 때, 변경되는 코드의 양이 가능한 한 최소화 된다면 훌륭한 소프트웨어라는 것이다.
서로 다른 목적으로 변경되는 요소를 적절하게 분리하고, 이들 요소 사이의 의존성을 체계화함으로써 변경량을 최소화할 수 있다.
아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층 구조로 조직화한다.
그리고, 애플리케이션에서 가장 높은 수준의 정책을 포함하는 모듈을 고수준 컴포넌트로 지정하여, 다른 부수적인 모듈들이 변경되더라도, 고수준 모듈은 영향이 안가도록 아키텍쳐를 설계한다.
△ OCP의 목표는 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다. 이러한 목표를 달성하려면, 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.
✔ Note
뭐 예를들어, View 와 비즈니스 로직의 분리로 보면 되는 것인가.
3. LSP : 리스코프 치환 원칙
프로그램 P가 있는데, S타입의 객체를 쓰고 있었다. 그 자리에 T타입의 객체로 치환을 하더라도, P의행의가 변하지 않는다면, S는 T의 하위타입이다.
1) 상속을 사용하도록 가이드하기
Billing 애플리케이션이, License Interface의 calcFee()를 사용하고 있다고 가정하자.
License Interface 하위에는 A License, B License가 있다고 하면, Billing 애플리케이션은 이 둘중 어떤 타입을 사용해도 앱이 돌아간다.
2) 정사각형/직사각형 문제
만약 치환가능하지 않은 구현체들이 있다면 LSP에 위반된다. 예를 들어, Rectangle이라는 타입 아래 Suqare라는 타입이 있다고 치면 앱에서 Rectangle을 써서 구했던 면적과 Suare를 써서 구했던 면적이 다르게 된다.
이런 형태의 LSP 위반을 막기 위한 유일한 방법은, 매서드를 쓰는 애플리케이션 단에서, if문으로 조건을 분기처리 하는 것이다. 하지만 이렇게 하면 앱의 행위가 사용하는 타입에 의존하게 되므로 결국 타입을 서로 치환할 수 없게 된다.
4. ISP : 인터페이스 분리 원칙
다수의 User가 하나의 클래스를 import해서 사용하고 있다면, 그 클래스가 변경되면 모든 User를 새로 배포해야 한다.
이걸 막기 위해서, 각 메서드(오퍼레이션)을 인터페이스 단위로 분리하여 해결할 수 있다.
- ISP는 아키텍처가 아니라 언어와 관련된 문제라고 결론내릴 수도 있다.
Java 와 같은 정적 타입언어는 사용자가 import, use 또는 include와 같은 타입 선언문을 사용하도록 강제해서, 소스코드 의존성이 발생하고, 이로 인해 재컴파일 또는 재배포가 강제되는 상황이 초래된다.
하지만, 파이썬이나 루비같은 동적 타입언어를 사용하면 보다 유연하고 결합도가 낮은 시스템을 만들 수 있다.
- ISP 를 아키텍쳐 관점에서 본다면 , 일반적으로 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일 (item-lib와 같이..) 이다.
불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠질 수 있으므로, 적절히 인터페이스로 분리해야 한다.
✔ Note
item-lib와 같이 많은 걸 포함하는 모듈에 의존하고 있다. 이 문제를 해결하려면, 공통으로 쓰는 예를 들어, 상품 정보저장이라든지, 매가 저장이라든지 하는 부분은 각 웹앱에서 인터페이스로 구현해서 가지고 있는 것도 방법일 수 있겠지만.. 만.. 그렇게 되면 한개가 변경되면 n개의 시스템을 수정해야 하는 이슈도 있겠다.
5. DIP : 의존성 역전 원칙
DIP에서 말하는 '유연성이 극대화된 시스템'이란 소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.
자바에서는 user, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상 선언만을 참조해야 한다는 뜻이다.
(String.class 와 같이 안정성이 보장된 환경은 제외)
1) 안정된 추상화
구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 변경될 필요가 없다.
실제로 뛰어난 소프트웨어는 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력한다. 이는 소프트웨어 설계의 기본이다.
✔ Note
이번 달에는 인터페이스를 이용한 설계를 해보자
즉, 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.
- 변동성이 큰 구체 클래스를 참조하지 말라. 대신 추상 인터페이스를 참조하라. (추상 팩토리를 사용)
- 변동성이 큰 구체 클래스로부터 파생(상속)하지 말라.
- 구체 함수를 오버라이드 하지말라. 오버라이드 하게되면 의존성이 생기므로 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
- 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.
2) 팩토리
사실 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 마련인데, 이를 처리할때 추상 팩토리를 사용할 수 있다.
위와 같이, Concrete Impl이라는 비즈니스 로직을 구현한 구현체를 생성하는 추상팩토리를 볼 수 있다.
Application에서 Concrete Impl을 호출한다면, 그것또한 의존성을 가지기 때문에, 추상팩토리가 해당 인스턴스를 생성하도록 한 것이다.
위에 보이는 곡선은 추상 컴포넌트(애플리케이션의 모든 고수준 업무 규칙)과 구체컴포넌트(업무 규칙을 다루기 위해 필요한 모든 세부사항) 으로 나뉘어진 것을 볼 수 있다.
제어 흐름은 소스코드 의존성과는 정반대 방향으로 곡선을 가로지른다. 추상적인 쪽으로 의존성이 향하고, 제어흐름은 구체컴포넌트(비즈니스 로직)쪽으로 흐르므로 의존성 역전이라고 부른다.
'Web Development > Clean Architecture' 카테고리의 다른 글
4부 컴포넌트 원칙 - (2) 컴포넌트 응집도 (0) | 2020.03.15 |
---|---|
4부 컴포넌트 원칙 - (1) 컴포넌트 (0) | 2020.03.15 |
클린 아키텍처 - 소프트웨어 구조와 설계의 원칙 스터디 시작 (0) | 2020.02.09 |
2부 벽돌부터 시작하기: 프로그래밍 패러다임 (0) | 2020.01.29 |
1부 소개 (0) | 2020.01.29 |