Skip to main content
Summary

메서드는 코드에 이름을 붙이는 방법이다

앞 장에서는 if, switch, for, while을 사용해 프로그램의 흐름을 제어했다. 조건문과 반복문을 배우면 프로그램은 금방 길어진다. 처음에는 한 파일에 모두 작성해도 괜찮지만, 코드가 길어질수록 "이 부분이 무슨 일을 하는지" 한눈에 보기 어려워진다.

이때 사용하는 도구가 메서드다. 메서드는 특정 작업을 수행하는 코드 묶음에 이름을 붙인 것이다.

예를 들어 다음 코드는 두 상품 가격에 부가세를 더해 총액을 계산한다.

int price1 = 10000;
int price2 = 15000;

double taxRate = 0.1;
double total = (price1 + price2) * (1 + taxRate);

Console.WriteLine($"총액: {total:N0}원");

실행 결과는 다음과 같다.

총액: 27,500원

계산식이 한 번만 필요하다면 이 정도도 괜찮다. 하지만 같은 계산을 여러 곳에서 반복한다면 문제가 생긴다. 세율이 바뀌거나 계산 방식이 바뀔 때 모든 코드를 찾아 수정해야 한다.

메서드로 분리하면 계산 방식에 이름을 붙일 수 있다.

double total = CalculateTotalPrice(10000, 15000, 0.1);
Console.WriteLine($"총액: {total:N0}원");

double CalculateTotalPrice(int price1, int price2, double taxRate)
{
    return (price1 + price2) * (1 + taxRate);
}

CalculateTotalPrice라는 이름만 읽어도 "총액을 계산하는 코드"라는 뜻을 알 수 있다. 이것이 메서드를 쓰는 첫 번째 이유다. 코드를 줄이기 위해서만이 아니라, 코드에 의미 있는 이름을 붙이기 위해 사용한다.

메서드로 코드를 나누고 재사용하기AI 생성 이미지

메서드 선언과 호출

메서드는 보통 다음 모양으로 작성한다.

반환형 메서드이름(매개변수목록)
{
    // 실행할 코드
    return 반환값;
}

방금 본 예제를 다시 보자.

double CalculateTotalPrice(int price1, int price2, double taxRate)
{
    return (price1 + price2) * (1 + taxRate);
}

각 부분의 의미는 다음과 같다.

  • double: 이 메서드가 돌려주는 값의 자료형이다.

  • CalculateTotalPrice: 메서드 이름이다.

  • int price1, int price2, double taxRate: 메서드가 작업할 때 필요한 값이다. 이런 값을 매개변수라고 한다.

  • return: 계산 결과를 메서드를 호출한 곳으로 돌려준다.

메서드를 사용하는 쪽에서는 다음처럼 이름을 부른다. 이것을 메서드 호출이라고 한다.

double total = CalculateTotalPrice(10000, 15000, 0.1);

여기서 10000, 15000, 0.1은 메서드에 실제로 전달하는 값이다. 이런 값을 인수라고 한다. 매개변수는 메서드 선언 쪽의 변수이고, 인수는 호출할 때 넣는 실제 값이라고 구분하면 된다.

조금 더 작은 예제로 확인해 보자.

int result = Add(3, 5);
Console.WriteLine(result);

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

실행 결과:

8

Add(3, 5)가 실행되면 a에는 3, b에는 5가 들어간다. return a + b;가 실행되어 8이 호출한 곳으로 돌아온다. 그래서 result 변수에 8이 저장된다.

void 메서드와 값을 반환하는 메서드

메서드는 크게 두 종류로 나누어 생각할 수 있다.

첫째, 작업만 하고 값을 돌려주지 않는 메서드가 있다. 이때 반환형으로 void를 쓴다.

PrintWelcomeMessage("민수");

void PrintWelcomeMessage(string name)
{
    Console.WriteLine($"{name}님, 환영합니다!");
}

실행 결과:

민수님, 환영합니다!

PrintWelcomeMessage는 화면에 문장을 출력하는 것이 목적이다. 호출한 곳으로 어떤 값을 돌려줄 필요가 없으므로 void가 적절하다.

둘째, 계산 결과를 돌려주는 메서드가 있다. 이때는 int, double, bool, string 같은 반환형을 쓴다.

bool adult = IsAdult(20);
Console.WriteLine(adult);

