책 <클린 아키텍처>의 [Part3 설계원칙]을 참조하여 정리한 내용입니다.

개요

SOLID원칙은 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명해준다.

나쁜 벽돌을 쌓아올려 건물을 짓는다면 당연히 좋은 아키텍처가 나올 수 없다. 그렇다고 좋은 벽돌을 이용한다고 무조건 좋은 아키텍처가 나오는 것도 아니다. SOLID원칙은 좋은 벽돌을 이용해서 좋은 아키텍처를 만들어내는 원칙을 정의한다. 
구체적인 정의를 보자면, SOLID원칙은 "함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명"하는 원칙이며, 모듈 수준에서 변경 용이성, 빠른 가독성, 재사용성 확보를 목표로 한다. 

SOLID원칙은 각 원칙의 앞글자를 따서 만들어진 이름이며, 따라서 5가지의 원칙이 있다.

  • 단일 책임 원칙(SRP, Single Responsibility Principle)
  • 개방-폐쇄 원칙(OCP, Open-Closed Principle)
  • 리스코프 치환 원칙(LSP, Liskov Substitution Principle)
  • 인터페이스 분리 원칙(ISP, Interface Segregation Principle)
  • 의존성 역전 원칙(DIP, Dependency Inversion Principle)

단일 책임 원칙(SRP, Single Responsibility Principle)

하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.

여기서 액터는 특정 변경을 요청하는 한 명 이상의 집단을 가리킨다. 
모듈은 간단히 말하면 '소스파일'이고, 언어에 따라서 적용되지 않는 경우에는 "함수와 데이터 구조로 구성된 응집된 집합"이다. 

단일 책임 원칙을 위반하는 사례와 이에 따른 해결책을 살펴보자.

잠재적 위험 - 공통 메소드의 변경

급여 어플리케이션에서 Employee클래스가 있다고 가정해 보자. 위의 Employee클래스는 3가지 메소드를 갖는데 각각은 다음과 같다.

  • calculatePay : 급여를 계산하는 메소드, 회계팀에서 기능을 정의하며, CFO보고를 위해 사용한다.
  • reportHours : 근무시간을 보고하는 메소드, 인사팀에서 기능을 정의하며, COO보고를 위해 사용한다.
  • save : Employee 관련 데이터를 저장하는 메소드, DBA가 기능을 정의하며, CTO보고를 위해 사용한다. 

하나의 클래스가 3명의 Actor를 책임지기 때문에, SRP에 위반된 경우이다.
그렇다면 SRP를 위반한 것이 구체적으로 어떤 위험을 초래할 수 있는 지 살펴보자.

다시 다음과 같은 사례를 가정해보자.
CFO에 보고되기 위해 사용되는 calculatePay메소드와 COO에 보고되기 위해 사용되는 reportHours메소드가 초과근무를 제외한 근무 시간을 계산하는 regularHours를 공통적으로 호출한다고 해보자.

이러한 상황에서 CFO의 요청으로 초과근무를 제외한 근무시간을 계산하는 메소드(regularHours)에 변경이 일어난다면, reportHours또한 영향을 받을 것이고, COO에게 보고되는 내용 또한 변경이 발생하게 된다. 이러한 변경이 인식되지 못한다면 큰 손실로 이루어질 위험이 있다.

잠재적 위험 - 병합시 충돌 상황

COO와 CTO 각각의 요청에 의해서 개발자 A와 B가 새로운 브랜치를 만들어서 변경을 수행한다고 가정해보자. 
CTO의 요청은 Employee클래스 데이터 구조의 변경이고 COO의 요청은 보고서 구조의 변경이다. 이 둘의 요청은 병합시 충돌을 야기할 것이다. 
이러한 충돌은 Employee클래스가 책임지고 있는 COO, CTO, CFO모두에게 잠재적인 위험이 될 수 있다.

해결책1 - 모듈의 분리

다음과 같이 하나의 모듈이 하나의 Actor를 책임진다면, 잠재적인 위험들을 모두 제거할 수 있다. 

  1. 공통적으로 사용하는 메소드의 변경으로 발생하는 위험이 제거 되고,
  2. 각각의 Actor에 의해서 발생하는 변경은 서로 다른 모듈의 변경만을 야기하므로 병합시 충돌 상황 또한 예방할 수 있다.

해결책2 - 퍼사드(Facade) 패턴

