사전 함수
개념

함수

gabury1고친 사람 github-actions[bot]

함수는 자주 하는 일을 한 번 써 두고 이름으로 불러 쓰게 해 줍니다. 부르는 쪽이 넘긴 값으로 일을 하고 결과를 돌려줍니다. 이 말은 수학에서 빌려 왔습니다. 수학의 함수는 입력마다 출력을 하나씩 짝지어 주는 규칙입니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 여러 번 쓸 코드에 이름을 붙여 둡니다. 두 수를 더하는 코드에 add 라는 이름을 붙이면 어디서든 add(2, 3) 한 줄로 5를 얻습니다.

왜 이렇게 하나 — 같은 코드를 곳곳에 복사해 두면 고칠 때 복사본을 전부 찾아야 합니다. 한 곳에 모아 두면 한 번만 고치면 됩니다. 읽는 사람도 이름만 보고 무슨 일을 하는지 압니다.

어떻게 도나

  1. 부르는 코드가 값을 넘기며 함수 이름을 부릅니다
  2. 부른 코드는 멈추고 함수의 코드가 돕니다
  3. 함수가 결과를 돌려주면 멈췄던 곳에서 이어서 돕니다

대가 — 너무 잘게 쪼개면 한 가지 일을 이해하려고 여러 함수를 오가며 읽어야 합니다. 부를 때마다 값을 넘기고 돌아올 곳을 적어 두는 품도 조금 듭니다.

상세

요리책의 레시피 한 장을 떠올려 봅시다. 레시피에는 넣을 재료와 만드는 순서가 적혀 있습니다. 같은 요리를 또 할 때는 순서를 새로 적지 않고 그 장을 다시 펼칩니다.

프로그래밍 언어의 함수는 코드 한 묶음에 이름을 붙여, 그 이름으로 불러 실행하게 해 줍니다. 두 수를 더하는 코드를 add 라는 함수로 묶어 두면 프로그램 어디서든 add(2, 3) 이라고 적어 5를 얻습니다. 레시피에 재료를 적어 두듯, 함수도 부를 때 받을 값을 미리 정해 둡니다.

함수를 쓰는 까닭은 둘입니다. 첫째는 같은 코드를 되풀이해 적지 않는 것입니다. 복사본이 열 군데 있으면 버그 하나를 고칠 때 열 군데를 다 찾아야 합니다. 함수로 모아 두면 한 곳만 고치면 됩니다.

둘째는 자세한 속을 이름 뒤로 감추는 것입니다. sendWelcomeEmail(user) 를 읽는 사람은 메일 서버에 어떻게 붙는지 몰라도 이 줄이 무슨 일을 하는지 압니다. 세부를 감추고 하는 일만 드러내는 이 방식이 추상화입니다.

함수로 나누는 데도 대가가 있습니다. 너무 잘게 쪼개면 한 가지 일을 이해하려고 여러 함수를 오가며 읽어야 합니다. 이름이 속을 감추는 만큼, 속을 봐야 할 때는 한 번 더 들어가야 하기 때문입니다.

함수를 이루는 네 부분

함수 하나는 네 부분으로 이루어집니다. 아래 표는 각 부분이 무엇이고 왜 있는지를 보입니다.

부분 무엇인가 왜 있나
이름 함수를 부를 때 쓰는 이름 부르는 쪽이 이 이름으로 함수를 찾는다
매개변수 함수가 받을 값에 붙인 이름 같은 코드로 다른 값을 처리하게 한다
본문 실행할 코드 함수가 하는 일 그 자체다
반환값 함수가 끝나며 돌려주는 값 부른 쪽이 결과를 받아 쓴다

파이썬으로 적으면 아래와 같습니다. def 는 함수를 만든다는 표시입니다. return 뒤의 값이 반환값입니다.

Python
def add(a, b):
    return a + b

add(2, 3)      # 5
add(10, -4)    # 6

