참고서적: Mastering Functional Programming

함수형 프로그래밍은 imperative(명령)한 프로그래밍과 차이를 보인다. Lambda, Map, Filter 와 같은것을 쓴다고 해서 함수형 프로그래밍이 아니다. 이것들을 쓰지 않더라도 함수형 프로그래밍을 할 수 있으며, 이것을 쓴다고 해도 함수형 프로그래밍이 아닐 수 있다. 사실 MapFilter는 함수형 프로그래밍에서 자주 쓰이는 패턴들을 묶은것 뿐이다. 함수형 프로그래밍의 핵심은 "하나의 함수는 외부의 영향없이 동일한 결과를 뽑아내야 한다" 이다.

일반적인 명령형 프로그래밍은 어떤 객체의 행동을 나타내기 위해 쓰인다. 특히 OOP에서는 객체의 값을 바꾸면서 (side-effect) 작업을 수행한다. 함수형 프로그래밍은 그렇지 않다. 함수형 프로그래밍은 purity와 referential transperncy를 중요하기 때문이다. Purity는 "side effect는 없어야 한다" 라는 것이며, referential trasnperency는 "함수의 결과값을 함수 호출로 바꿔도 문제가 없다"라는 것이다.

referential trasnperency는 바로 와닫지 않을 수 있다. 조금 풀어쓴다면, 아래와 같은 코드가 있다고 하자

class A:
	count = 1
	def work(self):
		self.count += 1
		return self.count

A = A()
A.work() # return: 2
A.work() # return: 3

이런 코드는 참조무결성이 만족되지 않는다. 리턴값인 2을 work()로 대체했을때 다른 결과가 나올수 있기 때문이다. 결과값을 특정 함수의 호출로 바꾸려면 "함수의 연산은 함수 내에서 모두 이루어져야 한다"는것을 깔고 가야한다. 함수가 자신의 코드 바깥에 있는 무언가를 계산에 이용한다면 (mutable한 변수) 매번 똑같은 결과를 낸다고 보장받을 수 없다.

정리해 보자면 함수형 프로그래밍에서 함수는 "자신의 바깥쪽에 있는 무언가를 절대 접근(Read/Write 모두) 하지 않고, 자신의 코드만으로 모든걸 계산"하는 것이다. 그러면 "상태가 필요한 경우가 있을수 있다. 이것들을 어떻게 해야하냐?" 와 같은 질문을 던질 수 있다. 예를들어, 교실이라는 객체가 있고, 학생이 들어오고 나간것을 처리하는 프로그램을 만들어야 한다고 생각해 보자

class SchoolClass:
	students = []
	def come(self, student):
		self.students.push(student)
	def out(self, student)
		self.students.remove(student)

명령형 프로그래밍을 이용한다면 위와같은 코드를 만들 수 있을것이다. self.students는 함수의 바깥에서 작성되어 있으며, 함수에 종속되지 않는다. 이런것들이 "side-effect" 함수라고 불린다. 이것을 함수형으로 어떻게 표현할 수 있을까? comeoutself.students에 맞물려서 작동하며 끊을수 없게 보인다. 그러나 이것을 끊어야 한다. 함수형 프로그래밍에서는 side-effect 사용을 거부하기 때문이다.

함수형 프로그래밍에서는 method를 Object의 행동으로 보지 않는다. 대신 모든것을 데이터의 관점으로 본다. 데이터의 관점으로 SchoolClass를 하나의 상태 데이터로 볼 수 있다. 매번 come(), out()이 실행 될때마다 새로운 SchoolClass 를 만들어 내는것이다. 그렇게 되면 함수내에서 모든것을 처리할 수 있게 된다.

class SchoolClass:
	students = []

def come(sc, student) -> SchoolClass:
	sc.push(student)
	return sc
def out(sc, student) -> SchoolClass:
	sc.remove(student)
	return sc

위와 같은 형태로 바꾸면, 모든 데이터는 함수 내에서 처리하게 된다. 입력값에 따라 모든것을 계산할 수 있게 되며, 같은 입력을 받으면 같은 출력을 내기 때문에 "함수의 결과값을 역으로 함수의 호출로 대체"할 수 있게 된다 (referential transperency)

