코틀린 언어 정리 3-1

함수형 프로그래밍

개요

함수형 프로그래밍에 대한 가장 간결한 설명/원칙은 아마도 “수학적으로 프로그래밍 하는 것" 일 것입니다. 그런데 함수형 프로그래밍을 처음 접하는 사람이거나 수학에 대해 깊게 생각해 보지 않은 사람의 입장에서 보면 이런 설명으로는, 함수형 프로그래밍이 구체적으로 어떤 것인지 상상하기 어려울 수 있습니다. 그래서 경험을 기준으로 좀 더 구체적인 용어의 설명 들로 바꿔 보았습니다.(특별한 수식어가 없는 한, 이후 설명에서의 함수는 대부분 프로그래밍 언어에서의 함수를 말합니다.)

 

  • 어떤 범위 내의 문제 해결을 위해 명시적으로 제공한 정보 이외에 범위 바깥의 다른 가변 정보를 이용하지 않고 처리합니다.
  • 함수 내의 로직이 함수 외부의 가변 요인으로부터 영향을 받지 않도록 구현합니다.
  • 함수에 매개변수로 입력되는 인수 이외에 함수 외부의 다른 변수들을 함수 구현에 사용하지 않습니다.

 

개발을 어느 정도 해 본 경우 공감이 가는 내용들일 것입니다. 대부분 알고 있을만한 내용들 일지도 모릅니다. 이 설명들을 보고 다시 수학적이라는 의미, 수학에서의 함수와 프로그래밍에서의 함수의 차이에 대해 생각해 보면 개념을 이해 하는데 도움이 될 것 같습니다. 

 

수학은 완전한 학문이라고 합니다.(저도 수학과는 아니지만 그렇게 생각합니다.) 오류가 없습니다. 수학에서의 함수는 함수 내에서 외부 변수를 사용할 수 없습니다. 입력된 매개변수와 상수로만 식을 정의합니다. 하지만 프로그래밍 언어에서의 함수는 다릅니다. 좀 더 개방적입니다. 함수 내부에서, 함수의 매개변수 이외에 함수 외부의 변수들도 원하는 만큼 참조할 수 있습니다. 수학에서의 함수는 매개변수로 입력된 값 이외의 다른 변수에 의해 영향 받을 일이 없으나 프로그래밍 언어에서의 함수는 매개변수를 포함하여, 참조한 모든 변수에 의해 영향을 받습니다.(함수형 프로그래밍에서의 함수는 프로그래밍 언어 보다는 수학에서의 함수를 의미한다고 볼 수 있습니다.) 그런데 코드에서 변수를 많이 사용할수록 오류가 발생할 가능성이 기하급수적으로 높아집니다. 반면 코드에서 변수를 사용하는 것을 제한할수록 안정적이게 됩니다. 극단적으로는 변수를 사용하지 않으면 그 자체로 완전한 코드가 됩니다. 그런데 프로그램이 어떤 기능을 제공한다는 것은 외부에서 입력값을 받고 그에 대한 결과를 내는 것으로 볼 수 있습니다. 수학에서나 개발에서나 입력값이라는 변수는 필수적인 것입니다. 수학의 함수에서는 입력값을 매개변수로만 받고, 프로그래밍에서의 함수는 입력값을 매개변수 + 접근 가능한 모든 변수로 받습니다.

프로그래밍에서의 함수는 접근 가능한 모든 변수를 입력값으로 사용할 수 있기 때문에 수학의 함수보다 오류가 발생할 가능성이 기하급수적으로 높습니다. 따라서 프로그래밍의 함수에서 오류를 최소화 하려면 수학의 함수처럼 입력값을 매개변수로 제한해야 합니다.

함수형 프로그래밍은, 프로그래밍 언어의 함수를 수학의 함수처럼 입력값을 매개변수로 제한하려고 노력하는데에서 시작한다고 볼 수 있습니다.



함수형 프로그래밍의 한계