a 와 b 가 매개변수입니다. 부를 때 넣은 2 와 3 은 인자입니다. 매개변수는 함수 쪽에서 받을 값에 붙인 이름입니다. 인자는 부르는 쪽이 넘긴 값입니다.

함수의 이름과 매개변수의 개수·타입, 반환값의 타입을 한데 묶어 함수 시그니처라고 합니다. 위 add 를 정수에 쓴다면 「정수 둘을 받아 정수 하나를 돌려주는 add」가 시그니처입니다. 부르는 쪽은 본문을 몰라도 시그니처만 알면 함수를 쓸 수 있습니다. 추상화가 기대는 것이 이 시그니처입니다.

부르면 벌어지는 일

함수를 부르는 것을 호출이라고 합니다. 호출이 일어나면 실행 흐름이 함수 안으로 들어갑니다. 함수가 끝나면 흐름은 부른 곳으로 돌아옵니다. 이 절은 그사이 메모리에 무엇이 쌓이고 빠지는지를 봅니다.

부른 코드가 기다리는 동안 함수는 자기만 쓰는 메모리 공간을 받습니다. 매개변수와 함수 안에서 만든 값, 끝난 뒤 돌아갈 곳이 그 공간에 들어갑니다. 이 공간이 스택 프레임입니다.

함수 안에서 또 함수를 부르면 프레임이 하나 더 위에 쌓입니다. 이렇게 쌓인 더미가 호출 스택입니다. 아래 그림은 함수 a 가 b 를 부르고, b 가 다시 c 를 부를 때입니다.

flowchart TD
    subgraph S1["① a 가 b 를 부른 뒤"]
        direction TB
        a2["b 의 프레임"] --- a1["a 의 프레임"]
    end
    subgraph S2["② b 가 c 를 부른 뒤"]
        direction TB
        b3["c 의 프레임"] --- b2["b 의 프레임"] --- b1["a 의 프레임"]
    end
    subgraph S3["③ c 가 끝난 뒤"]
        direction TB
        c2["b 의 프레임"] --- c1["a 의 프레임"]
    end
    S1 --> S2 --> S3

c 가 끝나면 맨 위의 c 프레임이 빠집니다. 흐름은 바로 아래 프레임의 b 로 돌아갑니다. 늦게 쌓인 프레임이 먼저 빠지는 셈입니다.

함수가 자기 자신을 다시 부를 수도 있습니다. 이것이 재귀입니다. 폴더 안의 폴더를 훑는 일처럼 같은 모양이 안에서 되풀이되는 문제에 씁니다. 재귀도 부를 때마다 프레임을 하나씩 더 쌓습니다.

호출에는 품이 조금 듭니다. 부를 때마다 인자를 넘기고 프레임을 새로 만들어야 합니다. 그래서 컴파일러는 아주 짧은 함수를 부르는 대신 그 본문을 부른 곳에 펼쳐 넣기도 합니다. 이것을 인라이닝이라고 부릅니다.

수학에서 빌려 온 이름

수학의 함수는 입력 하나에 출력 하나를 짝지어 주는 규칙입니다. f(x) = 2x 에 3을 넣으면 언제 넣어도 6이 나옵니다. 같은 입력에서 다른 출력이 나오는 일은 없습니다.

프로그래밍의 함수는 값을 받아 값을 내는 모양이 닮아서 이 이름을 씁니다. 그러나 프로그래밍의 함수는 결과를 돌려주는 것 말고도 다른 일을 할 수 있습니다. 파일에 쓰거나 데이터베이스를 고치기도 합니다. 지금 시각을 읽어 와서 부를 때마다 다른 값을 내기도 합니다.

부수 효과와 순수 함수

함수가 반환값 말고 바깥에 남기는 흔적이 부수 효과입니다. 파일 쓰기, 데이터베이스 수정, 로그 출력이 모두 부수 효과입니다. 함수 바깥에 있는 변수를 바꾸는 것도 부수 효과입니다.