bool IsAdult(int age)
{
    return age >= 19;
}

실행 결과:

True

IsAdult는 나이를 받아 성인 여부를 판단한다. 판단 결과가 필요하므로 bool 값을 반환한다.

구분 기준은 간단하다.

  • 화면 출력, 파일 저장, 상태 변경처럼 "행동"이 목적이면 void가 어울린다.

  • 계산, 판단, 변환처럼 "결과값"이 필요하면 값을 반환하는 메서드가 어울린다.

초보자가 자주 하는 실수는 계산 메서드 안에서 바로 출력까지 해 버리는 것이다.

void PrintDiscountedPrice(int price)
{
    int discounted = price - 1000;
    Console.WriteLine(discounted);
}

이 코드가 항상 나쁜 것은 아니다. 하지만 할인된 가격을 다른 계산에 다시 써야 한다면 불편하다. 그럴 때는 계산과 출력을 나누는 편이 좋다.

int discounted = GetDiscountedPrice(10000);
Console.WriteLine($"할인 가격: {discounted:N0}원");

int GetDiscountedPrice(int price)
{
    return price - 1000;
}

메서드 하나가 계산을 맡고, 출력은 호출한 쪽에서 처리한다. 이렇게 나누면 같은 계산 결과를 출력에도 쓰고, 결제 금액 계산에도 쓸 수 있다.

기본값 매개변수와 명명된 인수

메서드의 매개변수에는 기본값을 줄 수 있다. 호출할 때 값을 생략하면 기본값이 사용된다.

double total1 = CalculatePrice(10000);
double total2 = CalculatePrice(10000, 0.2);

Console.WriteLine($"기본 할인: {total1:N0}원");
Console.WriteLine($"20% 할인: {total2:N0}원");

double CalculatePrice(int price, double discountRate = 0.1)
{
    return price * (1 - discountRate);
}

실행 결과:

기본 할인: 9,000원
20% 할인: 8,000원

discountRate = 0.1처럼 기본값을 지정하면, CalculatePrice(10000)처럼 하나의 인수만 전달해도 된다. 기본값이 있는 매개변수는 보통 기본값이 없는 매개변수 뒤에 둔다. 위 예제에서도 반드시 받아야 하는 price를 먼저 쓰고, 생략할 수 있는 discountRate를 뒤에 두었다.

매개변수가 여러 개라면 명명된 인수를 사용할 수 있다. 명명된 인수는 호출할 때 매개변수 이름을 직접 쓰는 방식이다.

double total = CalculateOrderPrice(
    price: 20000,
    quantity: 3,
    discountRate: 0.15
);

Console.WriteLine($"주문 금액: {total:N0}원");

double CalculateOrderPrice(int price, int quantity, double discountRate = 0)
{
    return price * quantity * (1 - discountRate);
}

실행 결과:

주문 금액: 51,000원

명명된 인수는 인수의 의미를 분명하게 보여 준다. 특히 true, false, 0.1처럼 숫자나 논리값만 봐서는 의미가 약한 값을 전달할 때 유용하다.

// 의미가 덜 분명하다.
CreateUser("mina", true);

// 의미가 더 분명하다.
CreateUser("mina", isAdmin: true);

메서드 오버로딩

메서드 오버로딩은 같은 이름의 메서드를 매개변수 목록이 다르게 여러 개 만드는 기능이다. 같은 작업이지만 입력 형태가 다를 때 사용한다.

Console.WriteLine(Add(3, 5));
Console.WriteLine(Add(1.2, 3.4));
Console.WriteLine(Add(1, 2, 3));

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

double Add(double a, double b)
{
    return a + b;
}

int Add(int a, int b, int c)
{
    return a + b + c;
}

실행 결과:

8
4.6
6

세 메서드는 모두 이름이 Add지만 매개변수 목록이 다르다.

  • Add(int, int)

  • Add(double, double)

  • Add(int, int, int)

C#은 호출할 때 전달한 인수의 개수와 자료형을 보고 어떤 메서드를 실행할지 결정한다. 이때 중요한 기준은 메서드 이름과 매개변수 목록이다. 반환형만 다르게 만든 메서드는 오버로딩으로 구분할 수 없다.

다만 오버로딩을 남용하면 오히려 헷갈릴 수 있다. 같은 이름을 쓸 만한 이유가 있을 때만 사용하자. 의미가 다르면 다른 이름을 붙이는 편이 낫다.