함수형 프로그래밍을 하려면 우선 수학의 세계와 프로그래밍의 세계의 차이점에 대해 알아야 합니다. 수학은 완전한 이상(관념)의 세계에서 작동 하지만 프로그래밍은 현실에서 작동 합니다. 

구체적으로 말하면, 개발은 물리적 한계를 가진 컴퓨터에 기반하여 작동한다는 것입니다.

 

이 한계에 대해 좀 더 세밀하게 들여다 보겠습니다.

함수형 프로그래밍을 하려면 변수와 상수를 구분해야 합니다. 그런데 수학에서는 변수와 상수의 구분이 절대적이지만 프로그래밍에서는, 극당적인 상황에서 이 구분이 모호해지는 경우가 종종 있습니다. 결론부터 이야기 하면, 수학의 관점에서 엄밀히 보았을 때, 프로그래밍에서는 상수가 없다고 볼 수 있습니다. 예를 들면, 객체 생성 또는 메모리 할당 코드, 메모리 주소값, 매우 큰 숫자 사용, 개발자가 정의한 글로벌 객체, 다른 곳에서 개발한 라이브러리 코드 등은 상수인가에 대해 질문해 보면 됩니다. 객체 생성 또는 메모리 할당 코드는 메모리가 부족한 경우 실패할 수 있고, 매우 큰 숫자는 기본적으로 CPU 또는 사용 언어에서 지원할 수 있는 범위까지만 사용할 수 있습니다. 또 이전에 저장해 놓았던 값을 다시 참조하기 위해 메모리 주소값으로 메모리를 참조하는 코드의 실행 결과(ex. 함수 코드 참조)는, 외부 코드에 의해 메모리 주소가 참조하는 부분이 오염되었을 경우, 상수로 판단할 수 없습니다. 평소에 상수라고 생각하고 당연하게 사용하던 것들도 판단 기준과 상황에 따라 상수가 아닐 수 있습니다. 수학에서는 변수와 상수의 개념이 절대적이지만 프로그래밍 언어에서 변수와 상수의 개념은 상대적입니다.

이런 이유로, 어떤 상황에서는 변수와 상수를 판단하는 기준을 경험과 상식 또는 상호 협의에 의해 결정해야 하고, 이것을 기반으로 일반적인 상황에서 수학적 개념과 최대한 가까워질 수 있도록 프로그래밍을 해야 합니다.



함수형 프로그래밍을 해야 하는 이유

프로그래밍에서의 함수가 수학의 함수에 가까워질수록 오류 발생 가능성이 줄어들기 때문에 안정적인 결과를 위해서는 함수형 프로그래밍을 추구해야 합니다. 그런데 이렇게 개발하는 것은 당장 시간과 비용이 더 들어갈 수 밖에 없습니다. 무엇인가 새로 개발해야 하는 경우, 코드 구현 이전 단계부터, 코드를 구현하는 단계까지 모든 과정에서 이전 보다 더 고도로 논리를 정제 해야 합니다.

또 기존에 구현된, 이미 동작은 하고 있지만 닥치는 대로 너저분하게 구현된 코드들도 미래의 작업을 위해서는 함수들의 조합으로 리팩토링을 해야 합니다.

이렇게 하면 미래에 모두가 편해 지겠지만 이를 수행하기 위해서는 조각나지 않은 큰 덩어리의 집중된 시간과 노력이 들어가게 됩니다. 

반면, 대충 확인한 정보 또는 기존에 알고 있는 정보들에 대해 깊게 생각해 보지 않고 가볍게 코딩하거나, 정보들의 범위를 좁혀 입력 값을 매개변수화 히지 않고 닥치는대로(컴파일 에러가 발생하지 않는 정도로) 외부 변수를 참조하여 당장 빠르게 구현하여 일부 상황에서 동작하는 것만 확인한 후 작업을 종료하고, 이 후 파생되는 수많은 문제를 남에게 떠넘기는 것은 매우 쉬우며 흔하게 발생하는 일입니다.

