서문
좋은 소프트웨어 시스템은 깔끔한 코드로부터 시작한다.
좋은 벽돌을 사용하지 않으면 빌딩의 아키텍처가 좋고 나쁨은 그리 큰 차이가 없는 것과 같다. 반대로 좋은 벽돌을 사용하더라도 빌딩의 아키텍처를 엉망으로 만들 수 있다. 그래서 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데 그게 바로 SOLID이다.
SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명한다. 단, 클래스라는 단어를 사용했다고 객체지향 소프트웨어에만 국한되는 것은 아님. 클래스는 단순히 함수와 데이터의 집합을 가리킨다.
SOLID의 목적은 아래와 같다.
- 변경에 유연하다
- 이해하기 쉽다.
- 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
SRP 단일 책임 원칙
SOLID 원칙 중 의미 전달이 가장 잘못 전달된 케이스이다. 이 원칙의 이름만 보면 모듣ㄴ 모듈이 단 하나의 일만 해야 한다는 의미로 받아들이기 쉽다. 단 하나의 일만 해야 한다는 원칙은 사실 따로 있다. 함수는 반드시 하나의 일만 해야한다는 원칙이다.
역사적으로 SRP는 아래와 같이 기술되어 왔다.
- 단일 모듈은 변경 이유가 오직 하나이어야 한다.
- 하나의 모듈은 하나의 사용자 또는 이해관계자에 대해서만 책임져야 한다.
- 즉, 하나의 모듈은 하나의 액터에 대해서만 책임져야 한다.
모듈이란? 소스파일이다. 모듈은 단순히 함수와 데이터구조로 구성된 응집된 집합이다.
‘응집'이라는 단어가 SRP를 암시한다. 단일 액터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성이다.
이 원칙을 이해하는 가장 좋은 방법은 이 원칙을 위반하는 징후들을 살펴보는 것이다
- 우발적 중복
- 한 클래스에 여러 액터가 사용하는 메서드들이 있는 경우 각 액터들은 본인과 의존성이 없는 다른 메서드의 변경에도 영향을 받는다.
- 병합
- 두 명의 서로 다른 개발자가 같은 클래스를 변경하면 변경사항간에 충돌사항이 생긴다.
- 이 문제를 벗어나는 방법은 서로 다른 액터를 뒷받침하는 코드로 서로 분리하는 것이다.
단일 책임 원칙은 메서드와 클래스 수준의 원칙이다. 하지만 이보다 상위의 두 수준에서도 다른 형태로 다시 등장한다. 컴포넌트 수준에서는 공통 폐쇄 원칙이 되고, 아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축이 된다.
OCP 개방 폐쇄 원칙
소프트웨어는 확장에는 열려있고, 변경에는 닫혀있어야 한다. 즉 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안된다.
소프트웨어 아키텍처를 공부하는 가장 근본적인 이유가 바로 이 때문이다. 만약 요구사항을 살짝 확장하는데 소프트웨어를 엄청나게 수정해야 한다면 그 아키텍트는 실패한것
아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고 분리한 기능을 컴포넌트의 계층구조로 조직화한다. 컴포넌트 계층구조를 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
OCP는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는데 있다. 이러한 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.
LSP 리스코프 치환 법칙
상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야한다는 계약을 반드시 지켜야 한다.
LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙이다.
아키텍처 관점에서 LSP를 이해하는 최선의 방법은 이 원칙을 어겼을 때 시스템 아키텍처에서 무슨 일이 일어나는지 관찰하는 것이다.
위배사례 예시.
다양한 택시파견 서비스를 통합하는 애플리케이션을 만드는 상황이다. 택시업체 acme는 프로그래머를 몇명 고용했는데 이들은 서비스 사양서를 그다지 신중하게 읽지 않았다고 하자. 그래서 url의 ‘destination’이름의 필드를 dest로 축약해서 사용했다고 치자.
뻔한 일이지만, 우리는 이 예외 사항을 처리하는 로직을 추가해야만 할 것이다. 이를 위한 가장 간단한 방법은 파견 명령어를 구성하는 모듈에 if문을 추가하는것이다.
하지만 실력있는 아키텍트라면 당연히 시스템을 이런식으로 구성하는 것을 용납하지 않는다. ‘acme’라는 단어를 코드 자체에 추가하면, 끔찍할 뿐 아니라 이해할 수 없는 온갖 종류의 에러가 발생할 여지를 준다.
아키텍트는 이 같은 버그로부터 시스템을 격리해야 한다.
LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다. 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.
ISP 인터페이스 분리 법칙
소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다. ??
정적타입의 언어는 사용자가 import, use, include와 같은 타입 선언문을 사용하도록 강제한다. 루비나 파이선과 같은 동적 타입 언어에서는 소스 코드에 이러한 선언문이 존재하지 않고, 대신 런타임에 추론이 발생한다. 따라서 소스코드 의존성이 아예없으므로 결국 재컴파일과 재배포가 필요없다.
동적 타입 언어를 사용하면 정적 타입 언어를 사용할 때 보다 유연하며 결합도가 낮은 시스템을 만들 수 있는 이유는 바로 이 때문이다. 이러한 사실로 인해 ISP를 아키텍처가 아니라 언어와 관련된 문제라고 결론내릴 여지가 있지만 아니다.
ISP를 사용하는 근본적인 동기를 살펴보면, 잠재되어 있는 더 깊은 우려사항을 볼 수 있다. 일반적으로, 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일이다. 소스코드 의존성의 경우 이는 분명한 사실인데, 불필요한 재컴파일과 재배포를 강제하기 때문이다. 하지만 더 고수준인 아키텍처 수준에서도 마찬가지로 상황이 발생한다.
System S → Framwork F → Database D
예를들어 S시스템 구축에 참여하고 있는 아키텍트가 있다고 하자. 아키텍트는 F프레임워크를 시스템에 도입하기를 원한다. 그리고 F프레임워크는 특정한 D 데이터베이스를 반드시 사용하도록 만들어졌다고 가정하자.
F에서는 불필요한 기능, 따라서 S는 전혀 관계없는 기능이 D에 포함된다고 가정하면, 그 기능 때문에 D 내부가 변경되면서 F를 재배포해야 할 수 있고, 따라서 S까지 영향을 받을 수 있다.
여기서 배울 수 있는 교훈은 불필요한 짐을 실은 무언가에 의존하면 예상 치도 못한 문제에 빠진다는 사실이다.
DIP 의존성 역전 원칙
의존성 역전 원칙에서 말하는 ‘유연성이 극대화된 시스템'이란 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.
이 아이디어를 규칙으로 보기는 비현실적인다, 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존하기 때문이다. 예를들어 자바에서 사용하는 String을 애써 추상 클래스로 만들 필요는 없다. String 클래스는 매우 안정적이며, 변경되는 일이 거의 없고 있더라도 엄격하게 통제된다.
이러한 이유로 DIP를 논할 때 운영체제나 플랫폼과 같이 안정성이 보장된 환경에선 무시하는 편이다.
우리가 의존하지 않도록 피하고자 하는 것은 바로 변동성이 큰 구체적인 요소이다. 이 구체적인 요소는 우리가 열심히 개발하는 중이라 자주 변경될 수 밖에 없는 모듈이다.
추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 수정해야 한다. 반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 항상, 좀 더 정확이 말하면 대다수의 경우 변경될 필요가 없다. 따라서 인터페이스는 구현체보다 변동성이 낮다.
실제로 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추기위해 애쓴다. 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력한다. 이는 소프트웨어 설계의 기본이다.
즉, 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.
자바등 대다수의 객체지향언어 에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상팩토리를 사용하곤 한다.
Loading Comments...