해결책1과 같이 단순히 모듈을 분리하면 개발자 관점에서 세 가지 클래스를 각각 추적해야 한다는 단점이 있다. 
이를 보완하는 것이 위와 같은 퍼사드 패턴이다. 
하나의 퍼사드 클래스에서 각 클래스들을 인스턴스화하고 행위들을 각각의 인스턴스에 위임 한다.

해결책3 - 중요 업무 규칙과 데이터의 근접 배치

어떤 개발자는 가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 것을 선호한다. 그러한 경우에는 위와같이 클래스를 구성해 볼 수 있다.

결론

단일 책임 원칙은 클래스 수준의 원칙이다. 이보다 상위 수준(컴포넌트, 아키텍처)에서도 동일한 내용의 원칙이 다른 형태로 등장한다.

  • 컴포넌트 수준 -> 공통 폐쇄 원칙(Common Closure Principle)
  • 아키텍처 수준 -> 아키텍처 경계(Architectural Boundary)의 생성을 책임지는 변경의 축(Axis of Change)

 

요약해보자면..

  • 단일 책임 원칙은 하나의 모듈이 하나의 액터를 책임지도록 하는 클래스 수준의 설계 원칙이다.
  • 단일 책임 원칙을 위반할 경우, 공통 메소드 변경으로 인한 영향과 병합시 충돌상황과 같은 위험이 발생할 수 있다.
  • 단일 책임 원칙을 구현하는 방법으로,
    • 간단하게는 모듈을 분리하는 것이고
    • 개발적 관점을 고려한다면 퍼사드 패턴을 차용하는 것이다.
    • 선호에 따라서 중요한 비즈니스 로직과 데이터를 가깝게 배치하고, 퍼사드 패턴을 부분적으로 적용해 볼 수도 있다.

책 <클린 아키텍처>의 [Part2 벽돌부터 시작하기:프로그래밍 패러다임]을 참조하여 정리한 내용입니다.

람다(lambda)계산법을 비롯한 함수형 프로그래밍에 대한 기본적인 이해가 부족하여 본문의 요지만 간략히 정리합니다.

 

가변 변수


함수형 언어에서 변수는 변경되지 않는다.

대표적인 함수형 언어인 클로저와 자바의 극명한 차이는 가변 변수 사용 여부에 있다. 가변 변수는 프로그램 실행 중에 상태가 변할 수 있는 변수이며, 자바 언어는 가변 변수를 사용하는 반면 클로저는 가변 변수를 사용하지 않는다.

아키텍처를 고려할 때 변수의 가변성이 중요한 이유는 경합 조건(Race Condition), 교착상태(Deadlock), 동시성 문제(Concurrent Update)가 모두 가변 변수에 의해서 발생하기 때문이다. 

그러나 불변하는 변수만을 사용하여 프로그램을 만드는 것이 가능할지라도 자원이 한정되어 있다는 점에서 타협이 필요하다. 대표적인 타협안 중 하나는 컴포넌트의 분리다. 즉, 가변 컴포넌트와 불변 컴포넌트를 분리하여 프로그램을 설계하는 것이다.

 

가변성의 분리

프로그램을 구성하는 컴포넌트를 불변과 가변으로 분리할 경우, 하나의 불변 컴포넌트는 하나 이상의 가변 컴포넌트와 통신하도록 설계하며, 가변 컴포넌트는 트랜잭션 메모리와 같은 전략을 사용한다.

 

이벤트 소싱 전략

가변성을 분리하는 방법을 사용한다는 것은 저장공간과 연산 능력의 한계를 전제로 한다. 그런데 만약 이러한 한계가 없다면?  이벤트 소싱은 상태가 아닌 트랜잭션을 저장하는 전략으로서 충분한 저장공간과 연산 능력이 있다면 유효할 수 있는 전략이다

 

요약을 해보자면..

  1. 경합 조건, 교착 상태, 동시성 문제는 가변 변수에 의해서 발생하는 문제이다.
  2. 함수형 패러다임의 가장 큰 특징은 가변 변수를 사용하지 않는다는 것이다.
  3. 자원이 충분하다면 이벤트 소싱 전략을 이용하여 순수 함수형 프로그램을 만들 수 있다.
  4. 그렇지 않다면 컴포넌트를 가변과 불변으로 분리하여 적용해 볼 수 있다.

 

참고 문헌

  • 로버트C.마틴, 클린 아키텍처(소프트웨어 구조와 설계의 원칙), 인사이트, 송준이 옮김, 2019

책 <클린 아키텍처>의 [Part2 벽돌부터 시작하기:프로그래밍 패러다임]을 참조하여 정리한 내용입니다.