// 좋은 편: 같은 덧셈 작업이고 입력 형태만 다르다.
// int Add(int a, int b) { ... }
// double Add(double a, double b) { ... }

// 애매한 편: 이름은 같지만 하는 일이 서로 다르게 느껴진다.
// void Process(string text) { ... }
// void Process(int userId) { ... }

위 예시는 실제 메서드 전체가 아니라, 오버로딩을 생각할 때 비교해 볼 수 있는 형태를 주석으로 적은 것이다. 이름이 같아도 독자가 "같은 종류의 일"이라고 받아들이기 어렵다면 다른 이름을 고민하는 편이 좋다.

값 형식 매개변수는 복사되어 전달된다

C#에서 int, double, bool, char, decimal 같은 값 형식은 메서드에 전달될 때 기본적으로 값이 복사된다.

다음 코드를 보자.

int number = 10;
ChangeNumber(number);
Console.WriteLine(number);

void ChangeNumber(int value)
{
    value = 99;
}

실행 결과:

10

ChangeNumber 안에서 value를 99로 바꿨지만, 바깥의 number는 그대로 10이다. number 자체가 넘어간 것이 아니라 값 10의 복사본이 value에 들어갔다고 생각하면 된다.

이 동작은 안전하다. 메서드 안에서 매개변수를 바꿔도 호출한 쪽의 변수가 갑자기 바뀌지 않기 때문이다.

하지만 가끔은 메서드 안에서 바깥 변수를 직접 바꿔야 할 때가 있다. 이때 ref나 out을 사용할 수 있다.

ref와 out은 필요할 때만 사용한다

ref는 변수를 참조로 전달한다. 메서드 안에서 값을 바꾸면 호출한 쪽 변수도 바뀐다.

int number = 10;
ChangeNumber(ref number);
Console.WriteLine(number);

void ChangeNumber(ref int value)
{
    value = 99;
}

실행 결과:

99

ref를 사용할 때는 호출하는 쪽과 선언하는 쪽 모두에 ref를 써야 한다. 또한 ref로 전달하는 변수는 미리 값이 들어 있어야 한다.

out은 메서드가 값을 만들어 바깥으로 내보낼 때 사용한다. TryParse가 대표적인 예다. 앞 장에서 문자열을 숫자로 바꿀 때 다음 형태를 보았다.

string input = "25";

bool success = int.TryParse(input, out int age);

if (success)
{
    Console.WriteLine($"나이: {age}");
}

실행 결과:

나이: 25

직접 out을 사용하는 메서드도 만들 수 있다.

bool success = Divide(10, 2, out double result);

if (success)
{
    Console.WriteLine($"결과: {result}");
}
else
{
    Console.WriteLine("0으로 나눌 수 없습니다.");
}

bool Divide(double left, double right, out double result)
{
    if (right == 0)
    {
        result = 0;
        return false;
    }

    result = left / right;
    return true;
}

실행 결과:

결과: 5

out 매개변수는 메서드가 끝나기 전에 반드시 값을 넣어야 한다. 위 코드에서 right == 0인 경우에도 result = 0;을 넣은 이유가 여기에 있다.

ref와 out은 강력하지만 자주 쓸 문법은 아니다. 값을 하나만 돌려주면 충분한 경우에는 return을 쓰는 편이 읽기 쉽다. out은 "성공 여부와 결과값을 함께 알려줘야 하는 경우"처럼 목적이 분명할 때 사용하자.

하나의 메서드는 하나의 책임을 갖게 한다

메서드를 잘 나누는 기준은 "한 메서드가 한 가지 일을 하는가?"이다. 이것을 어렵게 생각할 필요는 없다. 메서드 이름을 붙여 보았을 때 자연스러우면 대체로 괜찮다.

다음 코드는 사용자에게 점수를 입력받고, 숫자로 변환하고, 등급을 계산하고, 출력까지 한곳에서 처리한다.

Console.Write("점수 입력: ");
string? input = Console.ReadLine();

if (int.TryParse(input, out int score))
{
    string grade;

    if (score >= 90)
    {
        grade = "A";
    }
    else if (score >= 80)
    {
        grade = "B";
    }
    else if (score >= 70)
    {
        grade = "C";
    }
    else
    {
        grade = "D";
    }

    Console.WriteLine($"등급: {grade}");
}
else
{
    Console.WriteLine("숫자를 입력하세요.");
}

