읽어놓고 항상 정리해야지, 정리해야지 하면서 미루던 책..
이 내용 진짜 진짜 재밌는 파트다. 잠깐 읽으려다가 여기서 충격먹고 날 8시간 동안 의자에 묶어놨던 책. ㅋㅋ
📕 목차
1. 패러다임 개요
2. 구조적 프로그래밍
3. 객체 지향 프로그래밍
4. 함수형 프로그래밍
1. 패러다임 개요
📌 생각할 거리
프로그래밍을 시작하고 이 책을 처음 접하기 전까지 객체 지향이란 그래서 무엇을 이야기하고 싶은 건지를 당췌 알 수가 없었다.
2년 전에 코딩 처음 시작할 때 블로그나 온갖 강의를 봐도, 현실 세계의 객체를 어쩌구 메시징을 어쩌구 그런 추상적인 용어들로 추상적인 개념을 설명하는데 솔직히 "아, 저 인간도 모르는구나"라는 생각밖에 안 들었다.
그나마 함수형 프로그래밍은 이해를 하겠는데, 객체 지향 패러다임만큼은 중요성은 알겠지만 "그래서 그게 뭔데?"라는 질문을 받으면 항상 애매모호하게 돌려 답하곤 했었다.
이 책에서는 프로그래밍 패러다임의 역할을 이렇게 말하고 있다.
프로그래밍 패러다임은 프로그래머에게서 권한을 박탈한다.
어느 패러다임도 새로운 권한을 부여하지 않는다.
즉, 패러다임은 "무엇을 해야한다"가 아니라, "무엇을 하지 말아야" 하는지를 이야기한다.
세 가지 패러다임(구조적 프로그래밍, 객체 지향 프로그래밍, 함수형 프로그래밍)은 각각 우리에게서 goto문, 함수 포인터, 할당문을 앗아간다.
이 이상 우리에게서 가져갈 수 있는 것이 더 남아 있을까? 아마 없을 것이다.
따라서 앞으로도 (적어도 부정적인 의도를 갖는)프로그래밍 패러다임은 딱 세 가지밖에 없을 것이다.
📌 구조적 프로그래밍
💡 구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.
- 무분별한 점프(goto문)는 프로그램 구조에 해롭다.
- if/then/else와 do/while/until과 같은 구조로 대체한다.
📌 객체 지향 프로그래밍
💡 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과한다.
- 놀랍게도 구조적 프로그래밍보다 2년 앞선 1966년에 등장했다.
- 함수 호출 stack frame을 heap으로 옮기면, 함수 호출이 반환된 이후에도 함수에서 선언된 지역 변수가 오랫동안 유지될 수 있다는 발견에서 시작했다.
- 이러한 함수가 클래스의 생성자가 되었다.
- 지역 변수는 인스턴스 변수, 중첩 함수는 메서드가 되었다.
- 함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 다형성이 등장하게 되었다.
📌 함수형 프로그래밍
💡 함수형 프로그래밍은 할당문에 대해 규칙을 부과한다.
- 더 놀랍게도 세 패러다임 중 가장 먼저 만들어졌으며, 컴퓨터 프로그래밍 자체보다 먼저 등장했다.
- 전에 한창 클로저 연구할 때, 람다와 관련된 재밌는 수학적 이야기들을 본 적이 있었다. ㅋㅋ
- 람다 계산법의 베이스는 불변성(immtability)이다.
- 대부분의 함수형 언어가 변수 값을 변경할 수 있는 방법을 제공하지만, 매우 까다로운 조건 하에서 허용한다.
2. 구조적 프로그래밍
📌 유클리드 계층구조
데이크스트라가 초기에 인식한 문제는 모든 프로그램은 설령 단순할지라도 인간의 두뇌로 감당하기에는 너무 많은 세부 사항을 담고 있다는 것이었다.
그래서 데이크스트라는 증명(proof)이라는 수학적 원리를 적용했다.
✒️ 유클리드 계층구조
• def. 공리(axiom)는 증명 없이 참으로 받아들이는 명제
• 공리의 예시: "두 점이 주어졌을 때, 두 점을 지나는 직선이 단 하나 존재한다" → 증명 불가능한 명제
• 정리(theorem): 증명의 과정을 통해 참이라는 것이 밝혀진 명제
• 보조정리(lemma): 정리를 증명하는 데 필요한 부가 정리
• 따름정리(corollary): 정리를 통해 자연스럽게 도출되는 정리
데이크스트라는 프로그램을 증명하는 이러한 계층구조를 만들고자 했다.
위 wiki는 Open University 연구에서 VDM에 관한 사례 연구 내용.
📌 goto문
assembly 레벨의 언어를 다루다보면 goto문이 나오는데, 고수준 언어를 다룰 때는 goto문을 사용하는 것을 지양하라는 이야기를 자주 들어봤을 것이다.
이런 goto문은 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에서 방해가 되기 때문이다.
모듈을 분해할 수 없다면, 합리적 증명을 위한 필수적 기법인 분할 정복 접근법을 사용할 수 없게 된다.
하지만 goto문은 항상 나쁘냐고 묻는다면, 사실상 if, else, break, while 같은 구문도 low level 언어에선 jump라서 그렇지는 않다.
그래서 goto문의 좋은 사용 방식은 다음과 같다.
- 분기: if/then/else
- 반복: do/while
모든 모듈이 이러한 종류의 제어 구조만을 사용한다면, 모든 모듈을 증명 가능한 단위까지 재귀적으로 세분화하는 것이 가능해보였다.
📌 순차 실행(sequential execution)과 제어 구조
뵘과 야코피니는 데이크스트라보다 2년 앞서 모든 프로그램을 순차(sequence), 분기(selection), 반복(iteration)이라는 세 가지 구조만으로 표현 가능하나는 사실을 증명했다.
즉, 모듈을 증명 가능하게 하는 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다는 사실이었다.
- 분기는 열거법을 재적용하여 증명했다. 분기를 통한 각 경로를 열거하고, 결과적으로 두 경로가 수학적으로 적절한 경과를 만들어낸다면, 증명을 신뢰할 수 있게 된다.
- 반복은 귀납법을 사용하여 증명했다. 1과 N의 경우가 올바를 때, N+1의 경우도 올바름을 증명하는 방식이었다.
- 심지어 반복의 시작 조건과 종료 조건도 열거법을 통해 증명했다.
위 논문을 이야기하는 건지는 모르겠는데..아무리 뒤져봐도 잘 안 나온다.
여튼 읽어보면 모기랑 코끼리로 은유를 재밌게 해놔서 읽어볼만 하다.
📌 기능적 분해
결국 컴퓨터 언어가 진화하면서 goto 문장을 포함하지 않게 되었다.
제어 흐름을 제약 없이 직접 전환할 수 있는 선택권 자체를 언어에서 제공하지 않기 때문이다.
break와 예외가 goto문과 유사하긴 하지만, 아무 제약 없이 직접 전환 가능하던 과거와는 상당히 다르다.
심지어 goto 키워드를 지원하는 언어조차도 목적지 범위를 현재 함수 안으로 한정시키는 편이다.
여튼 간에 구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 되었고, 이는 결국 모듈을 기능적으로 분해할 수 있음을 의미했다.
거대한 문제 기술서 → 고수준의 기능 → 저수준의 함수
이들 기법을 통해 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있었고, 더 나아가 모듈과 컴포넌트는 입증 가능한 아주 작은 기능들로 세분화할 수 있다.
📌 수학적 증명이 아닌 과학적 증명
애석하게도 데이크스트라의 바람과 달리, 프로그램 관점에서 정리에 대한 유클리드 계층구조는 끝내 만들어지지 않았다.
대신 엄격한 수학적 증명 대신, 과학적 방법을 통해 무언가 올바른지를 입증하게 되었다.
- 만류인력의 법칙은 시연 가능하고, 높은 정확도로 측정할 수는 있지만 수학적으로 증명할 수는 없다.
- 이는 실험을 아무리 많이 수행하고, 경험적 증거를 아무리 많이 수집한다고 해서 언젠가는 다른 실험을 통해 거짓임이 밝혀질 가능성이 열려있기 때문이다.
즉, 과학은 서술된 내용이 사실임을 증명하는 방식이 아니다.
그저 수많은 사례를 들고 와도 반례를 들 수 없는 서술이 존재한다면 목표에 부함할 만큼은 참이라고 볼 뿐이다.
수학은 증명 가능한 서술이 참임을 입증하지만, 과학은 증명 가능한 서술이 거짓임을 입증한다.
✒️ 테스트의 증명법
테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다.
다시 말해 테스트를 통해 프로그램이 잘못되었음을 증명할 수는 있어도, 맞다고는 증명할 수 없다.
그저 테스트에 충분한 노력을 들였다면 적어도 목표에 부합할 만큼은 프로그램이 충분히 참이라고 여길 수 있게 해주는 것이다.
하지만 이처럼 부정확함에 대한 증명은 입증 가능한 프로그램이라는 전제가 필요하다.
제약 없는 goto문을 사용하는 등의 이유로 입증이 불가능한 프로그램은 테스트를 아무리 많이 수행해도 절대로 올바르다고 볼 수 없다.
구조적 프로그래밍은 프로그램을 증명 가능한 세부 기능 집합으로 재귀적으로 분해할 것을 강요한다.
그렇기에 테스트를 통해 증명 가능한 세부 기능들이 거짓인지를 증명하려는 시도가 올바르다고 볼 수 있는 것이다.
즉, 구조적 프로그래밍이 오늘날까지 가치 있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있다는 능력때문이다.
3. 객체 지향 프로그래밍
📌 객체 지향이 뭔데?
처음 OOP를 배우면 가장 짜증나는 부분.
객체 지향이 뭐냐고 물으면 하나같이 "실제 세계를 모델링한다"라고 하는데, 대체 거기에 어떤 가치가 존재하는지에 대해서는 아무도 알려주지 않는다.
그래서 내가 그걸 왜 해야 하냐고?
- 데이터와 함수의 조합?
- o.f()가 f(o)와 다르다는 의미를 내포하는데, 터무니 없는 말이다.
- 함수 호출 stack frame을 heap으로 옮기고 OO를 발명한 1966년보다 훨씬 이전부터 프로그래머는 데이터 구조를 함수에 전달해 왔다.
- 실제 세계를 모델링하는 새로운 방법?
- 기껏해야 얼버무리는 수준에 지나지 않는다.
- 마치 OO는 현실 세계와 의미적으로 가까우므로, SW를 좀 더 이해할 수 있다는 듯이 말한다.
- 하지만 그 의도조차 불분명하고, 정의가 너무 모호하다. 설명하는 사람조차 본인이 무슨 말을 하는지 모른다.
- OOP의 특징?
- 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism)을 무슨 주문처럼 사용한다.
- OO는 이 세 가지 개념을 적절하게 조합하거나, 또는 최소한 세 가지 요소를 반드시 지원해야 한다고 한다. (과연 그럴까?)
📌 캡슐화(encapsulation)?
캡슐화는 OO언어가 아니라 오히려 C언어에서 완벽하게 가능하다.
// point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance (strcut Point *p1, struct Point *p2);
// point.c
#include "point.h"
#include <stdlib.h>
#include <math.h>
struct Point {
double x,y;
};
struct Point* makepoint(double x, double y) {
struct Point* p = malloc(sizeof(struct Point));
p->x = x;
p->y = y;
return p;
}
double distance(struct Point* p1, struct Point* p2) {
double dx = p1->x - p2->x;
double dy = p1->y - p2->y;
return sqrt(dx*dy + dy*dy);
}
- point.h를 사용하는 측에서 struct Point의 멤버에 접근할 방법이 전혀 없다.
- makePoint() 함수와 distance() 함수를 호출할 수는 있어도, Point 구조체 데이터 구조와 함수가 어떻게 구현되었는지는 전혀 알 수 없다.
- 프로그램 사용자는 구현 파일에 작성된 항목에 대해서는 어떠한 방법으로도 접근할 수 없었다.
이 완벽한 캡슐화의 형태가 C++이라는 OO에서 어떻게 깨졌는지 알아보자.
// point.h
class Point {
public:
Point(double x, double y);
double distance(const Point& p) const;
private:
double x;
double y;
}
// point.cc
#include "point.h"
#include <math.h>
Point::Point(double x, double y) : x(x), y(y) {}
double Point::distance(const Point& p) const {
double dx = x - p.x;
double dy = y - p.y;
return sqrt(dx*dx + dy*dy);
}
- C++ 컴파일러는 클래스 인스턴스 크기를 알 수 있어야 한다는 기술적 한계로 멤버 변수를 클래스의 헤더 파일에 선언할 것을 요구했다.
- point.h 헤더 파일을 사용하는 측에서는 멤버 변수인 x와 y를 알게 된다.
- 물론 멤버 변수 접근은 컴파일러가 막겠지만, 사용자가 멤버 변수가 존재한다는 사실 자체를 알게 된다.
- 멤버 변수의 이름이 바뀌면 point.cc 파일은 다시 컴파일해야 하므로 캡슐화가 깨진 것이다.
- public, private, protected 키워드는 컴파일러가 헤더 파일에서 멤버 변수를 볼 수 있어야 했기 때문에 조치한 임시방편일 뿐이다.
- 자바와 C#은 헤더와 구현체를 분리하는 방식을 모두 버렸고, 이로 인해 캡슐화는 더욱 심하게 훼손되었다.
실제로 많은 OO언어가 캡슐화를 거의 강제하지 않으며, 그저 프로그래머가 충분히 올바르게 행동함으로써 캡슐화된 데이터를 우회해서 사용하지 않을 것이라는 믿음을 기반으로 한다.
하지만 OO를 제공하는 언어들이 되려 C언어에서 누렸던 완벽한 캡슐화를 약화시켜온 것은 사실이다.
📌 상속(inheritance)?
상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과하다.
OO 언어가 확실히 편리함을 제공하긴 했지만, 그 이전에도 대다수 언어들은 언어의 도움 없이도 구현할 수 있었다.
// namePoint.h
struct NamedPoint;
struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);
// namedPoint.c
#include "namedPoint.h"
#include <stdlib.h>
struct NamedPoint {
double x, y;
char* name;
};
struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
p->x = x;
p->y = y;
p->name = name;
return p;
}
void setName(struct NamedPoint* np, char* name) {
np->name = name;
}
char* getName(struct NamedPoint* np) {
return np->name;
}
// main.c
#include "point.h"
#include "namedPoint.h"
#include <stdio.h>
int main(int ac, char** av) {
struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
struct NamedPoint* upperRight = makeNamedPoint(1.0, 1.0, "upperRight");
printf("distance=%f\n", distance((struct Point*) origin, (struct Point*) upperRight));
}
- main 프로그램을 보면 NamedPoint 데이터 구조가 마치 Point 데이터 구조로부터 파생된 구조인 것처럼 동작한다.
- NamedPoint에 선언된 두 변수의 순서가 Point와 동일하기 때문이다.
- 눈속임처럼 보이는 이 방식은 실제로 C++에서 단일 상속을 구현할 때 사용한 방법이다.
물론 가능하긴 했지만, OO에서 훨씬 간편함을 제공해준 것은 사실이다.
- 다중 상속은 위와 같은 방법으로 구현할 수 없다.
- OO 언어에서는 NamedPoint 인자를 Point 타입으로 강제로 형변환하지 않아도 업캐스팅이 암묵적으로 수행된다.
하지만 이것이 OOP의 진정한 가치라고 볼 수는 없다.
📌 다형성(polymorphism)?
OO 언어 이전에도 당연히 다형성을 표현할 수는 있었다.
#include <stdio.h>
void copy() {
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
- getchar() 함수는 STDIN에서 문자를 읽는다. 그러면 STDIN은 어떤 장치인가?
- putchar() 함수는 STDOUT으로 문자를 쓴다. 그러면 STDOUT은 어떤 장치인가?
이는 굉장히 다형적이다.
다만 자바 형식의 인터페이스가 존재하고 장치별로 구현체가 존재하는 것과 달리,
C 프로그램은 유닉스 운영체제의 모든 입출력 장치 드라이버가 다섯 가지 표준 함수(open, close, read, write, seek)를 제공할 것을 요구한다.
FILE 데이터 구조는 이들 다섯 함수를 가리키는 포인터들을 포함하고, 콘솔용 입출력 드라이버에서는 이들 함수를 정의하여 FILE 데이터 구조를 함수에 대한 주소와 함께 로드할 것이다.
이제 STDIN을 FILE*로 선언하면, STDIN은 콘솔 데이터 구조를 가리키므로 다음과 같이 구현할 수 있다.
extern struct FILE* STDIN;
int getchar() {
return STDIN -> read();
}
즉, OO 언어는 다형성을 좀 더 안전하고 편리하게 사용할 수 있게 했을 뿐, 새롭게 제공하지는 못 했다.
하지만 OO 언어가 제공해주는 다형성에는 다음과 같은 차이가 있다.
- 함수에 대한 포인터를 직접 사용하여 다형적 행위를 만드는 방식은 굉장히 위험하다.
- 프로그래머가 특정 관례를 수동으로 따라야 하며, 포인터를 통해 모든 함수를 호출하는 관례를 준수해야 한다.
- 만약 프로그래머가 이 사실을 망각하면 버그가 발생하고, 이러한 버그를 찾아내고 없애기가 매우 힘들다.
하지만 OO 언어에서는 이러한 관례를 없애주며, 다형성은 대수롭지 않은 일이 된다.
💡 OO는 제어흐름을 간접적으로 전환하는 규칙을 부과한다.
📌 다형성과 의존성 역전(dependency inversion)
디자인 패턴을 공부해보면 아는 내용이겠지만, 특정 구현체에 의존한다는 것은 매우 위험한 생각이다.
플러그인 아키텍처(plugin architecture)는 특정 구현체로 부터 독립성을 지원하여, 구현체가 변경되어도 기존 코드가 수정될 일이 없음을 보장한다.
하지만 대다수 프로그래머는 함수를 가리키는 포인터를 사용하는 방식의 리스크를 피하기 위해, 직접 작성하는 프로그램에서는 이러한 개념을 확장해서 적용하지는 않았다.
그러나 OO의 등장으로 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되었다.
- 과거 제어흐름은 시스템의 행위에 따라 결정되며, 소스 코드 의존성은 제어 흐름에 따라 결정된다.
- 하지만 다형성이 끼어들면 비록 인터페이스가 런타임에 존재하지 않음에도 불구하고, 소스 코드 의존성이 제어 흐름과 반대로 뒤집히게 된다. ⇒ 의존성 역전
- 여기가 굉장히 중요하다. OO가 다형성을 안전하고 편리하게 제공한다는 것은 소스 코드 의존성을 어디서든지 역전시킬 수 있다는 의미를 내포한다.
💡 OO 언어로 개발된 시스템은 소스 코드 의존성 전부에 대해 방향성을 제어할 절대적 권한을 갖는다.
- 업무 규칙이 DB와 UI에 의존하지 않고, 반대로 DB와 UI가 업무 규칙에 의존하게 만들 수 있다.
- 즉, UI와 DB가 업무 규칙의 플러그인이 된다.
- 업무 규칙의 소스 코드에서는 UI나 DB를 호출하지 않는다.
- 업무 규칙, UI, DB로 분리된 컴포넌트 또는 배포 가능한 단위로 컴파일 가능해지고, 업무 규칙을 독립적으로 배포할 수 있다. ⇒ 배포 독립성(independent deployability)
- UI나 DB에서 발생한 변경 사항이 업무 규칙에 일절 영향을 미치지 않게 된다. 따라서 각자 팀에서 각 모듈을 독립적으로 개발할 수 있다. ⇒ 개발 독립성(independent developability)
📌 소프트웨어 아키텍처 관점에서의 OOP란
💡 OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적 제어 권한을 획득할 수 있는 능력이다.
- 아키텍트는 플러그인 아키텍처를 손쉽게 구성할 수 있다.
- 고수준 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장한다.
- 저수준의 세부사항은 중요도가 낮은 플로그인 모듈로 만들 수 있고, 고수준의 정책을 포함하는 모듈과 독립적을 개발하고 배포할 수 있다.
4. 함수형 프로그래밍
📌 가변 변수(mutable variable)의 유무
다음은 똑같은 동작을 반복문과 클로저로 다르게 표현한 스니펫이다.
public class Main{
public static void main(String args[]) {
for (int i=0; i<25; ++i)
System.out.println(i*i);
}
}
public class Main {
public static void main(String args[]) {
IntStream.range(0, 25).forEach(i -> System.out.println(i*i));
}
}
- 기존의 방식은 가변 변수 i를 사용하여, 변수 값을 수정한다.
- 클로저에서는 i가 한 번 초기화되면 절대로 변하지 않는다.
여기서 주목할만한 점은 이것이다.
함수형 언어에서 변수는 변경되지 않는 불변의 상태를 갖는다.
람다와 클로저에 대한 고찰은 지난 번에 포스팅을 한 적이 있으므로 참고용.
📌 불변성과 아키첵처
- 경합(race) 조건, 교착 상태(deadlock) 조건, 동시 업데이트(concurrent update) 문제는 모두 가변 변수로 인해 발생한다.
- 어떠한 변수도 갱신되지 않는다면 경합 조건과 동시 업데이트 문제는 발생하지 않는다.
- 락(lock)이 가변적이지 않다면 교착 상태조차 발생하지 않는다.
즉, 우리가 동시성 애플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 절대로 생기지 않는다.
하지만 불변성이 정말로 실현 가능할까?
여기엔 일종의 타협이 필요하다.
📌 가변성의 분리
여긴 좀 내 멋대로 해석한 내용.
현실적으로 프로그램 전체를 불변식으로 구현하는 것은 불가능하거나 굉장히 어렵다고 생각한다.
따라서 책에서는 내부 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 작업에 대해 이야기 한다.
- 불변 컴포넌트는 순수하게 함수형 방식으로만 작업을 처리한다.
- 어떤 가변 변수도 사용되지 않는다.
- 변수의 상태를 변경할 수 있는(순수 함수형 컴포넌트가 아닌) 하나 이상의 다른 컴포넌트와 서로 통신한다.
그리고 트랜잭션 메모리와 같은 실천법으로 동시성 문제로부터 가변 변수를 보호한다.
💡 가능한 많은 처리를 불변 컴포넌트로 옮기고, 가변 컴포넌트에서는 가능한 많은 코드를 빼내야 한다.
📌 이벤트 소싱
와, 블록체인 공부하면서 봤던 내용 나오니까 좀 재밌었다 ㅎㅎ
- 최근엔 저장 공간과 처리 능력 한계로 인한 문제는 거의 사라지고 있다.
- 은행은 계좌 잔고를 변경하는 대신 트랜잭션 자체를 저장하고, 상태가 필요해지면 모든 트랜잭션을 처리한다.
- 모든 트랜잭션을 단순히 더하기만 하므로 가변 변수가 하나도 필요 없다.
- 은행은 보통 자정 쯤에 서버 닫아놓고 상태 정보를 모두 갱신한다.
- 더 이상 데이터 저장소에 CRUD가 아닌, CR만 발생하므로 동시성 업데이트 문제 또한 발생하지 않는다.
- 즉, 저장 공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 따르도록 만들 수 있고, 완전한 함수형으로 만들 수 있게 된다.