최근 수정 시각 : 2024-11-03 19:36:24

함수(프로그래밍)




파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r478에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r478 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)


프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터 · 구조체 · size_t) · C++(자료형 · 클래스 · 이름공간 · 상수 표현식 · 특성) · C# · Java · Python(함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript · Haskell(모나드)
마크업 문법 HTML · CSS
개념과 용어 함수(인라인 함수 · 고차 함수 · 람다식) · 리터럴 · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · deprecated · NaN · null · undefined · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}


1. 함수의 개념
1.1. 함수의 장점
2. 수학 함수와의 관계3. 프로그래밍 언어의 함수 구현
3.1. 호출 규약3.2. 인라인 함수3.3. 익명함수(람다식)와 고차 함수3.4. 함수 호출 시 오버헤드
4. 관련 문서

1. 함수의 개념

프로그래밍에서의 함수는 프로그램 소스 코드에서 일정한 동작을 수행하는 코드(들)[1]을 말한다.

Ada(프로그래밍 언어), PL/SQL 등 언어에 따라 함수와 구분되는 "프로시저"가 별도로 존재하는 경우도 있다. 이 경우 함수와의 차이점은 반환값의 유무이다. 실행 후 반환되는 값이 있으면 함수, 없는 것을 프로시저로 구분한다.

프로그램이 동작할 때 함수의 호출 과정은 다음과 같다.
  • 코드상에서 함수가 호출된다. 이때 변수를 넘겨주기도 하는데 이를 매개변수 또는 인수(parameter)라고 한다.
  • 호출된 함수의 코드가 동작한다.
  • 함수의 동작이 끝나면 함수를 종료하고 경우에 따라 반환값을 코드에 반환한다.

함수는 크게 내장 함수와 사용자 정의 함수로 나뉜다. 내장 함수는 프로그래밍 언어 차원에서 이미 정의되어 있는 함수로, 별도로 함수를 만들 필요 없이 형식에 맞춰 불러 쓰기만 하면 된다. 언어 레퍼런스를 보면 상당히 많은 내장 함수가 제공되는 것을 알 수 있다.

사용자 정의 함수는 프로그래머가 만들어 쓰는 함수로, 코드의 어딘가에 미리 정의해야 사용할 수 있다. 전역에서 불러오는 전역 함수가 있는 반면, 클래스에서 static 선언을 통해 전역으로 쓰는 함수도 있고, 인스턴스를 통해서만 쓸 수 있는 함수도 있다. 프로그래머가 쓰는 절대다수의 함수는 인스턴스를 통해서 쓰는 방식이다.

C#Java에서는 메소드(method), C++에서는 프로시저(procedure)라고 부른다. 대개는 함수라는 표현이 통용되며, 메소드의 경우 클래스 내 함수(프로퍼티 함수) 또는 라이브러리 함수에 쓰는 경우가 많다.[2]

1.1. 함수의 장점

함수를 이용하면 반복적으로 사용되는 코드 또는 논리적으로 하나의 단위로 표현될 수 있는 작업의 집합을 함수로 분리하여 호출하는 식으로 간단하게 정리할 수 있다. 또한 프로그램을 사람이 쉽게 이해할 수 있는 작은 단위로 분할하는 것이 개발과 유지보수에 도움이 되기 때문에 현재 대다수의 프로그래밍 언어는 함수를 지원한다.

2. 수학 함수와의 관계

수학에서의 함수와 프로그래밍에서의 함수는 대동소이해 보이지만 크게 다르다. 특정한 값을 입력하여 특정한 값을 출력하는 메커니즘 자체는 같지만, 프로그래밍에서의 함수는 내적으로 그 입력값을 어떻게 다루어 출력값을 내는지도 보는 반면, 수학에서의 함수는 순수하게 입력값과 출력값만을 본다. 전자를 Intensionality 라 하고, 후자를 Extensionality 라 하는데[3], 이로 인해 깊은 부분에서 큰 차이가 발생하는 경우가 많다.

둘째로 정의역과 공역이 모든 집합이 다 허용되는 수학 함수와는 달리 프로그래밍에서는 이산적인 집합만 허용된다.[4] 이는 알고리즘적인 집합(recursively enumerable set)만 다룰 수 있는 컴퓨터 자료 구조의 한계, 나아가서는 알고리즘의 수학적 모델 자체의 한계로 인해 발생한 것.

심지어는 파일/콘솔 입출력 관련 함수들 중에서는 입력값이나 출력값 중 하나가 없는 함수도 존재하는데[5], 이걸 수학적 개념으로 설명할 수가 없다. 그나마 수학 함수와 흡사하게 설계한 Haskell과 같은 여러 함수형 언어들조차도 이 모순은 해결하지 못했다. 하물며 아예 내용 자체가 없는 Dummy 함수까지 있으니 수학의 함수와는 그냥 이름만 같은 존재로 정의내릴 수 있다.

