Skip to main content

7. 함수로 나누는 프로그램: 재사용과 구조화의 시작

지금까지는 main() 안에 코드를 길게 이어 붙여도 어느 정도 프로그램이 돌아갔습니다. 하지만 코드가 조금만 길어지면 금방 복잡해집니다. 같은 계산이 여러 번 나오고, 어디서 무엇을 하는지 찾기 어려워지고, 한 줄 고치면 다른 부분이 같이 흔들리기도 합니다. 이때 등장하는 가장 강력한 정리 도구가 바로 함수(function) 입니다.

함수는 "특정 일을 맡는 작은 작업 단위"라고 생각하면 쉽습니다. 예를 들어 게임 점수를 계산하는 일, 세 숫자 중 최대값을 구하는 일, 사용자에게 인사 메시지를 출력하는 일을 각각 함수로 나눌 수 있습니다. 이렇게 나누면 코드는 짧아지고, 다시 쓸 수 있고, 읽기도 쉬워집니다. 프로그램이 커질수록 main() 하나로 버티는 습관과, 함수를 잘게 나누는 습관의 차이는 정말 크게 벌어집니다.12

함수로 나누는 프로그램: 재사용과 구조화의 시작

왜 함수가 중요한가

초보자가 처음 짠 프로그램은 흔히 이런 모습입니다.

#include <iostream>
using namespace std;

int main() {
    int kor, eng, math;
    cin >> kor >> eng >> math;

    int total = kor + eng + math;
    double average = total / 3.0;

    cout << "총점: " << total << 'n';
    cout << "평균: " << average << 'n';

    if (average >= 90) {
        cout << "등급: An";
    } else if (average >= 80) {
        cout << "등급: Bn";
    } else if (average >= 70) {
        cout << "등급: Cn";
    } else {
        cout << "등급: Dn";
    }

    return 0;
}

작은 예제라면 괜찮아 보입니다. 그런데 여기에 학생 수가 늘어나고, 출력 형식이 바뀌고, 평균 규칙이 달라지고, 파일 저장 기능까지 붙으면 어떻게 될까요? main()은 금세 한 장짜리 메모에서 "미로"가 됩니다.

이럴 때 함수로 나누면 역할이 선명해집니다.

#include <iostream>
using namespace std;

int getTotal(int kor, int eng, int math) {
    return kor + eng + math;
}

double getAverage(int total) {
    return total / 3.0;
}

char getGrade(double average) {
    if (average >= 90) return 'A';
    if (average >= 80) return 'B';
    if (average >= 70) return 'C';
    return 'D';
}

void printReport(int total, double average, char grade) {
    cout << "총점: " << total << 'n';
    cout << "평균: " << average << 'n';
    cout << "등급: " << grade << 'n';
}

int main() {
    int kor, eng, math;
    cin >> kor >> eng >> math;

    int total = getTotal(kor, eng, math);
    double average = getAverage(total);
    char grade = getGrade(average);

    printReport(total, average, grade);
    return 0;
}

똑같은 일을 하지만 훨씬 읽기 쉽습니다. 함수 이름만 훑어봐도 프로그램의 흐름이 보입니다. 이것이 함수의 힘입니다.

함수란 무엇인가

함수는 특정 작업을 수행하는 이름 붙은 코드 묶음입니다. 필요할 때 함수 이름으로 불러서 실행할 수 있습니다. C++ 프로그램은 반드시 main() 함수에서 시작하는데, 다른 함수들은 main()이나 또 다른 함수가 호출할 때 실행됩니다.3

간단한 함수 하나를 봅시다.

int square(int x) {
    return x * x;
}

이 함수는 정수 하나를 받아서 제곱한 결과를 돌려줍니다. square(5)라고 부르면 결과는 25입니다.

함수는 보통 네 부분으로 읽습니다.

  • 반환형(return type): 함수가 돌려주는 값의 자료형

  • 함수 이름(name): 함수를 부를 때 사용하는 이름

  • 매개변수(parameter): 함수가 입력으로 받는 값

  • 함수 본문(body): 실제로 실행되는 코드

위의 square 함수에서 int는 반환형, square는 이름, int x는 매개변수, 중괄호 안은 본문입니다.

함수 선언과 정의

C++에서는 선언(declaration) 과 정의(definition) 를 구분하는 경우가 많습니다.