순수 함수는 두 조건을 함께 지키는 함수입니다. 첫째, 같은 인자에는 언제나 같은 반환값을 냅니다. 둘째, 부수 효과가 없습니다. 둘 중 하나만 어겨도 순수 함수가 아닙니다.

앞 절의 지금 시각을 읽는 함수는 바깥에 흔적을 남기지 않습니다. 그래도 부를 때마다 값이 달라서 첫째 조건에 걸립니다. 바깥 값을 읽기만 해도 순수 함수가 되지 못합니다.

아래 두 함수를 견줘 봅시다. total 은 순수 함수입니다. save 는 순수 함수가 아닙니다.

Python
def total(prices):
    return sum(prices)

total([100, 200])    # 300
total([100, 200])    # 300

def save(order):
    db.insert(order)  # 바깥을 바꾼다

total 은 몇 번을 불러도 같은 인자에 같은 값을 냅니다. save 는 돌려주는 값이 없습니다. 대신 부를 때마다 데이터베이스에 한 줄을 더합니다.

순수 함수는 테스트하기 쉽습니다. 인자를 넣고 반환값이 기대한 값인지만 보면 됩니다. 결과를 저장해 두었다 다시 쓰는 메모이제이션도 걱정 없이 쓸 수 있습니다. 같은 인자에는 같은 결과가 나오기 때문입니다.

부수 효과가 있는 함수는 두 번 부르면 결과가 달라질 수 있습니다. 네트워크 오류로 save 를 다시 부르면 주문이 두 건 생깁니다. 여러 번 불러도 한 번 부른 것과 결과가 같은 성질이 멱등성입니다.

쓸모 있는 프로그램은 부수 효과 없이 돌 수 없습니다. 결과를 저장하거나 응답을 보내는 일이 곧 부수 효과이기 때문입니다. 그래서 계산은 순수 함수에 맡기는 설계를 흔히 씁니다. 바깥을 바꾸는 일은 몇몇 함수에 모아 둡니다.

값처럼 넘기는 함수

많은 언어에서 함수를 숫자나 문자열처럼 값으로 다룰 수 있습니다. 함수를 변수에 담을 수 있습니다. 다른 함수에 인자로 넘기거나 반환값으로 돌려줄 수도 있습니다. 이렇게 값처럼 다룰 수 있는 함수가 일급 함수입니다.

이 성질은 「무엇을 할지」의 일부를 부르는 쪽이 정하게 하고 싶을 때 씁니다. 아래 twice 는 함수 f 를 받아 두 번 부릅니다.

Python
def twice(f, x):
    return f(f(x))

def inc(n):
    return n + 1

twice(inc, 5)                # 7
twice(lambda n: n * 3, 2)    # 18

inc 를 넘기면 5에 1을 두 번 더해 7이 됩니다. twice 처럼 함수를 인자로 받거나 반환값으로 돌려주는 함수가 고차 함수입니다.

마지막 줄의 lambda n: n * 3 은 그 줄에서 바로 만든 이름 없는 함수입니다. 이런 함수가 익명 함수입니다. 한 번만 쓸 짧은 함수에 이름을 짓지 않아도 됩니다. 언어에 따라 람다 표현식이라는 이름으로도 부릅니다.

콜백은 다른 함수나 플랫폼에 미리 넘겨 두는 함수입니다. 넘겨받은 쪽이 때가 되면 그 함수를 부릅니다. 일이 언제 끝날지 부르는 쪽이 모를 때 씁니다.

콜백을 가장 흔히 넘기는 대상이 이벤트입니다. 이벤트는 버튼 클릭이나 파일 도착처럼 프로그램 밖에서 일어난 일을 알리는 신호입니다. 자바스크립트의 button.addEventListener("click", onClick) 은 버튼을 누를 때 부를 함수 onClick 을 미리 넘겨 둡니다.

함수와 비슷한 이름들