<aside> 💡 매번 새로운 상태를 만들어낸다면 프로그램의 퍼포먼스가 떨어지지 않을까? 걱정이 될 수 있다. 실제로 OOP관점에서 데이터를 직접 수정하는것이 성능 관점에서 더 좋을것이다. 하지만, 함수형 프로그래밍으로 만들면 모든것을 immutable한 것으로 표현할 수 있다. 그 덕에 상태에 대해 신경을 안써도 아무 문제 없이 동작하도록 만들어 준다. 멀티쓰레드 환경에선 lock을 사용하지 않게 만들어주어 성능적으로 이점이 생길 수 있다. 프로그래밍은 단순히 "실제 세상의 문제를 코드로 로직으로 풀어내는것"이 끝이 아니다. "프로그래밍 코드상의 문제"를 함께 풀어내야 하는것 또한 중요하다. 초보 프로그래머의 경우 현실 세계의 문제를 풀어내는것에만 열중하여 코드가 얼마나 더럽게 되든 상관없이 코딩하는 경우가 종종 있다. 그 결과로 흔히 말하는 스파게티 코드가 나온다. 콜백 지옥에 빠질수도 있다. if문이 도배되어 이게 어디서 분기되고 어디서 처리되는지 알기 어렵게 되기도 한다. 성능이 중요하지 않다는것이 절대 아니다. 하지만 프로그래밍 현실을 전혀 생각하지 않고 성능에만 몰두하면 문제가 발생한다. 그렇기에 성능과 프로그래밍의 현실을 적절히 타협하여 코드를 작성하는것이 중요하다. 그런점에서 함수형 프로그래밍은 충분한 대안이 된다. 모든 상태가 immutable하기에 이것저것 확인해야 하는것이 줄어든다. 코드의 복잡도가 떨어지며, 놓치기 쉬운 문제들(NPE, Exception Throw)이 적어져서 코드 전반의 품질을 올려준다.

</aside>

<aside> 💡 소규모 소프트웨어 프로젝트에서는 간단하고 표현이 풍부한 코드로 말끔하게 시스템을 작성 할 수 있다. 하지만 프로젝트가 커짐에 따라 시스템은 매우 복잡하고 이해하기 어려워진다. 복잡도는 같은 시스템에서 작업해야 하는 모든 사람의 진행을 느리게 하고, 유지보수 비용을 증가시킨다. 복잡도 수렁에 빠진 소프트웨어를 **커다란 진흙 덩어리(big ball of mud)**라고 하는 경우까지 있다.

</aside>

우리는 질좋은 소프트웨어를 제때, 주어진 예산 안에서 구축해야 한다. 아키텍처는 장기적인 투자이다. 만약 아키텍처가 세금 공제처럼 즉각적이고 실질적인 이득을 주지 못하거나, 비용과 시간이 남아도는 상태가 아니라면 돈을 지불하는 사람들은 이를 쉽게 간과해 버린다. 대부분의 경우 고객들은 내일 당장 쓸 수 있는 것을 원한다. 때론, 개발 프로세스를 관리하고 통제하는 사람들도 아키텍처를 중요하지 않은 것으로 치부해 버리기도 한다. 프로그래머들이 좋은 아키텍처를 구축하기 위해 열심히 노력해도 인정을 받지 못하고, 관리자들도 이에 대한 비용을 지불하기를 꺼려한다면 치명적인 악순환의 고리가 탄생한다.

프로그램의 속도는 "프로젝트의 한 부분"일 뿐이다. 팀의 전체적인 속도가 더욱 더 중요하다. 한 부분을 잡는다고 팀의 진행속도를 다 깨부순다면 아무 의미가 없다.

함수형 프로그래밍에서는 input만 같으면 함수의 결과값이 항상 동일해야 한다는것을 알게 되었다. 그 점을 이용하면 "고차함수"를 사용할 수 있다. 고차 함수는 "argument(input)로 함수를 받는 함수" 이다. 모든것을 함수와 데이터로 풀어내는 구조상 편하게 프로그래밍을 하려면 어쩔수 없이 끌고와야 하는 개념이다. 어짜피 모든 함수의 결과값은 함수 내에서 계산된다. 함수를 넘겨줘도 다른곳에 걸리는것이 없으니 전혀 문제가 없다.

<aside> 💡 요즈음에는 캡쳐링을 통해서 참조무결성이 부족한 함수들도 끌고올 수 있기는 하다.

</aside>