[개발서적] 클린 아키텍처 2부 벽돌부터 시작하기: 프로그래밍 패러다임 - 내용 정리 및 요약
이번에는 로버트 C 마틴의 클린 아키텍처를 읽은 내용을 정리해보도록 하겠습니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다.
3장. 패러다임 개요
[ 구조적 프로그래밍 ]
- 최초로 만들어진 패러다임은 아니지만 최초로 적용된 패러다임(1968년 다익스트라)
- 다익스트라는 무분별한 점프(goto 문법)는 프로그램 구조에 해롭다는 사실을 제시함
- 대신 if/then/else와 do/while/until과 같은 더 익숙한 구조로 대체함
- 구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과하는 패러다임
"직접적인 전환에 대한 규칙을 부과한다"는 의미는 goto 문으로 코드를 직접 넘나드는 것을 막는다는 뜻이다. 이는 언어 차원에서 goto 문법을 지원하지 않음으로써 자연스럽게 제약을 받는다.
[ 객체지향 프로그래밍 ]
- 구조적 프로그래밍보다 2년 전에 등장함(1966년 올레 요한 달, 크레스텐 니가드)
- 이들은 함수 호출 스택 프레임을 힙으로 옮기면, 함수 호출이 반환된 이후에도 함수에서 선언된 지역변수가 오래 유지될 수 있음을 발견함. 이러한 함수가 클래스의 생성자가 되었고, 지역 변수는 인스턴스 변수, 그리고 중첩 함수는 메소드가 됨
- 함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 필연적으로 다형성이 등장하게 됨
- 객체지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과하는 패러다임
일반적으로 지역 변수는 스택 영역에 저장되며 함수 호출과 함께 소멸된다. 하지만 함수를 힙으로 옮기면 함수와 그 내부의 지역변수가 메모리 해제 전까지 계속해서 남아있게 된다. 그리고 이것이 객체의 기반이 되었다는 것이다.
"간접적인 전환에 대한 규칙을 부과한다"는 의미는 절자치향에서 다형성을 구현하기 위해 함수 포인터를 사용하는 것을 막는다는 뜻이다. 이 역시도 언어 차원에서 함수 포인터를 사용하지 않고 편리하게 다형성을 구현할 수 있도록 도와준다.
[ 함수형 프로그래밍 ]
- 함수형 패러다임은 최근에야 겨우 도입되기 시작했지만, 3가지 중 가장 먼저 만들어졌고, 컴퓨터 프로그래밍 자체보다 먼저 등장함
- 알론소 처치가 수학 문제를 해결하는 과정에서 람다 계산법을 발명하였고, 함수형 프로그래밍은 람다 셰산법을 바탕으로 만들어짐
- 람다 계산법의 기초 개념은 불변성으로 심볼의 값이 변경되지 않는다는 것, 이는 할당문이 전혀 없다는 뜻
- 함수형 프로그래밍은 할당문에 대해 규칙을 부과하는 패러다임
"할당문에 대해 규칙을 부과한다"는 의미는 우리가 흔히 사용하는 할당문이 전혀 없다는 뜻이다. 굉장히 낯설 수 있지만 스칼라와 같은 함수형 언어에서는 변수에 값을 할당하는 부분이 전혀 존재하지 않는다.
[ 생각할 거리 ]
- 각 패러다임은 프로그래머에게서 권한을 박탈함
- 어느 패러다임도 새로운 권한을 부여하지 않고, 부정적인 의도를 갖는 일종의 추가적인 규칙을 부과함
- 즉, 패러다임은 무엇을 해야 할지를 말하기 보다 무엇을 해서는 안되는지를 말해줌
- 이제 우리에게서 뺏어갈 것은 더 없을 것이고, 이는 새로운 패러다임이 없을 것 임을 의미함
- 세 패러다임은 1958~1968에 걸쳐 만들어졌지만, 실제로 이후에 새롭게 등장한 패러다임은 전혀 없음.
실제로 업무를 하면서 느낀 좋은 시스템이란 제약이 많은 시스템이라는 것이다. 예를 들어 무분별하게 @Setter를 사용해 변경 가능성을 열어두는 것 보다는 필요한 변수를 변경하는 메소드를 직접 만드는 것이 훨씬 유지보수 하기 좋은 시스템이라는 것을 몸소 느꼈다. 이러한 이유로 프로젝트의 가능한 모든 곳(지역변수, 파라미터, 클래스 변수 등)에 final을 붙여주고 @Setter를 제거하는 등의 제약을 주고 있다.
그리고 또 하나 흥미로운 부분은 새로운 패러다임이 없을 것이라는 부분이다. 우리는 오늘날 엄청나게 기술적인 변화들이 많다고 얘기하지만 사실 핵심은 그대로이다. 핵심은 비즈니스 로직이고 이를 지원하는 테스트이다. 그러므로 해당 부분에 힘을 주는 것이 좋다.
4장. 구조적 프로그래밍
[ 증명 ]
- 다익스트라는 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있음을 발견함
- 반면 if/then/else와 do/while과 같은 분기와 반복이라는 단순한 제어 구조에 goto문을 사용하는 것이 좋은 방식임을 발견함
- 이러한 제어 구조만 사용한다면 증명 가능한 단위로까지 모듈을 재귀적으로 세분화하는 것이 가능해보였음
- 실제로 뵘과 야코피니는 다익스트라보다 2년 전에 프로그램이 순차, 분기, 반복이라는 3가지 구조만으로 표현할 수 있음을 증명함
[ 해로운 성명서 ]
- 다익스트라는 1968년 “goto문의 해로움”이라는 제목의 글을 실음
- 10년 이상 논쟁이 되다가 다익스트라가 승리하면서 논쟁은 수그라듬
- 컴퓨터 언어가 진화하면서 goto 문장은 거의 사라졌고, 대다수의 현대적 언어는 goto 문장을 포함하지 않음
- 현재의 우리는 모두 구조적 프로그래머이며, 여기에는 선택의 여지가 없음
- 제어흐름을 제약 없이 직접 전환할 수 있는 선택권 자체를 언어에서 제공하지 않기 때문임
우리가 흔히 사용하는 자바나 코틀린 등과 같은 오늘날의 언어에서는 goto 문법을 제공하지 않는다. 그래서 우리는 강제적으로 구조적 프로그래머일 수 밖에 없다. 또한 우리는 객체지향 언어를 이용해 객체지향으로 개발을 하고, 필요한 경우에 언어가 제공하는 함수형(Stream API 등) 패러다임을 사용하기도 한다. 즉, 우리는 3가지 패러다임을 혼용하는 개발자이다.
[ 기능적 분해 ]
- 구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 즉, 기능적으로 분해할 수 있게 되었음
- 즉, 거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있음. 그리고 이들 각 기능은 다시 저수준의 함수들로 분해할 수 있고, 이러한 분해 과정을 끝없이 반복할 수 있음
- 게다가 이렇게 분해한 기능들은 구조적 프로그래밍의 제한된 제어 구조를 이용하여 표현할 수 있음
- 이를 토대로 구조적 분석이나 구조적 설계가 인기를 끌었고, 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있었고, 더 나아가 모듈과 컴포넌트는 입증할 수 있는 아주 작은 기능들로 세분화할 수 있음
[ 테스트 ]
- 다익스트라는 “테스트는 버그가 있음을 보여줄 분, 버그가 없음을 보여줄 수는 없다"고 말한 적이 있음
- 다시 말해 프로그램이 잘못되었음은 증명 가능하지만, 프로그램이 맞다고는 증명할 수 없음
- 테스트에 충분한 노력을 들였다면 테스트가 보장할 수 있는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있을 뿐임
[ 결론 ]
- 구조적 프로그래밍이 오늘날까지 가치 있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 능력 때문임
- 또한 흔히 현대적 언어가 아무런 제약이 없는 goto를 지원하지 않는 이유이기도 함
- 뿐만 아니라 아키텍처 관점에서는 기능적 분해를 최고의 실천법 중 하나로 여기는 이유이기도 함
- 가장 작은 기능에서부터 가장 큰 컴포넌트에 이르기까지 모든 수준에서 소프트웨어는 반증 가능성에 의해 주도됨
- 소프트웨어 아키텍터는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주히 노력해야 하며, 이를 위해 구조적 프로그래밍과 유사한 제한적인 규칙들을 받아들여 활용해야 함
반증 가능성은 결국 testability이다. 반증 가능성이 높지 않다는 것은 테스트하기 어렵다는 것이고, 이는 테스트는 그만큼 중요하다는 것이다. 좋은 아키텍처는 테스트하기 쉬워야 한다는 부분을 내포하고 있다.
5장. 구조적 프로그래밍
[ 서론 ]
- 좋은 아키텍처를 만드는 일은 객체 지향 설계 원칙을 이해하고 응용하는데서 출발함. 그렇다면 OO(Object-Oriented)란 무엇인가?
- “데이터와 함수의 조합”
- 대체로 이런 방식으로 많이 설명되지만 그다지 만족스러운 대답은 아님
- o.f()가 왠지 f(o)가 다르다는 의미를 내포하기 때문인데, OO의 발명 이전부터 프로그래머는 데이터를 함수에 전달해왔음
- “실제 세계를 모델링하는 새로운 방법"
- 이는 얼버무리는 수준에 지나지 않음. “실제 세계를 모델링한다”는 것이 무엇을 의미하며, 왜 우리는 그 방향을 추구해야 하는가?
- 이 답변의 의도는 OO가 현실 세계와 의미적으로 가깝기 때문에 OO를 사용하면 소프트웨어를 조금 더 쉽게 이해할 수 있다는 데 있는듯 하지만 이 의도조차도 불분명하며, 그 정의가 너무 모호함
- "캡슐화, 상속, 다형성을 기반으로 설명"
- OO의 본질을 설명하기 위해 3가지(캡슐화, 상속, 다형성)에 기대기도 함
- 이들은 OO가 이 세 가지 개념을 적절하게 조합한 것이거나, OO 언어는 최소한 3가지 요소를 반드시 지원해야 한다고 함
여기서 o.f()는 f(o)와 같이 때문에 데이터와 함수의 조합으로 객체 지향을 설명하는 것이 부적절하다고 얘기한다. 앞서 살펴보았듯 객체라는 것도 결국 함수를 힙으로 옮긴 것(함수를 기반으로 만들어진 것)일 뿐이므로 사실상 차이가 없다는 뜻인 것 같다.
[ 캡슐화 ]
- 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 OO 언어가 제공하기 때문에 OO를 정의하는 요소 중 하나로 캡슐화가 언급됨
- 데이터와 함수가 응집력 있게 구성된 집단에서 데이터는 구분선 바깥에서 은닉되고, 일부 함수만이 외부에 노출됨
- 하지만 이는 OO가 아닌 언어에서도 충분히 가능하며, OO 언어에서 오히려 깨짐
- 실제로 많은 OO 언어가 캡슐화를 강제하지 않고, 단지 프로그래머가 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 함
실제로 OO 언어는 캡슐화를 강제하지 않는다. 다만 개발자가 이를 잘 사용할 것이라는 믿음 아래 진행된다. 하지만 우리의 코드를 생각해보면 public이 남용되는데, 과연 캡슐화가 OO의 핵심이라고 얘기할 수 있을까? 오히려 책에서 주어지는 예시 코드를 보면 C언어에서 캡슐화가 훨씬 잘 지켜짐을 확인할 수 있다.
[ 상속 ]
- OO 언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 확실히 제공했음
- 하지만 상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과함
- 사실상 OO 언어가 있기 전에도 이를 구현하는 것은 가능했음. 물론 상속을 흉내내는 것이라 상속만큼 편리한 방식은 아님
- 그러므로 캡슐화에 대해서는 OO에 점수를 줄 수 없고, 상속에 대해서만 0.5점 정도 줄 수 있지만 이는 그저 그런 점수임
[ 다형성 ]
- 다형성 역시도 함수를 가리키는 포인터를 응용한 것일 뿐이며 OO가 새롭게 만든 것은 전혀 없음
- 대신 함수 포인터에 대한 직접적인 사용을 없애주고 실수할 위험을 줄임으로써, 다형성을 좀 더 안전하고 편리하게 사용할 수 있게 해줌
- OO 언어에서 다형성은 쉽게 사용 가능하므로 OO는 제어흐름을 간접적으로 전환하는 규칙을 부과한다고 결론지을 수 있음
"제어흐름을 간접적으로 전환하는 규칙을 부과"는 함수의 포인터를 사용해 다형성을 사용하지 않는 것임을 파악할 수 있다.
[ 다형성이 가진 힘 ]
- 기존에는 함수를 가리키는 포인터를 사용하면 위험을 수반하기 때문에 이를 적용하지 않았었음
- 하지만 OO의 등장으로 다형성을 손쉽게 사용할 수 있게 되면서 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되었음
플러그인 아키텍처란 인터페이스나 추상화된 객체에 의존함으로써 손쉽게 구현체를 바꿀 수 있는 아키텍처로 책의 뒷장에서 다시 한번 설명이 나온다.
[ 의존성 역전 ]
- 다형성을 안전하고 편리하게 적용할 수 있는 메커니즘이 등장하기 이전
- 소스 코드 의존성의 방향은 반드시 제어 흐름을 따르게 됨
- main 함수가 고수준 함수를 호출하고, 고수준은 중간 수준 함수를, 중간 수준 함수는 저수준 함수를 호출함
- 이러한 제약 조건으로 인해 소프트웨어 아키텍트에게 남은 선택지는 별로 없었음
- 제어흐름은 시스템의 행위에 따라 결정되며, 소스코드 의존성은 제어흐름에 따라 결정됨
- 다형성 메커니즘이 등장한 이후
- 인터페이스를 통해 소스 코드 의존성이 제어 흐름과 반대가 될 수 있음. 이는 의존성 역전이라고 불림
- OO 언어가 다형성을 안전하고 편리하게 제공한다는 사실은 소스 코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이기도 함
- 즉, 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대해 방향을 결정하는 절대적인 권한을 갖게 됨
- 즉, 소스 코드 의존성이 제어흐름의 방향과 일치되도록 제한되지 않음. 호출하는 모듈이든 호출받는 모듈이든 원하는 방향으로 소스코드 의존성을 설정할 수 있음. 이것이 바로 OO가 제공하는 힘이며, OO가 지향하는 것
- 배포 독립성과 개발 독립성
- 비즈니스 로직, UI, DB는 배포 가능한 단위로 컴파일 가능하며, 비즈니스 로직은 UI와 DB 컴포넌트에 의존하지 않음
- 컴포넌트는 개별적이며 독립적으로 배포 가능함.
- 특정 컴포넌트의 소스 코드가 변경되면 해당 코드가 포함된 컴포넌트만 다시 배포하면 됨(배포 독립성)
- 시스템의 모듈을 독립적으로 배포할 수 있게 되면 서로 다른 팀에서 각 모듈을 독립적으로 개발할 수 있음(개발 독립성)
기존의 계층형 아키텍처를 기준으로 다형성이 없는 상황을 살펴보도록 하자. 우리는 반드시 컨트롤러가 서비스를, 서비스가 레포지토리를 호출하면서 자연스럽게 데이터베이스가 중심이 될 수 밖에 없다. 하지만 레포지토리에 인터페이스를 만들고, 이를 서비스에 주입한다면 의존성의 방향이 데이터베이스가 아닌 서비스로 향하게 된다. 서비스는 비즈니스 로직과 연관된 계층이므로, 데이터베이스가 아닌 비즈니스 로직이 중심이 될 수 있는 것이다. 또한 협업을 할 때 인터페이스만 정의해둔다면 각각의 컴포넌트를 독립적으로 개발할 수 있다. 더 나아가 모듈까지 분리한다면 독립적으로 배포까지 가능하해진다. 즉, 다형성 덕분에 의존성의 방향을 제어할 수 있게된 것이다.
[ 결론 ]
- OO란 다형성을 이용해 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 권한을 획득할 수 있는 능력임
- OO를 사용하면 아키텍트는 플러그인 아키텍처를 구성할 수 있고, 고수준의 정책을 포함하는 모듈이 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있음
- 저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만고, 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있음
OO란 결국 다형성을 통해 의존성 방향을 제어할 수 있도록 하는 것이라고 설명해준다. 그만큼 의존성 방향은 중요하다.
6장. 구조적 프로그래밍
[ 정수를 제곱하기 ]
- 일반적으로 자바와 같은 프로그램은 가변 변수를 사용하는데, 이는 프로그램 실행 중에 상태가 변할 수 있음을 의미함
- 하지만 함수형 프로그램에서는 가변 변수가 전혀 없음
- 변수가 한번 초기화되면 절대로 변하지 않는다는 것
[ 불변성과 아키텍처 ]
- 경합 조건(race condition), 교착 상태, 동시 갱신 문제 모두 가변 변수로 인해 발생함
- 만약 어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 갱신 문제가 일어나지 않음
- 락이 가변적이지 않다면 교착상태도 일어나지 않음. 즉, 동시성 문제는 가변 변수가 없다면 절대로 생기지 않음
- 쓰레드와 프로세스가 여러 개인 상황에서도 시스템이 강건해야 하므로, 아키텍트라면 동시성 문제에 지대한 관심을 가져야 함
- 불변성은 저장 공간이 무한하고 프로세서의 속도가 무한히 빠르다면 실현 가능하지만 자원이 무한대가 아니라면 일종의 타협이 필요함
인프런에서 영한님의 강의를 듣다 보면 1년에 한 두번씩은 꼭 동시성 문제에 의한 버그가 발생했고, 가장 찾기 힘든 류의 버그라는 얘기를 해주신다. 그러므로 가능한 불변으로 만들어서 애초에 변경 가능성을 차단하도록 하자.
나의 경우에는 클래스 변수, 지역 변수, 파라미터 등 가능한 모든 곳에 final을 선언하고, final을 붙일 수 없는 곳이라면 Setter라도 가능한 절대 허용하지 않는다. 이러한 노력들은 결국 유지보수하는데 도움이 된다.
[ 가변성의 분리 ]
- 가장 중요한 타협 중 하나는 가변 컴포넌트와 불변 컴포넌트를 분리하는 것
- 불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되며, 어떠한 가변 변수도 사용되지 않음
- 불변 컴포넌트는 가변 컴포넌트들과 통신함
- 가변 컴포넌트에서는 트랜잭션 메모리나 재시도 기법 등으로 동시 갱신과 경합 조건 문제로부터 가변 변수를 보호해야 함
- 여러 변수가 상호 의존하는 상황에서는 더 정교한 장치를 사용해야 할 수 있음
- 결국 애플리케이션을 제대로 구조화하려면 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리해야 함
- 그리고 이렇게 분리하려면 가변 변수들을 보호하는 적절한 수단을 동원해 뒷받침해야 함
- 현명한 아키텍트라면 가능한 많은 처리를 불변 컴포넌트로 옮기고, 가변 컴포넌트에서는 가능한 많은 코드를 빼내야 함
[ 이벤트 소싱 ]
- 이벤트 소싱은 상태가 아닌 트랜잭션을 저장하자는 전략이며, 트랜잭션을 저장하면 가변 변수가 하나도 필요없음
- 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리함
- 하지만 시간이 지날수록 트랜잭션 수는 끝없이 증가하므로 필요한 컴퓨팅 자원이 커져 무한한 저장 공간과 무한한 처리 능력이 필요함
- 그러므로 애플리케이션의 생명주기 동안만 문제없이 동작할 정도의 저장 공간과 처리 능력만 사용하는 이벤트 소싱을 활용하면 됨
- 지름길로 매일 자정에 상태를 계산한 후 저장하고, 그 이후 트랜잭션만을 갖고 있게 할 수도 있음
- 이렇게하면 애플리케이션은 CRUD가 아니라 CR만 수행한다. 그래서 동시 업데이트 문제 또한 일어나지 않음
- 저장 공간과 처리 능력이 충분하다면 애플리케이션을 완전히 불변하게 만들 수 있고, 완전한 함수형으로 만들 수 있다.
이벤트 소싱이란 발생한 이벤트를 저장하는 전략이다. 그러면 사용자 삭제/등록도 상태 변경이 아닌 삭제 이벤트, 등록 이벤트로 저장이 된다. 트래픽이 많은 경우에 동시성 문제를 최소화할 수 있는 기법이다. 하지만 책에서 설명한대로 우리의 저장 공간과 처리 능력은 무한하지 않으므로 이벤트 소싱 아키텍처를 구현하려면 중간마다 스냅샷? 갱신? 등을 해줘야 한다.
[ 결론 ]
- 3가지 패러다임 요약
- 구조적 프로그래밍은 제어 흐름의 직접적인 전환에 부과되는 규율
- 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율
- 함수형 프로그래밍은 변수 할당에 부과되는 규율
- 3가지 패러다임은 모두 우리에게서 무언가를 빼앗아갔으며, 코드를 작성하는 형태를 한정시킴. 권한이나 능력을 더해주지 않음
- 지난 시간동안 우리가 배운 것은 해서는 안되는 것들이며, 결국 소프트웨어는 급격히 발전하는 기술이 아님
- 도구는 달라졌고 하드웨어도 변했지만 핵심은 여전히 그대로임
- 소프트웨어는 순차, 분기, 반복, 참조 그 이상 이하도 아님
개발바닥 유튜브에서 향로님이 결이 비슷한 얘기를 해주었는데 NodeJS 진영은 스프링과 가는 방향이 달랐기에, 스프링에서 적극 활용하는 DI 방식이 아닌 꽤나 진보된 프레임워크가 나올 것이라고 기대했다고 한다. 그런데 최근에 NestJS라고 뜨고 있는 백엔드 프레임워크를 보면 스프링 부트의 경량화된 버전으로 느꼈다고 한다. 결국 핵심은 달라지지 않는다는 것을 보여주는 모습이 아닌가 싶다.
위의 내용은 로버트 C 마틴의 클린 아키텍처 책을 읽고 정리한 내용입니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다! 혹시 추가적인 의견 있으면 편하게 댓글 남겨주세요ㅎㅎ
관련 포스팅
- 클린 아키텍처 1부 소개 - 내용 정리 및 요약
- 클린 아키텍처 2부 벽돌부터 시작하기: 프로그래밍 패러다임- 내용 정리 및 요약
- 클린 아키텍처 3부 설계 원칙 - 내용 정리 및 요약
- 클린 아키텍처 4부 컴포넌트 원칙 - 내용 정리 및 요약
- 클린 아키텍처 5부 아키텍처 - 내용 정리 및 요약
- 클린 아키텍처 6부 세부사항 - 내용 정리 및 요약