이런식으로 개발하는 것은 결국 다 같이 공멸의 길로 가는 것입니다. 이런 공멸의 길을 가지 않기 위해, 함수형 프로그래밍을 추구해야 하며 이를 위해서는 개발자 스스로의 선견지명과 양심에 기반한 적극적인 노력이 필요합니다. 또한 개발자를 고용한 사업자도 이를 이해하고 이에 대한 비용을 감내 할 수 있어야 합니다.



효과

함수형 프로그래밍을 하면 다음의 효과들이 나타게 됩니다.

 

부수 효과가 없는 코드

함수는 입력된 매개변수와 함수 내부의 변수만 사용하여 계산합니다. 함수 외부의 변수는 사용하지 않습니다. 따라서 함수가 실행되면 외부의 영향을 받지 않으며, 마찬가지로 함수도 리턴하기 전까지는 함수 외부에 영향을 주지 않습니다. 코드가 실행되는 동안 외부와 단절되므로 부수효과가 없다고 말할 수 있습니다. 즉 함수형 프로그래밍을 하게 되면 부수효과가 없는 코드를 만들게 됩니다.

 

오류가 없는 코드

함수는 외부 변수를 사용하지 않으므로, 함수에 문제가 생기면 함수 내부만 분석하면 됩니다. 

함수 내부의 요소만으로 로직이 구성되어 있으므로 코드를 분석하기가 쉽습니다.

입출력 통로가 단일화 되어 코드의 목적성이 확연하게 드러납니다.

따라서 코드에서 오류를 발견하기 쉬워지고, 이로 인해, 코드에 오류가 줄어들게 됩니다.

 

조금 더 넓은 범위로 설명하면 다음과 같습니다. 

함수는 입력된 값만 사용하므로 고려해야 하는 로직의 범위가 줄어듭니다. 어떤 함수를 분석하면서 함수 이외에 다른 부분을 볼 필요가 거의 없다는 말입니다. 마찬가지로, 코드에 오류가 있을 경우 함수 단위로 범위가 좁혀지므로 오류를 발견하기도 쉬워집니다. 따라서 오류를 더 쉽게 제거할 수 있습니다. 그리고 오류를 제거하다보면 불필요한 코드가 사라지므로 논리가 명확해집니다. 논리가 명확한 것은 그렇지 않은 것보다 말로 표현하기도 쉬워지고, 요약(주제를 정하는 것)하기도 쉬워집니다. 이렇게 요약된 언어로 함수 이름을 정하면 함수 이름과 구현 코드의 간극이 최소화 될 수 있습니다. 따라서 함수 이름을 통해 큰 흐름의 논리를 파악할 때에도 마찬가지로 오류가 줄어듭니다.

 

주의할 점이 있는데, 바로 위에서 논리가 명확한 것은 말로 표현하기 쉬워진다고 언급했습니다. 이는 상대적인 것이지 절대적인 것이 아닙니다. 즉 잘 만들어진 모든 함수가 말로 표현하기 쉬운 것은 아닙니다. 개념이 원래 어렵거나 기존에 없던 새로운 개념을 함수로 구현한 경우 논리가 명확하더라도 이해하기 어려운 것이 당연하고, 말로 설명하기도 어렵습니다. 그런데 구현된 내용에 대해 잘 모르는 비전문가(하지만 어느 정도 논리적이면서 언어 수단이 있다고 자신하는 사람)가 구현된 내용을 잘 모르는 상태에서 개발자의 결과물에 대해 설명을 요청할 때 이 부분을 간과하고 개발자에게 더 쉬운 설명만 계속 요청하는 경우가 있습니다. 이런 경우, 설명을 듣고 이해를 하지 못하는 이유가 자신의 무지 때문인지 개발자의 설명 기술 부족인지 잘 판단할 수 있어야 합니다. 이와 반대의 상황도 있습니다. 매우 쉬운 개념, 일반적인 상식을 함수로 구현한 경우 코드가 함수적이 아니거나 또 매우 복잡하더라도 말로 표현하기는 쉬울 수 있습니다. 이런 경우 구현된 내용을 잘 모르는 비전문가는 매우 간단한 작업으로 착각할 수 있습니다.