또한 수학 함수와는 달리 하나에 입력에 여러 가지 출력이 대응되어도 상관없다. 사이드 이펙트가 없이 같은 입력에 대해서 항상 같은 결과를 되돌려주는 함수를 참조 투명(Referentially Transparent, 명사형은 Referential Transparency)하다고 하며, 이런 함수를 '순수 함수'라고 한다. 반대로 사이드 이펙트가 있으며, 같은 입력에 대해서 다른 결과가 나올 수도 있는 함수를 참조 불투명(Referentially Opaque, 명사형은 Referential Opacity)하다고 한다. 난수가 참조 불투명한 함수의 가장 좋은 예이다.

여담이지만, 프로그래밍에서의 함수를 수학에서의 함수처럼 에뮬레이션하여(이 경우에도 Intensionality와 알고리듬적인 자료구조만을 사용하는 것은 동일하다. 사이드 이펙트만을 제한한 것이다.) 프로그래밍 하는 순수 함수형 프로그래밍 언어 역시 존재한다.

대부분 전사나 단사를 따지지 않아도 되지만, 이걸 따져야 하는 경우도 있다. 튜링 테스트의 대표격인 CAPTCHA와, JPEG의 양자화(또는 계수화)에는 역함수가 존재하지 않기 때문에(단사가 아니기 때문에) 완벽하게 복원할 방법이 없으며, 이 때문에 아직 튜링 테스트를 통과한 기계가 없고, 앞으로도 당연히 없을 것이다. 비손실 압축과 양방향 암호 알고리즘은 해당 함수가 전단사여야만 유효하며,[6] 굳이 단사일 필요가 없는 단방향 암호 알고리즘마저도 단사가 아닌 경우(충돌)를 최소화해야 한다.

3. 프로그래밍 언어의 함수 구현

3.1. 호출 규약

상술한대로 함수를 호출할 때에는 함수의 인자(parameter)를 피호출자(callee)에게 전달할 필요가 있으며, 함수가 종료된 후에는 void(혹은 procedure)가 아닌 이상 리턴값을 받아올 방법이 있어야 한다. 컴파일러별로, 혹은 라이브러리를 만드는 회사별로 인자를 메모리에 적재하는 방식이 다르다면 일일이 맞추어 주거나 사용을 포기할 수밖에 없다. 이를 위해서 함수를 호출할 때 스택의 구조, 파라미터를 넘기는 방법, 리턴값을 전달하는 방법 등을 미리 통일해 두어야 한다. 이렇게 통일된 규칙을 호출 규약(Calling convention)이라 한다. 같은 운영체제라면 호출 규약이 통일되어 있다고 보아도 좋다. Python과 같은 언어에서 고속 처리를 위해 C 라이브러리를 불러서 사용할 수 있는 이유도 호출 규약대로 인자를 적재하고 call 명령을 내리면 바로 C로 작성된 함수가 호출되며 피호출자 안에서 호출자가 적재한 인자를 그대로 읽어올 수 있기 때문이다. 소스코드의 인터페이스가 아니라 이미 컴파일된 이진(Binary) 파일을 호출하는 인터페이스이기에 응용 프로그램 이진 인터페이스(Applicaiton Binary Interface = ABI)라고 부른다. 또한 스택 정리를 호출자가 하느냐 피호출자가 하는가, 함수 호출 전후로 보존되어야 하는 레지스터가 무엇무엇인가 등도 모두 호출 규약에 정의되어 있다. 스택 정리를 피호출자가 하면 X86에서는 retn(N)[7] 명령 하나로 스택 정리와 리턴을 동시에 수행할 수 있어서 조금 더 빠르고 편리하게 함수 호출에서 리턴할 수 있었다. 하지만 가변 인자(ex. printf, scanf 등.)를 사용할 수 없어서 C에서는 호출자가 리턴 후 스택을 정리해준다. 가변 인자를 사용하면 피호출자는 인자의 개수를 알 방법이 없기 때문이다.

예) X86 CPU의 호출 규약
아키텍처 호출 규약 이름 레지스터 매개변수 스택의 매개변수 순서 스택 정리 주체 비고
IA-32(32비트 X86) cdecl RTL 호출자
stdcall RTL 피호출자 [8]
fastcall ECX, EDX RTL 피호출자
thiscall ECX RTL 피호출자 [9]
X86-64 Microsoft X64 RCX, RDX, R8, R9 RTL 호출자 [10]
System V RDI, RSI, RDX, RCX, R8, R9 RTL 호출자 [11]

3.2. 인라인 함수

파일:상세 내용 아이콘.svg   자세한 내용은 인라인 함수 문서
번 문단을
부분을
참고하십시오.

3.3. 익명함수(람다식)와 고차 함수

파일:상세 내용 아이콘.svg   자세한 내용은 람다식 문서
번 문단을
부분을
참고하십시오.
파일:상세 내용 아이콘.svg   자세한 내용은 고차 함수 문서
번 문단을
부분을
참고하십시오.