작동은 하지만 코드가 길다. 등급 계산만 다시 사용하기도 어렵다. 등급 계산을 메서드로 분리해 보자.

Console.Write("점수 입력: ");
string? input = Console.ReadLine();

if (int.TryParse(input, out int score))
{
    string grade = GetGrade(score);
    Console.WriteLine($"등급: {grade}");
}
else
{
    Console.WriteLine("숫자를 입력하세요.");
}

string GetGrade(int score)
{
    if (score >= 90)
    {
        return "A";
    }
    else if (score >= 80)
    {
        return "B";
    }
    else if (score >= 70)
    {
        return "C";
    }
    else
    {
        return "D";
    }
}

이제 메인 흐름은 더 읽기 쉬워졌다.

  1. 입력을 받는다.

  2. 숫자로 변환한다.

  3. 등급을 계산한다.

  4. 출력한다.

GetGrade는 등급 계산만 책임진다. 나중에 등급 기준이 바뀌면 이 메서드만 보면 된다.

메서드를 나눌 때는 다음 질문을 해 보자.

  • 같은 코드가 두 번 이상 반복되는가?

  • 코드 블록에 설명 이름을 붙일 수 있는가?

  • 한 메서드 안에서 입력, 계산, 출력, 저장이 너무 많이 섞여 있는가?

  • 조건문이나 반복문이 길어져서 핵심 흐름이 보이지 않는가?

이 질문 중 하나라도 "그렇다"면 메서드 분리를 고려할 만하다.

흔한 실수

첫째, 반환형과 return 값의 자료형이 맞지 않는 경우다.

int GetName()
{
    return "Mina"; // 오류: string을 int로 반환할 수 없다.
}

문자열을 반환하려면 반환형도 string이어야 한다.

string GetName()
{
    return "Mina";
}

둘째, 값을 반환하는 메서드에서 모든 경로가 값을 반환하지 않는 경우다.

string GetPassMessage(int score)
{
    if (score >= 60)
    {
        return "합격";
    }
    // score가 60 미만이면 반환값이 없다.
}

else를 추가하거나 마지막에 기본 반환값을 두어야 한다.

string GetPassMessage(int score)
{
    if (score >= 60)
    {
        return "합격";
    }

    return "불합격";
}

셋째, 메서드 이름이 너무 모호한 경우다.

void DoIt()
{
    // 무엇을 하는지 이름만으로 알기 어렵다.
}

PrintMenu, CalculateTotal, GetGrade, IsAdult처럼 동작과 결과가 드러나는 이름을 쓰자.

넷째, 메서드가 너무 많은 일을 하는 경우다. ReadScoreAndCalculateGradeAndPrintResult 같은 이름이 필요해진다면 메서드를 나눌 신호다.

작은 연습 문제

  1. 두 정수 a, b를 받아 더 큰 값을 반환하는 GetMax 메서드를 작성해 보세요.

  2. 상품 가격과 수량을 받아 총액을 반환하는 CalculateTotal 메서드를 작성해 보세요. 수량의 기본값은 1로 두세요.

  3. 이름을 받아 안녕하세요, {이름}님!을 출력하는 PrintGreeting 메서드를 작성해 보세요. 이 메서드는 값을 반환해야 할까요, void여야 할까요?

  4. 문자열을 받아 정수 변환에 성공하면 true를 반환하고 결과를 out으로 내보내는 TryReadNumber 메서드를 만들어 보세요.

  5. 앞 장에서 작성한 조건문 또는 반복문 예제 중 하나를 골라, 계산 부분을 별도 메서드로 분리해 보세요.

메서드는 긴 코드를 짧게 보이게 만드는 장식이 아니다. 프로그램의 생각 단위를 나누는 방법이다. 잘 나눈 메서드는 이름만으로 의도를 설명하고, 같은 코드를 반복하지 않게 하며, 수정해야 할 위치를 줄여 준다. 다음 장에서는 여러 값을 함께 다루기 위해 배열, 문자열, 기본 컬렉션을 사용하는 방법을 살펴본다.

책 목록

C# 학습: 기초 문법부터 작은 프로젝트까지

전체 목차 보기

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

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

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

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

참여하기