같은 것을 두고 언어와 시대에 따라 다른 이름을 씁니다. 대부분 「이름을 붙여 따로 부르는 코드 묶음」이라는 뜻을 함수와 나눠 가집니다.

백엔드 코드에서 가장 자주 듣는 것은 메서드입니다. 클래스는 같은 모양의 객체를 찍어 내는 틀입니다. 메서드는 그 틀 안에 적힌 함수입니다. 자바처럼 모든 함수를 클래스 안에 두는 언어에서는 함수를 거의 다 메서드라고 부릅니다.

메서드 말고도 오래된 코드나 다른 언어에서 두 이름을 더 만납니다. 셋을 나란히 놓으면 아래와 같습니다.

이름 무엇을 가리키나
메서드 클래스 안에 적힌 함수
프로시저 반환값 없이 일만 하는 코드 묶음. 언어에 따라 함수와 가르기도 하고 같은 것으로 치기도 한다
서브루틴 따로 떼어 불러 쓰는 코드 묶음을 가리키는 오래된 말. 함수와 거의 같은 뜻으로 쓴다

배포 단위가 된 함수

서버리스는 서버를 직접 띄워 관리하지 않고 코드만 올려 두는 방식입니다. 이 방식에서는 함수 하나가 배포하는 단위가 됩니다. 요청이나 이벤트가 오면 플랫폼이 그 함수를 불러 실행합니다.

함수 단위로 코드를 맡기는 이런 서비스가 FaaS(Function as a Service, 서비스형 함수)입니다. FaaS 에서도 함수의 모양은 바뀌지 않습니다.

아마존 웹 서비스(AWS, Amazon Web Services)의 FaaS 인 AWS Lambda 에서 파이썬 함수는 def handler(event, context): 꼴입니다. 이벤트 한 건을 event 인자로 받습니다. 처리 결과는 반환값으로 돌려줍니다.

보통 함수도 호출이 끝나면 지역 값은 사라집니다. 서버리스에서는 함수 밖 전역 변수에 남긴 값도 다음 호출까지 남는다는 보장이 없습니다. 플랫폼이 함수를 돌리는 실행 환경을 호출마다 새로 띄울 수 있기 때문입니다.

한동안 안 쓰인 실행 환경은 플랫폼이 치워 버리기도 합니다. 그래서 다음 호출로 이어 가야 할 값은 데이터베이스 같은 함수 밖의 저장소에 둡니다.

관련 항목

함수를 이루는 구성 요소

매개변수 · 인자 · 반환값 · 함수 시그니처 · 지역 변수 · 스코프

함수를 부를 때 움직이는 실행 구조

스택 프레임 · 호출 스택 · 호출 규약 · 반환 주소 · 재귀 · 꼬리 호출 · 인라이닝 · 컴파일러

부수 효과 유무로 갈리는 성질과 기법

순수 함수 · 부수 효과 · 참조 투명성 · 멱등성 · 불변성 · 메모이제이션

함수를 값으로 다루는 기법

일급 함수 · 고차 함수 · 익명 함수 · 람다 표현식 · 콜백 · 클로저 · 함수 포인터

함수와 헷갈리는 비슷한 이름

메서드 · 프로시저 · 서브루틴 · 저장 프로시저 · 매크로

함수로 코드를 나누는 설계 원리

추상화 · 모듈 · 캡슐화 · 관심사 분리 · 단일 책임 원칙

함수를 어디에 두느냐로 갈리는 프로그래밍 방식

함수형 프로그래밍 · 절차적 프로그래밍 · 객체 지향 프로그래밍 · 프로그래밍 언어 · 클래스

함수를 배포 단위로 삼는 실행 환경

서버리스 · FaaS · AWS Lambda · 핸들러 · 이벤트 · 콜드 스타트

함수라는 이름이 비롯된 수학

정의역 · 공역 · 치역 · 람다 계산법

다른 이름: function