선언은 "이런 함수가 있다"라고 컴파일러에게 먼저 알려주는 것입니다.

int add(int a, int b);

정의는 "실제로 이 함수가 이렇게 동작한다"를 적는 것입니다.

int add(int a, int b) {
    return a + b;
}

둘을 함께 쓸 수도 있고, 나눠서 쓸 수도 있습니다. 함수 정의가 main() 아래에 있을 때는 위쪽에 선언을 적어 두어야 컴파일러가 함수의 이름, 반환형, 매개변수 정보를 미리 알고 처리할 수 있습니다.14

예를 들어 다음 코드는 아주 흔한 형태입니다.

#include <iostream>
using namespace std;

int add(int a, int b);  // 함수 선언

int main() {
    cout << add(3, 5) << 'n';
    return 0;
}

int add(int a, int b) { // 함수 정의
    return a + b;
}

초보자가 자주 하는 실수는 "정의가 아래에 있는데 선언을 안 쓰는 것"입니다. 그러면 컴파일러는 main()에서 add()를 만났을 때 "그게 뭔데?"라는 표정이 됩니다.

함수 호출: 이름으로 일을 시킨다

함수를 사용하려면 호출(call) 해야 합니다.

#include <iostream>
using namespace std;

void greet() {
    cout << "안녕하세요, C++!" << 'n';
}

int main() {
    greet();
    greet();
    return 0;
}

greet();를 호출할 때마다 함수 본문이 실행됩니다. 한 번 정의해 두고 여러 번 부를 수 있다는 점이 바로 재사용입니다.

함수 호출은 "점프했다가 돌아오는 것"으로 이해하면 편합니다. 프로그램은 현재 위치를 잠깐 저장해 두고 함수를 실행한 뒤, 함수가 끝나면 원래 자리로 돌아옵니다. 책 읽다가 전화가 와서 책갈피를 꽂고 통화한 뒤, 다시 그 페이지로 돌아오는 느낌과 비슷합니다.3

매개변수와 인수: 함수에 값을 넣는 법

함수는 종종 입력값이 필요합니다. 이때 함수 정의 쪽에 적는 변수를 매개변수(parameter), 호출할 때 실제로 넣는 값을 인수(argument) 라고 합니다.5

#include <iostream>
using namespace std;

void printNum(int n) {
    cout << n << 'n';
}

int main() {
    int num = 10;
    printNum(num); // num은 인수, n은 매개변수
    printNum(99);
    return 0;
}

여기서 중요한 점은 함수 안에서는 num이 아니라 n을 쓴다는 것입니다. 함수는 호출한 쪽의 변수 이름을 그대로 가져오는 것이 아니라, 받은 값을 자기 매개변수에 담아 사용합니다.

매개변수는 여러 개일 수도 있습니다.

int add(int a, int b) {
    return a + b;
}

호출은 이렇게 합니다.

int result = add(3, 4);

매개변수의 개수와 순서, 자료형은 매우 중요합니다. add(int a, int b)라면 보통 정수 두 개를 순서대로 넘겨야 합니다. 초보자에게는 이 규칙이 조금 엄격해 보일 수 있지만, 덕분에 함수가 어떤 입력을 기대하는지 분명해집니다.

반환값: 함수가 계산 결과를 돌려준다

함수는 일을 마친 뒤 값을 하나 돌려줄 수 있습니다. 이 값을 반환값(return value) 이라고 합니다. 반환값을 돌려주는 함수는 반환형을 void가 아닌 자료형으로 적습니다.

int multiply(int x, int y) {
    return x * y;
}

이 함수는 int 값을 하나 반환합니다.

int result = multiply(4, 5);
cout << result << 'n';

반환할 값이 없는 함수는 void를 사용합니다.

void printLine() {
    cout << "----------------n";
}

return은 단지 값을 돌려주는 것만이 아니라, 함수를 끝내는 역할도 합니다. 그래서 return 뒤의 코드는 실행되지 않습니다.4

int getPositive(int n) {
    if (n < 0) {
        return 0;
    }

    return n;
}

이 함수는 음수가 들어오면 바로 0을 반환하고 끝납니다.

값으로 전달된다는 뜻: 함수는 기본적으로 복사본을 받는다