정리하면, 설명을 이해하기 어렵다고 함수적이지 않다고 단정 지을수는 없습니다. 이와 반대의 상황, 즉 설명하기 쉬운 모든 함수(로 간주되는 코드)가 잘 만들어진(수학적 함수와 가까운) 함수라고 단정지을수도 없습니다.

 

결국 함수가 수학적인가 아닌가는 최종적으로 코드레벨에서만 확실하게 판단할 수 있습니다.



의도가 명확한 코드 = 코드의 의도가 명확해집니다.

함수는 “~한 값을 입력하면 ~한 결과가 일관성 있게(예외 없이) 나온다"를 의미합니다. 그 자체로 의도와 동작이 명확합니다. 동작이 명확한 함수에 동작과 맞지 않는 이름을 붙이기는 어렵습니다. 함수 동작과 함수 이름이 일치하므로 함수 이름 또한 의도가 명확합니다. 함수 이름만 확인해도 의도에 맞게 활용하기가 쉽습니다. 대부분의 잘 만들어진 라이브러리는 의도가 명확하며 함수적 특징이 많습니다.

 

중복 없는 코드

재사용하기 어려운 지저분한 코드를 리팩토링 하지 않고 그 중 원하는 동작을 할 것 같은 일부 코드를 copy/paste 하고 또 이렇게 갖다 붙인 코드를 주변에 맞추기 위해 조금씩 수정하다 보면 중복이 많이 발생합니다. 그런데 함수적 코드는 재사용(함수 호출)하기 쉽고, 불필요하게 복잡하게 구현되어 있지 않습니다. 따라서 중복 없는 코드를 생성할 가능성이 높아집니다. 그리고 함수형으로 개발하게 되면, 함수 호출 만으로 이루어지는 코드가 점점 더 많아지기 때문에 이런 환경에서는 copy/paste와 같은 행위를 하는 것이 더 부담스러워집니다. 이런 행위가 좋지 않다는 것을 본능적으로 알려주게 됩니다. 또한 중복이 없고, 불필요한 코드가 없어지므로 코드량이 더 줄어 중복된 내용을 발견하기가 쉬워집니다.

 

간결한 코드

코드가 명확하고 재사용을 많이 하게 되면 결과적으로 간결해집니다.(간단하면서 짜임새가 있습니다. 구조적입니다.)



주요 요소

순수함수

함수의 실행 결과가 함수의 입력(매개 변수를 통한 값 전달) 이외에 다른 변수에 의해 영향을 받지 않는 함수를 말합니다. 함수형 프로그래밍에서 추구해야 하는 함수의 유형입니다.

 

고차함수

함수를 입력 받아 함수를 리턴하는 함수를 말합니다. 

고차함수도 순수함수가 될 수 있습니다.

 

참고: 일반적으로 많은 프로그래밍 언어에서 사용하던 함수 스타일은, 함수의 매개변수를 통해 기본형이나 객체 타입의 인자를 입력 받고 결과값(기본형 또는 객체)를 리턴합니다.

 

1급 객체

다른 요소들에 적용되는 모든 연산을 모두 적용할 수 있는 객체를 말합니다. 함수형 프로그래밍에서는 함수가 일급 객체인 언어를 사용하는 것이 편합니다. 최신 언어들은 함수가 일급개체인 경우가 많습니다.  코틀린에서도 함수는 일급 객체입니다.

1급 객체의 구체적인 조건

변수나 데이터 구조 안에 저장할 수 있습니다.

함수의 매개변수를 통해 전달할 수 있습니다.

함수의 리턴값으로 사용할 수 있습니다.

 

+ Recent posts