서문

  • 1945년 경 앨런튜링은 바이너리 언어를 사용하여 반복문, 분기문, 할당문, 서브루틴, 스택 등과 같은 구조를 사용하여 프로그램을 만들었다.
  • 1940년대 후반 어셈블리 언어, 1951년 A0컴파일러를 필두로 수 많은 언어들이 탄생하였다.
  • 이처럼 언어적인 차원에서 수 많은 혁신이 일어난 것처럼, 프로그래밍 패러다임에 있어서도 많은 변화가 이루어져왔다.
  • 패러다임은 "프로그래밍을 하는 방법"을 정의하며 언제 어떠한 구조를 사용해야 할 지에 대한 기준이 된다.

구조적 패러다임

데이크스트라는 무부별한 점프(goto)는 프로그램 구조에 해롭다는 사실을 제시했다.

 

C언어에는 goto문이 있다는데, C언어를 안 써본 나는 오히려 goto문이 생소하다.
그렇다.. 구조적 패러다임은 goto문장과 관련이 있다.

 

구조적 패러다임은 무엇인가

구조적 패러다임은 if/then/else(분기문)와 do/while/until(반복문)과 같은 문법으로 프로그래밍을 하는 방식이다. 다시 말하면 goto문을 제약하고 분기와 반복문을 사용하는 기법이다.

 

구조적 패러다임은 현 시대의 프로그래밍 언어를 사용한다면 너무 당연하고 익순한 내용이다. 당연한 걸 당연하다고 말하는 이 패러다임..
'등장'의 이유가 중요하다.

 

구조적 패러다임의 등장 배경

데이크스트라라는 아저씨, 아니, 컴퓨터 과학자가 등장한다.
1950년 대, 데이크스트라가 살던 시절에는 바이너리 언어로 프로그래밍을 하던 시절이였고, 당시에는 간단한 프로그램일지라도 많은 세부사항들을 조작해야만 했다. 그래서 사소한 세부사항을 놓쳐서 프로그램이 실패하는 상황이 자주 벌어졌다.
이러한 상황에서 데이크스트라는 프로그램의 올바름을 입증할 필요성을 느꼈고, 수학의 공리, 정리, 따름정리, 보조정리와 같은 개념을 차용하고자 한다. 즉 작은 단위의 입증된 코드들을 결합하여 전체 코드의 올바름을 입증하고자 하였다.
그런데 무분별한 goto문은 전체 프로그램을 작은 단위로 분해하는데 방해가 된 반면에 특정 방식으로 goto문을 사용하는 것은 오히려 도움이 되었다. 이러한 방식이 바로 분기문(if/then/else)과 반복문(do/while/until)이다.

 

자세한 증명 방식은 생략, 그러나 엄밀한 의미에서 증명은 아니다

 

그러나 증명은 없었다

사실상 프로그램에서 세세한 기능들을 엄밀하게 증명하는 것은 고된 작업이었고, 사실상 그러한 입증을 할 수는 없었다. 그러나 다이크스트라는 과학적 의미에서의 입증은 가능하다고 생각하였다. 즉 어떤 서술(여기선 프로그램)에 대해서 반례가 나오지 않았다면 그것은 목표에 부합할 만큼은 참이라고 여길 수 있다.

이런 관점에서 테스트를 얘기 하자면..

그런 의미에서 테스트는 해당 프로그램이 거짓임을 입증하려는 시도라고 볼 수 있다. 그러므로 테스트 코드가 통과했다면, 해당 프로그램은 목표한 기능에 대해서 만큼은 참이라고 말할 수 있다. 다시 말해 프로그램이 목표한 기능에 대해서 올바르게 동작할 것을 입증하였다고 볼 수 있다.

 

대신에 과학적 증명이 있었다.

 

요약(을 해보자면..)

  1. goto문이 난무하던 옛날(50-60년대)에도 프로그램의 실패가 잦았음.
  2. 다이크스트라라는 컴퓨터 과학자는 프로그램의 정상동작을 보장 할 방법으로 수학적 증명의 방법을 차용하였지만, 결국엔 과학적 증명방식을 창안함.
  3. goto문을 특정 방식으로 제약하면 프로그램의 (과학적)증명이 수월하였음.
  4. 그러한 제약 방식들이 분기문(if/then/else)과 반복문(do/while/until)임.

 

 

참고 문헌

  • 로버트C.마틴, 클린 아키텍처(소프트웨어 구조와 설계의 원칙), 인사이트, 송준이 옮김

+ Recent posts