아주 중요한 개념 하나를 여기서 익혀야 합니다. C++에서 함수 인수는 기본적으로 값 전달(pass by value) 됩니다. 즉, 함수는 원본을 직접 받는 것이 아니라 복사본을 받습니다.56

#include <iostream>
using namespace std;

void changeValue(int x) {
    x = 100;
}

int main() {
    int num = 10;
    changeValue(num);
    cout << num << 'n';
    return 0;
}

출력 결과는 100이 아니라 10입니다. 왜냐하면 함수 안의 x는 num의 복사본이기 때문입니다. 함수 안에서 복사본을 아무리 바꿔도 원래 변수 num은 그대로입니다.

이 개념을 모르면 많은 초보자가 "분명 함수 안에서 바꿨는데 왜 안 바뀌지?" 하고 멈춥니다. 사실 프로그램이 말을 안 듣는 게 아니라, 복사본만 바꾼 것입니다.

물론 원본을 바꾸는 방법도 있습니다. 참조자와 포인터를 이용하는 방법인데, 그 내용은 뒤의 챕터에서 본격적으로 다룹니다. 여기서는 우선 기본은 값 전달이라는 감각만 확실히 잡아 두면 충분합니다.

스코프: 변수는 어디까지 보이는가

이제 함수와 함께 꼭 알아야 하는 개념이 스코프(scope) 입니다. 스코프는 쉽게 말해 변수를 사용할 수 있는 범위입니다.7

가장 먼저 알아둘 것은 지역 변수(local variable) 입니다.

#include <iostream>
using namespace std;

void test() {
    int x = 5;
    cout << x << 'n';
}

int main() {
    test();
    return 0;
}

여기서 x는 test() 안에서만 사용할 수 있습니다. main()에서 x를 출력하려고 하면 오류가 납니다.

#include <iostream>
using namespace std;

void test() {
    int x = 5;
}

int main() {
    cout << x << 'n'; // 오류
    return 0;
}

왜 그럴까요? x는 test()라는 방 안에 놓인 물건이기 때문입니다. main()은 다른 방입니다. 문을 열고 몰래 가져올 수 없습니다.

함수의 매개변수도 함수 안에서는 지역 변수처럼 동작합니다.

void printDouble(int value) {
    cout << value * 2 << 'n';
}

여기서 value는 printDouble() 안에서만 쓸 수 있습니다.5

지역 변수와 전역 변수

함수 밖에서 선언한 변수는 전역 변수(global variable) 라고 합니다.

#include <iostream>
using namespace std;

int x = 5; // 전역 변수

void show() {
    cout << x << 'n';
}

int main() {
    show();
    cout << x << 'n';
    return 0;
}

전역 변수는 여러 함수에서 접근할 수 있어서 편리해 보입니다. 하지만 너무 쉽게 손댈 수 있기 때문에, 프로그램이 커질수록 누가 값을 바꿨는지 추적하기 어려워집니다. 그래서 초보자일수록 "필요한 값은 함수 매개변수로 넘기고, 결과는 반환값으로 받는 습관"을 들이는 편이 좋습니다.8

같은 이름의 지역 변수와 전역 변수가 함께 있을 수도 있습니다.

#include <iostream>
using namespace std;

int x = 5;

void show() {
    int x = 22;
    cout << x << 'n';
}

int main() {
    show();
    cout << x << 'n';
    return 0;
}

이 경우 함수 안의 x는 지역 변수이고, 함수 밖의 x는 전역 변수입니다. 둘은 이름만 같을 뿐 서로 다른 변수입니다. 이런 코드는 헷갈리기 쉬우니 초반에는 피하는 편이 좋습니다.

블록 스코프: 중괄호 안에서만 사는 변수

스코프는 함수 단위로만 생기지 않습니다. if, for, while 같은 문장의 중괄호 안에서도 새 범위가 생깁니다.7

#include <iostream>
using namespace std;

int main() {
    if (true) {
        int score = 100;
        cout << score << 'n';
    }

    // cout << score << 'n'; // 오류
    return 0;
}

score는 if 블록 안에서만 존재합니다. 블록 밖으로 나오면 사라집니다.

반복문에서도 비슷합니다.

for (int i = 0; i < 3; i++) {
    cout << i << 'n';
}

// cout << i << 'n'; // 오류

i는 반복문 안에서만 유효합니다. 그래서 같은 이름을 다른 곳에서 다시 쓸 수 있기도 합니다. 다만 너무 남발하면 독자가 길을 잃습니다.