3.4. 함수 호출 시 오버헤드

함수를 불러올 때, 호출 뒤 귀환할 주소와 현재의 상태를 담고 있는 각종 레지스터를 어딘가에 저장해야한다. 이런 정보들을 저장하는 장소를 보통 호출 스택(call stack) 또는 런 스택(run stack)이라고 한다. 함수를 콜 하기 전에 이런 정보를 저장하는 작업이 필요하므로 약간의 오버헤드가 생긴다. 따라서 프로그램의 속도를 골수로 뽑아내야 하는 시스템 프로그램에는 필요 이상의 함수 콜을 자제하고, 함수 스타일의 매크로를 많이 이용하곤 했다. 또한, 과거에는 컴파일을 하고도 그걸 도로 디스어셈블해서 코드를 손으로 최적화하는 삽질이 많았으나 대부분 옛날 얘기고, 요즘은 웬만한 함수 호출 부하따윈 씹어먹을 만큼 하드웨어가 발전했고, 정 부하가 될 만한 부분은 컴파일러가 자동으로 함수 호출 코드를 함수 내용으로 바꿔주는(인라이닝[12]) 등의 최적화 테크닉을 컴파일러가 자동으로 해주므로 함수를 멀리하고 매크로를 가까이 할 필요성은 사실상 사라졌다.

재귀함수를 짤 경우 호출 스택의 용량에는 한계가 있기 때문에 그 용량을 초과해서 재귀 호출하면 스택 오버플로우가 발생할 수 있다. 단 일반 함수와 달리 함수를 후 값을 반환하는 대신 주어진 환경에서 메모리 주소를 점프하며 함수만 갈아끼우는 꼬리재귀 최적화[13](Tail-recursion optimization)가 지원되는 컴파일러는 재귀 스타일로 짜더라도 스택 오버플로우를 방지할 수 있다. 물론 꼬리재귀 최적화가 가능하게 짜 줘야 한다[14]. 현재 주요 컴파일러(GCC, clang/llvm, vc)에는 모두 포함되어 있는 기능이다. Java에선 지원하지 않는다. 대신 다른 JVM기반 언어인 Scala나 Clojure에선 각각 꼬리재귀 최적화 가능하게 코딩하면 컴파일 시 루프로 변환하거나 명시적으로 재귀시켜 컴파일 시 루프로 변환시킬 수 있다.

4. 관련 문서


[1] 보통은 코드가 여럿인 경우만 생각하지만, C언어의 매크로 함수나 인라인 함수, OOP의 접근자(accessor) 등의 경우 코드가 하나인 경우도 있다. 사실 실용성을 버린다면 void형(리턴 값이 없는 함수)으로 선언해서 안에 한 줄도 안 써놓아도 함수는 함수.[2] 클래스 내 변수는 C#에서는 프로퍼티, Java에서는 필드라고 부른다. 변수건 함수건 부르는 이름만 다를 뿐이며, 헷갈리면 변수, 상수, 함수라고 명확히 표현해주는 것이 의사소통에서 도움이 된다.[3] 집합론의 제일공리가 Extensionality 이다.[4] 이산확률분포의 확률질량함수를 떠올리면 된다.[5] 가장 대표적인 것은 C언어의 free(). 입력값이 존재하나 메모리 내에서만 기능을 가질 뿐 실제로는 아무것도 반환하지 않는다. 이외에도 rewind()와 Sleep()도 입력값이 존재하나 출력값이 없다.[6] 암호 알고리즘이 전단사가 아니라면 원하지 않는 개인키로 암호를 해독하는 등, 암호 체계 자체가 쓸모 없어진다.[7] 스택 포인터를 N 감소시키고 리턴한다[8] Windows에서 이 방식을 사용했으며 이 때문에 API로 프로그램을 작성할 때에는 int main(...)이 아닌 int PASCAL WinMain(...)으로 프로그램을 시작하였다[9] C++의 멤버 함수에서 사용하는 방법으로 this 포인터를 ECX 레지스터에 적재한 후 호출한다.[10] 마이크로소프트 Windows에서 사용하는 규약. 처음의 인자 네 개는 레지스터에, 그 이후 인자는 스택에 적재한다. C++에서 숨겨진 인자인 this 포인터는 첫번째 인자로 간주되어 RCX 레지스터로 넘어온다.[11] 리눅스, x86 솔라리스, BSD, OS X에서 사용하는 규약. 처음의 인자 여섯 개는 레지스터에, 그 이후 인자는 스택에 적재한다. C++에서 숨겨진 인자인 this 포인터는 명확한 규칙은 없으나 보통 RAX 레지스터로 넘어온다.[12] 언어에 따라 inline 키워드를 붙여 마음대로 인라인 할 수도 있다. 물론 재귀함수 같은 건 키워드 붙여도 안 된다.[13] "끝재귀 최적화"라고도 한다.[14] 함수 호출 후 아무것도 하지 않고 바로 그 값을 리턴할 때.