함수를 잘 나누는 기준

"언제 함수를 만들면 좋을까?"라는 질문은 아주 중요합니다. 정답은 의외로 단순합니다.

다음과 같은 신호가 보이면 함수로 분리할 타이밍입니다.

  • 같은 코드가 두 번 이상 등장한다.

  • 코드 덩어리를 한 문장으로 설명할 수 있다.

  • main()이 너무 길어서 한눈에 읽기 어렵다.

  • 입력, 계산, 출력이 한곳에 뒤엉켜 있다.

예를 들어 아래 코드는 아직 기능은 작지만, 벌써 역할이 섞여 있습니다.

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    int sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }

    cout << "1부터 " << n << "까지의 합: " << sum << 'n';
    return 0;
}

이 코드를 함수로 나누면 더 또렷해집니다.

#include <iostream>
using namespace std;

int sumToN(int n) {
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    return sum;
}

void printResult(int n, int sum) {
    cout << "1부터 " << n << "까지의 합: " << sum << 'n';
}

int main() {
    int n;
    cin >> n;

    int result = sumToN(n);
    printResult(n, result);
    return 0;
}

이제 main()은 "입력 받기 → 계산하기 → 출력하기"라는 큰 흐름만 보여 줍니다. 세부 계산은 sumToN()이 맡고, 출력 형식은 printResult()가 맡습니다.

좋은 함수 이름은 설명서 역할을 한다

함수를 잘게 나누는 것만으로는 충분하지 않습니다. 이름도 좋아야 합니다. 함수 이름은 "이 함수가 무엇을 하는지"를 말해 줘야 합니다.8

예를 들어 아래 이름은 애매합니다.

int doThing(int a, int b);
void process(int x);

무슨 일을 하는지 알기 어렵습니다. 반면 이런 이름은 훨씬 읽기 쉽습니다.

int getTotalScore(int kor, int eng, int math);
bool isEven(int number);
void printMenu();

좋은 이름은 코드를 줄이는 마법보다 더 큰 효과를 냅니다. 코드를 읽는 사람의 머릿속 설명서를 대신 써 주기 때문입니다.

입력, 계산, 출력을 섞지 말자

초보자 프로그램에서 자주 보이는 문제는 한 함수가 너무 많은 일을 하는 것입니다. 예를 들어 사용자 입력도 받고, 계산도 하고, 출력도 하고, 오류 처리도 한 번에 하려 듭니다. 처음에는 편해 보여도 금방 엉킵니다.

가능하면 함수를 역할별로 나누어 보세요.

  • 입력 함수: 값을 읽는다.

  • 계산 함수: 결과를 만든다.

  • 출력 함수: 화면에 보여 준다.

예를 들어 짝수 판별 프로그램은 이렇게 나눌 수 있습니다.

#include <iostream>
using namespace std;

bool isEven(int number) {
    return number % 2 == 0;
}

void printResult(int number) {
    if (isEven(number)) {
        cout << number << "는 짝수입니다.n";
    } else {
        cout << number << "는 홀수입니다.n";
    }
}

int main() {
    int n;
    cin >> n;
    printResult(n);
    return 0;
}

여기서 isEven()은 오직 판별만 합니다. 화면 출력에 관심이 없습니다. 이런 분리는 앞으로 디버깅, 테스트, 재사용에서 큰 힘을 발휘합니다.

초보자가 자주 하는 실수

함수를 배우는 시점에서 많이 겪는 실수를 미리 짚어 보겠습니다.

괄호를 빼먹는 실수

greet;

이건 함수 호출이 아닙니다. 함수는 반드시 greet();처럼 괄호를 붙여야 호출됩니다.6

반환형과 반환값이 맞지 않는 실수

int hello() {
    cout << "Hellon";
}

int라고 써 놓았으면 정수 값을 return해야 합니다. 값을 돌려주지 않을 거라면 void를 써야 합니다.

void 함수의 결과를 변수에 넣으려는 실수

void greet() {
    cout << "Hin";
}

int main() {
    int x = greet(); // 오류
}

void 함수는 값이 없으므로 변수에 담을 수 없습니다.

지역 변수를 함수 밖에서 쓰려는 실수

함수 안에서 만든 변수는 함수 밖에서 보이지 않습니다. 이 규칙을 잊으면 "분명 만들었는데 왜 못 쓰지?"라는 상황이 자주 생깁니다.

함수 하나에 너무 많은 일을 몰아넣는 실수

함수 이름이 handleEverything()처럼 느껴진다면 거의 틀림없이 너무 많은 일을 하고 있습니다. 함수는 한 가지 책임에 집중할수록 이해하기 쉽습니다.8

작은 실전 예제: 계산기를 함수로 나누기

이번에는 함수를 실제로 나누는 감각을 익히기 위해 간단한 계산기 예제를 보겠습니다.

#include <iostream>
using namespace std;

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

void printMenu() {
    cout << "1. 더하기n";
    cout << "2. 빼기n";
    cout << "3. 곱하기n";
}

int main() {
    int choice;
    int x, y;

    printMenu();
    cin >> choice;
    cin >> x >> y;

    if (choice == 1) {
        cout << add(x, y) << 'n';
    } else if (choice == 2) {
        cout << subtract(x, y) << 'n';
    } else if (choice == 3) {
        cout << multiply(x, y) << 'n';
    } else {
        cout << "잘못된 선택입니다.n";
    }

    return 0;
}

이 프로그램은 아직 간단하지만, 이미 함수 분리의 장점이 보입니다. 계산 방식이 바뀌면 해당 함수만 고치면 되고, 메뉴 출력 형식을 바꾸고 싶으면 printMenu()만 고치면 됩니다.

스스로 점검해 보기

함수를 만들고 나면 다음 질문을 던져 보세요.

  • 이 함수 이름만 보고 무슨 일을 하는지 알 수 있는가?

  • 이 함수는 한 가지 일만 하고 있는가?

  • 필요한 값은 매개변수로 받고 있는가?

  • 결과는 반환값으로 자연스럽게 돌려주는가?

  • 함수 안에서만 쓸 변수는 지역 변수로 두었는가?

이 질문에 "대체로 그렇다"라고 답할 수 있다면, 이미 좋은 방향으로 가고 있는 것입니다.

연습 문제 아이디어

직접 손으로 짜 보며 익히는 것이 가장 좋습니다. 다음 문제를 함수로 나누어 작성해 보세요.

  • 두 정수를 받아 큰 값을 반환하는 함수 만들기

  • 정수를 받아 짝수인지 홀수인지 판별하는 함수 만들기

  • 섭씨를 받아 화씨로 바꾸는 함수 만들기

  • 1부터 n까지의 합을 반환하는 함수 만들기

  • 세 과목 점수를 받아 평균과 등급을 출력하는 프로그램을 여러 함수로 나누기

처음에는 함수가 오히려 번거롭게 느껴질 수 있습니다. 하지만 딱 몇 번만 직접 나누어 보면, 긴 코드를 한 덩어리로 두는 편이 더 불편하다는 걸 금방 느끼게 됩니다.

마무리

함수는 단순히 문법 하나를 배우는 장이 아닙니다. 생각을 나누는 법을 배우는 장입니다. 긴 코드를 작은 작업으로 분리하고, 필요한 값을 매개변수로 받고, 결과를 반환값으로 돌려주고, 변수의 범위를 스코프로 관리하는 습관은 앞으로 배우게 될 배열, 문자열, 클래스, STL, 객체지향까지 전부 이어집니다.

특히 초보자에게 함수는 "코드를 예쁘게 만드는 장식"이 아니라, 복잡함을 통제하는 가장 첫 번째 도구입니다. main()에 모든 것을 몰아넣는 습관을 조금씩 버리고, 한 문장으로 설명할 수 있는 작업마다 함수를 만들어 보세요. 그 순간부터 코드는 단순한 줄의 나열이 아니라, 읽히는 프로그램이 됩니다.

참고

1Functions in C++ - GeeksforGeeks

2C++ Function (With Examples) - Programiz

32.1 — Introduction to functions - Learn C++

4C++ Functions - W3Schools

52.4 — Introduction to function parameters and arguments - Learn C++

6Functions - C++ Users

7Scope - cppreference.com

8Functions in C++ - Definition, Parameters, Return Values, Overloading and Best Practices  CompilerSutra

책 목록

초보자를 위한 C++ 수업: 기초부터 현대 C++까지 한 권으로 끝내기

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기